Skip to content

feat(cli,core): the organization-ownership ceremony promotes customized email templates and moves the settings global rung (C7b, PR 3) - #23195

Closed
objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/issue-22937-ceremony-promotions
Closed

objectstack-fleet[bot] wants to merge 4 commits into
mainfrom
claude/issue-22937-ceremony-promotions

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #22937
Clause-②: yes (widening)
Stacked on PR #23034: until it lands, this diff also carries its hunks; this PR lands only after PR #23034.

PR 3 of the C7b build-ahead card. It applies the maintainer's ruling 6138591940 on Q3 and Q4 (both A) under dispatch 6139305919. PR #23034 carries the card's closing line; this one closes no card. It stays a draft until #15204 stage 8 is on main and PR #23034 has landed. A contract-tier review is owed before the queue.

What it adds (671 changed lines against PR #23034's head, merge of main excluded)

  1. emailTemplateDefinitionFromRow in packages/core/src/security/, beside permissionSetDefinitionFromRow / positionDefinitionFromRow.
    • A pure mapper: a sys_email_template row in, the email_template body out.
    • It imports nothing, so the lean-entry question does not arise.
    • The pin holds the body to EmailTemplateDefinitionSchema: no undeclared key, and a full safeParse passes.
  2. template-promotion, a ceremony step (Q3, ruling C on ADR-0131 §6 Q1).
    • What it promotes: every row an admin customized (customized: true) or authored (managed_by not a seeder). Each becomes an environment email_template item through the real metadata door (protocol.saveMetaItem), one per (name, locale) bundle member, before mirror deletion.
    • What it skips: seeded, uncustomized copies (NULL residue, or an organization's copy) are copies of code. They are not promoted, and they are deleted as mirrors.
    • When it stops: on rows of one (name, locale) that differ; on an existing environment template that differs (the environment's edit is never overwritten); or on a door refusal.
    • Under a wall: the rows are reported and kept, because one organization's edit would become every organization's.
    • Idempotent: a promotion that already landed is recognized and skipped. This was measured against the real stored row.
  3. settings-move, a ceremony step (Q4, feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207's ruling 6043540291). It executes the inventory's global-rung-move departure.
    • Each sys_setting row with scope = 'global' is copied into sys_platform_setting by INSERT … SELECT, so value and the value_enc secret handle move verbatim. The row then leaves sys_setting.
    • Already moved: a key sys_platform_setting already holds with an equal value is only removed from sys_setting.
    • When it stops: on a key held there with a different value (the one the runtime serves), or on differing source rows. It moves nothing for that key.
    • Secret handles stay referenced: the secret reference union (secret-reference-union.ts) already reads both holder tables.
  4. Mirror deletion now discharges the template categories under single once the promotion has run. Every remaining template row is then a mirror. Under a wall the promotion population is excluded from deletion as well.
  5. The report gains movedOut / movedIn per table, and counts reconcile as before − deleted − moved out + moved in.

Step order: attribution → authored-conversion → template-promotion → settings-move → rewrite → mirror-deletion → column-drops.

Q4's sharing-rule mirrors: measured, not clean, not built

The sys_sharing_rule declared-rule mirrors stay as the departure the plan reports. The measurement, on main at e81cae0149:

  • the declared-rule seeder is still live: plugin-sharing/src/sharing-plugin.ts:768 calls bootstrapDeclaredSharingRules on every boot;
  • the rule service evaluates rules from the rows themselves;
  • it materialises grants as sys_record_share rows with source = 'rule' and source_id = rule.id (sharing-rule-service.ts:231, writes at :1081 / :1102);
  • at boot, sweepOrphanedRuleGrants (:845-880) revokes every rule grant whose source_id names no rule row.

So deleting a declared-rule row today revokes its grants at the next boot. The seeder then re-seeds the rule under a new id and reconcile re-grants under that id. The row is the runtime's source of truth, not a mirror of code, until D2 retires the seeder. The removal belongs after that retirement; the report carries it as an open question.

Tests and evidence

  • Core: email-template-row-definition.test.ts (5) and the whole src/security/ directory: 39 files, 877 tests passed.
  • CLI: 5 files, 54 tests passed:
    • organization-ownership-apply.test.ts, now 10 cases. The acceptance fixture gains four template rows (a seeded mirror, an organization's seeded copy, a customized seeded row and an admin-authored row) and the settings rung. Two new cases: the settings-conflict blocker, and the "never overwrite an environment template" blocker. The resume and gate cases were updated for the new step order.
    • organization-ownership.apply-flags.test.ts;
    • the plan and inventory tests;
    • vitest-tiers-partition.
  • One-shot family pin: -t "organization-ownership|every module", 6 passed.
  • Typecheck: @objectstack/core and @objectstack/cli (tsc plus test-typecheck), both exit 0.
  • Ablations through scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty:
    • the settings value guard disabled: the settings-conflict case goes red;
    • the template door save removed: the acceptance case goes red.
  • End to end with the built CLI on today's composition:
    • the real metadata door stored email_template/crm.renewal in sys_metadata;
    • the global row moved to sys_platform_setting, and the template rows were deleted after the promotion;
    • a second run recognised the stored promotion (alreadyPromoted: 1).
  • Gates: dispatch-gates --commands derived 68 at this head, and --ran reports "68 derived, 68 run, 0 NOT-MEASURED (a DERIVED zero)".
    • check:dual-build-cjs-loads and check:i18n-coverage first exited 3 on unbuilt sibling dists in this fresh worktree. They were built under the verify lock and re-run green.
    • check:type-check-debt hit this runner's 400 s per-gate timeout. Re-run alone, it exits 0.

Also in this diff

organization-ownership.apply-flags.test.ts (from PR #23034) now pays the command's first load in beforeAll and gives each case 60 s. On the merged tree, its first case crossed vitest's 5 s default (5,011 ms), which also broke the second case's stdout capture. PR #23034 carries the same file at 4.4 s. If CI ever reddens it there, this is the fix.

Not touched

packages/plugins/plugin-security/**, the files of D / S8-T / S8, packages/spec, and organization-ownership-inventory.ts. The departure is executed from its own data.


Generated by Claude Code

…10 ceremony's apply step and post-check

The read-only plan gains its writing mode. --apply asks about the backup
first (--backup-taken, or a yes at the prompt; refused non-interactively),
then performs the fates in order, stopping at the first blocker:

1. attribution — the owners the plan's own walk derives (anchors, else the
   Default Organization under single), to a fixed point; an unattributable
   row is never assigned and stays listed by id;
2. authored catalog rows — permission sets and positions become environment
   metadata through the metadata door via core's row-to-definition mappers,
   with position bindings carried into permissionSets (single only);
3. the verified id-to-name rewrite of sys_user_permission_set.permission_set,
   then every grant verified to name a set the security catalog resolves
   (permission_set_id stays);
4. mirror deletion, gated on that verification read again before the first
   delete; only mirror rows no named category claims;
5. column drops through the schema-drift ceremony's drop_column entries.

Every write is conditioned on the state it changes, and each finished step
is checkpointed in sys_migration (adr-0131-organization-ownership), so an
interrupted run resumes at its step. The post-check re-reads the plan; the
ledger row's applied_at / verified_at are the ceremony's marker.

The acceptance fixture carries every fate: effective access identical, with
a positive control; mirrors gone; attributed rows stamped; the orphan row
present and reported; row counts reconciling.

Claude-Session: https://claude.ai/code/session_01DjKf52bcyXa3fbGW723phR
Co-authored-by: Claude <noreply@anthropic.com>
… as the email_template definition it carries

The pure row-to-definition mapper beside permissionSetDefinitionFromRow and
positionDefinitionFromRow, for the v18 ceremony's promotion of customized
template rows (ADR-0131 D10, ruling C on section 6 Q1). No import, so the
lean entry carries it. Pinned against EmailTemplateDefinitionSchema.

Claude-Session: https://claude.ai/code/session_01DjKf52bcyXa3fbGW723phR
Co-Authored-By: Claude <noreply@anthropic.com>
… the settings global rung

Two steps of `os migrate organization-ownership --apply`, per the
maintainer's ruling on #22937 (Q3, Q4 → A), checkpointed and resumable
like the others:

- template-promotion: customized and admin-authored sys_email_template
  rows become environment email_template items through the metadata
  door (core's emailTemplateDefinitionFromRow) before mirror deletion;
  never over an environment template that differs; reported under a wall.
- settings-move: the inventory's global-rung-move departure, executed:
  sys_setting scope = global rows copied into sys_platform_setting by
  INSERT ... SELECT, then removed; a key already served with a different
  value is a blocker.

The sys_sharing_rule declared-rule mirrors are measured and not removed:
the declared-rule seeder is still live and rule grants key on the row id.

Claude-Session: https://claude.ai/code/session_01DjKf52bcyXa3fbGW723phR
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/core, touching 124 documentable anchor(s).

27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json d4aaeaf0e2eb39ac574f1ba171a88535f9a678b4.

⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: organization_id (literal, 35 pages)
  • 62 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 46 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d4aaeaf0e2eb39ac574f1ba171a88535f9a678b4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 57ff8bc838e76ef424fe364396543a55b2ef66df — the merge of head 49d85fc6538769d536c7e0d4699219ad3f31fa27 into base d4aaeaf0e2eb39ac574f1ba171a88535f9a678b4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 57ff8bc838e76ef424fe364396543a55b2ef66df && git checkout 57ff8bc838e76ef424fe364396543a55b2ef66df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d4aaeaf0e2eb39ac574f1ba171a88535f9a678b4 49d85fc6538769d536c7e0d4699219ad3f31fa27 && git checkout -B drift-repro d4aaeaf0e2eb39ac574f1ba171a88535f9a678b4 && git merge --no-ff 49d85fc6538769d536c7e0d4699219ad3f31fa27

node scripts/docs-audit/affected-docs.mjs --json d4aaeaf0e2eb39ac574f1ba171a88535f9a678b4

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs d4aaeaf0e2eb39ac574f1ba171a88535f9a678b4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Closed unmerged by domain:engine seat 2 · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-13T07:14Z: a stacked PR is not a supported form (AGENTS.md, Multi-agent discipline §2: "⛔ Off main is literal"). The branch claude/issue-22937-ceremony-promotions at 49d85fc653 stays. When PR #23034 lands, main is merged into the branch and this PR is reopened with the diff off main. The contract-tier review runs on that head. Record and answers: 6140351542 on #22937.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants