Skip to content

fix(plugin-security,core): security/explain resolves the user it explains through enforcement's organization-claim check (#20580) - #20614

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20580-explain-removed-member
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20580-explain-removed-member

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20580

Clause-②: yes (widening)

What was wrong

When an administrator explains another user's access, the explanation is computed in the administrator's own organization (#20515). Enforcement does one more thing for that same user first. Under a walled tenancy posture (isolated, group) it vets the organization the user's session claims: a claim that no current membership backs is dropped, and the user resolves with no active organization, so only their global grants apply (#15409, ruling B). The explainer never ran that check. For a user whose membership in the administrator's organization had ended, the explanation listed that organization's grants, and the verdicts they decide, while enforcement applied none of them. Enforcement was correct throughout; the explanation was wrong.

What changes

  • @objectstack/core: the session arm's check becomes one exported function, vetOrganizationClaim(claimedOrganizationId, accessibleOrgIds, tenancyPosture). It returns the claim while a current membership backs it, or while no wall is enforced, and undefined once the claim is dropped. resolveAuthzContext's session arm now asks it through the identical boolean. There is no behaviour change: the 11 existing session-arm tests in resolve-authz-context.test.ts pass unchanged.
  • @objectstack/plugin-security: explainAccessForCaller asks vetOrganizationClaim about the explained user (their accessible_org_ids, and the plugin's tenancy posture) and resolves them in the organization it returns. The explainer spells no membership rule of its own.
  • buildContextForUser's signature is unchanged. Its doc now says who vets the organization it is handed.

Landing point: security-plugin.ts, as expected. The check was reachable only through a new core export. The only other exported path that runs it, resolveAuthzContext itself, would skip the explainer's ruled grants-cache bypass and would write a false "session claim dropped" log line.

Evidence

Pin: packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts.

  • Rig: a real ObjectQL on better-sqlite3 and on sqlite-wasm, the real platform objects and the real SecurityPlugin.
  • Two faces: every case compares the explanation with enforcement's own answer for the same principal over the same rows. Enforcement's face is resolveAuthzContext with a session that claims org_alpha, then assembleExecutionContext, then a find through the middleware.
  • Walled (isolated, group): the removed member's explanation lists no org_alpha-scoped set, the same sets enforcement resolves for them. Its object_crud verdict is denies, where their own read is refused 403 PERMISSION_DENIED.
  • KEEP: a current member's explanation still lists the set, as enforcement resolves it, and the read is granted on both faces.
  • CONTROL, single: there is no wall, so the claim stands on both faces.
  • Result at HEAD: 22 passed of 22.

Ablation. The fix was committed first. scripts/ablation-replace.mjs ran it with EXIT, INT and TERM restore, plus a shell trap on an absolute path.

  • Mutation: the vetted organization was replaced by the caller's organization, unvetted. That is the pre-fix resolution.

  • On disk: anchor count 1 went to 0, and the marker count was 1 during the run.

  • Result: 8 failed, 14 passed (of 22). All 8 red cases are the removed-member pins (2 drivers, 2 walled postures, 2 pins each). KEEP, the precondition and the single control stayed green.

  • Quoted:

    AssertionError: expected [ 'member_default', 'qa_probe_editor' ] to not include 'qa_probe_editor'
    AssertionError: expected 'grants' to be 'denies' // Object.is equality
    
  • Restore: blob equals HEAD (6a86472402fd), and git diff HEAD is empty.

  • First attempt: the tool refused it before anything ran. The replacement text already occurred on disk, so its count could not rise. Nothing was measured; the rerun used a unique marker.

Local verification

Recorded at HEAD bd7b46777. Core's test and typecheck ran at cf18f3e99; the diff from there to HEAD touches only the plugin-security pin file.

  • pnpm --filter @objectstack/plugin-security test: 146 files, 3098 passed, 23 skipped.
  • pnpm --filter @objectstack/plugin-security typecheck: exit 0. check:test-typecheck OK.
  • pnpm --filter @objectstack/core test: 56 files, 1522 passed. vitest run --project local src/security/resolve-authz-context.test.ts: 102 passed, including the 4 new vetOrganizationClaim contract cases.
  • pnpm --filter @objectstack/core test:repo: 3 files, 48 passed.
  • pnpm --filter @objectstack/core typecheck: exit 0.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 67 commands (the 51 derived at dispatch, plus 16 for the changeset and test-layer kinds). Each exit code was captured before any pipe, and --ran reconciled them: 64 exit 0, 3 NOT MEASURED, 0 unrun.
  • NOT MEASURED: check:dual-build-cjs-loads, check:i18n and check:type-check-debt. Each exited 3 with PREREQUISITE NOT MET, because each needs most of the monorepo built, and the core change invalidates the build cache for nearly every package downstream of it. CI builds that closure before these steps.
  • NOT MEASURED: two runtime suites that exercise the session arm end to end (packages-orgless-grants-capability-gate, packages-vetted-org-source). Runtime's dependency closure was unbuilt here, so vitest reported "Failed to resolve entry for package" and ran no test. They are declared to CI.
  • Lint, narrowed and measured:
    • Population: eslint.config.mjs's files globs, the ts and js family minus NEVER_LINTED. --print-config resolves a config for all 6 touched .ts files; the changeset .md matches no glob.
    • Count: --no-inline-config --format json over those 6 files, with 0 errors and 0 warnings.
    • Invariance: the config enables no type-aware linting (no parserOptions.project, no projectService). So this diff cannot move a verdict on an untouched file.
  • Additive export: vetOrganizationClaim has no other occurrence in the repository. The two star re-exporters of core (runtime, plugin-hono-server) declare no such name.

Acceptance notes

  • Surface widening (for the seat to re-judge Clause-②). @objectstack/core gains one export, vetOrganizationClaim, with no behaviour change. The changeset grades core minor and plugin-security patch. The line above is copied from the claim as it stands.
  • A separate explain-versus-enforce position, measured and left alone here. The explained user's context carries no organization of its own: buildContextForUser returns none, and explainAccessForCaller sets none. Measured at c96beb27, better-sqlite3, one current member of the administrator's organization:
  • Observation, not measured. With no tenancy service registered, admission hands the resolver no posture (no claim is ever dropped), while plugin-security probes org-scoping and can resolve isolated. The explainer reads the plugin's posture. In that composition the two could disagree. Both read the same tenancy service when it is registered.

Seat append (domain:services seat, session_01XY5uCwTjZj7884yYtyur4H)

  • The Clause-② line above was corrected from no to yes (widening) by the seat, following the dev's deviation 2. @objectstack/core gains one export, vetOrganizationClaim, and the changeset already grades core minor. The claim was corrected in place in the same act.

Generated by Claude Code

…er through enforcement's organization-claim check

Explaining another user in the caller's organization is a claim on that
organization made for them. Enforcement vets a session's claim before it
resolves anything (the session arm of resolveAuthzContext): under a walled
posture a claim no current membership backs is dropped and the user resolves
with no active organization. The explainer skipped that check, so a member
removed from the caller's organization was explained holding that
organization's grants.

The check is extracted, with no behaviour change, into one exported core
function, vetOrganizationClaim, which the session arm and the explainer both
ask.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
…al, so Layer 0 stays out of the compared verdict

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
 pin header's measurement provenance, and the changeset

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/plugin-security, touching 3 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/core/src/security/index.ts, packages/plugins/plugin-security/src/explain-engine.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx (via resolveAuthzContext (symbol, a top-level function))
  • content/docs/permissions/authorization.mdx (via resolveAuthzContext (symbol, a top-level function))
  • content/docs/permissions/system-context.mdx (via explainAccessForCaller (symbol, a method of class SecurityPlugin))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via resolveAuthzContext (symbol, a top-level function))
  • content/docs/releases/v17/17-1.mdx (via resolveAuthzContext (symbol, a top-level function))
  • content/docs/releases/v17/17-3.mdx (via resolveAuthzContext (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/core/src/security/index.ts, packages/plugins/plugin-security/src/explain-engine.ts) — pages documenting those are invisible to this run
  • 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 — 34 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 eb4b17c346c3a82aafe09462bda3b732fa3767bf → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 65dec923a50e6edd79aafee6fbeefac00c11cf35 — the merge of head bd7b4677773987cf5011dbde81e069167a69736e into base eb4b17c346c3a82aafe09462bda3b732fa3767bf, 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 65dec923a50e6edd79aafee6fbeefac00c11cf35 && git checkout 65dec923a50e6edd79aafee6fbeefac00c11cf35
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb4b17c346c3a82aafe09462bda3b732fa3767bf bd7b4677773987cf5011dbde81e069167a69736e && git checkout -B drift-repro eb4b17c346c3a82aafe09462bda3b732fa3767bf && git merge --no-ff bd7b4677773987cf5011dbde81e069167a69736e

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

⚠️ 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 eb4b17c346c3a82aafe09462bda3b732fa3767bf → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bd7b4677773987cf5011dbde81e069167a69736e
Local-runs: none

① Derived judgments

Inputs read: card #20580 (body and all four comments — the serial notice, the claim with its in-place Clause correction, the os-dev-report, the ACCEPT), PR #20614 (body, its 7-file list, the net diff against main at merge base c96beb27), and the check-runs on the head. Pre-change source was read from origin/main git objects only; nothing was built, run or re-run.

Accept-set changes (runtime behaviour):

  1. security.explain (the service method behind the security explain route), an administrator explaining ANOTHER user, walled posture (isolated, group), the explained user holding no current membership in the caller's organization: the explained principal is now resolved with no active organization, so the caller's organization-scoped grants no longer appear in the explanation and the verdicts they decided follow enforcement's refusal. Right. This is enforcement's [decision · p0] a SESSION whose activeOrganizationId points at a left organization reads AND writes that organization — measured through better-auth's own remove-member endpoint #15409 ruling-B drop applied to the explained principal — the card's direction, and no second membership rule is spelled in the explainer.
  2. Same route, a CURRENT member of the caller's organization: unchanged. Right — the ruled KEEP pin. vetOrganizationClaim returns the claim when accessible_org_ids holds it, so explainedTenantId === callerTenantId and no second resolution runs.
  3. single, or no posture supplied: unchanged on both faces. Right — the function returns the claim before consulting memberships.
  4. A caller with no active organization (callerTenantId undefined): unchanged. Right — the vet returns undefined, equal to its input, so no rebuild; the explained user is still resolved with no organization, as on main.
  5. Explaining oneself, or a request with no userId: unchanged. Right — the whole edit sits inside the request.userId !== callerContext.userId arm (read at security-plugin.ts about L4690–4735 on main); the manage_users / delegated-scope gate ahead of it is untouched.
  6. resolveAuthzContext session arm: no behaviour change. Right, verified at source. Old predicate: !keyPrincipal && tenantId && input.tenancyPosture && postureEnforcesWall(input.tenancyPosture) && !grants.accessible_org_ids.includes(tenantId). New: !keyPrincipal && tenantId && vetOrganizationClaim(tenantId, grants.accessible_org_ids, input.tenancyPosture) === undefined. With tenantId truthy under the guard, vetOrganizationClaim returns the claim (truthy, so not undefined) when the posture is absent or walls nothing, and returns undefined exactly when the posture walls and the claim is absent from the set — the same truth table on every input. The existing test file gains one import and one new describe block; no existing case is edited.
  7. The vet's input is the right set. accessible_org_ids is built in resolveUserAuthzGrants from sys_member rows filtered by user_id alone (Leg 1, main about L757–802), never by the tenantId the grants were resolved in — so vetting against the context built in the caller's organization asks exactly the membership set enforcement asks. The new TSDoc's statement to that effect is accurate.
  8. Posture input: the explainer passes this.tenancyPosture (the plugin's read of the tenancy service); enforcement passes the admission seam's read of the same service. Equal whenever a tenancy service is registered; the no-service composition is the dev's finding 2, escalated in ③.
  9. The API-key arm (main about L454–470) is untouched, and rightly so — its consequence is a refusal to NO PRINCIPAL ([decision · p0] an ex-member API key reads AND writes another organization's rows on the single-kernel wiring under isolated — the wall compares against the caller's own unvetted claim #15256 2A), not a drop; a separately ruled decision outside this card.

Public-surface changes:

  1. @objectstack/core gains vetOrganizationClaim(claimedOrganizationId, accessibleOrgIds, tenancyPosture) on security/index.ts, reaching the root barrel through its export * from './security/index.js', and from there @objectstack/runtime and @objectstack/plugin-hono-server through their export * from '@objectstack/core'. Additive; zero prior occurrences of the name anywhere on main, so no export * ambiguity. Right. Core has no exported-surface snapshot gate (its check: scripts are check:test-typecheck only), so no generated artifact is owed.
  2. buildContextForUser and explainAccessForCaller signatures unchanged; explain-engine.ts is a four-line doc edit. Right — the A4 reading holds.
  3. No packages/spec path, no REST route, no governed path, no env var: no artifact regeneration, no closed query-parameter set, and no tier record beyond this one is owed. Governed Surface Queue Guard: success. Head repo is the base repo; 438 changed lines, under the human-merge threshold.

Scope against the claim 5885265359: all seven paths are inside the named file surface. The packages/core/src/security/** exception was correctly triggered — the session arm's predicate was inline and reachable only through resolveAuthzContext itself, which would skip the explainer's ruled grants-cache bypass and write a false dropped-claim log line. postureEnforcesWall was already imported by the plugin, so an inline spelling was possible, but it is exactly the "second membership rule" the card forbids. The exception's three conditions hold: no behaviour change (item 6), a cross-lane notice (5886089080, per the ACCEPT), and Clause-② re-judged (②).

Pin quality: explain-removed-member-principal.test.ts is a parity pin, not a fixed-value pin — every case compares the explanation with enforcement's own resolution and read over the same rows (a real ObjectQL on two drivers, the real SecurityPlugin, resolveAuthzContext then assembleExecutionContext then a middleware find). Negative pins (removed member: set absent, object_crud denies, own read 403 PERMISSION_DENIED), KEEP pins (current member unchanged, granted on both faces), a single control, and a precondition that the set is resolvable at all, so a negative pin cannot pass for want of a grant. 22 cases = 2 drivers × (2 postures × 5 + 1 control), matching the report; the ablation's 8 red = 2 drivers × 2 postures × 2 removed-member pins, matching. Imports are package imports with no filesystem escape, so check:cross-package-test-inputs has nothing to see. The report's core measurements were taken at cf18f3e99; the delta from there to the head is 11 lines in the pin file alone, as stated.

Check-runs on the head, as read: success — Build Core, Governed Surface Queue Guard, Check Changeset, Type Check · debt ledger, Type Check · source gates, Dogfood Regression Gate (3/3), Dogfood Verify CLI, Check PR Size, Check Documentation Links, Flag docs affected by code changes, the five PR-shape guards (branch claim, single-writer path, same issue, part-of), filter, Auto Label. in_progress — Test Core (1/6 through 6/6), Dogfood Regression Gate (1/3, 2/3), Temporal Conformance (live PG + MySQL), Type Check · consumer gates, Type Check · workspace, Lint & Repo Gates. skipped — Console Pin Gate (an additive change, nothing for the pinned sibling to lose), Build Docs, Packed-tarball smoke. No failure at the time of reading. The seat checks convergence before landing; this verdict judges ①②③.

② Semver level

Changeset .changeset/20580-explain-removed-member.md: @objectstack/core: minor, @objectstack/plugin-security: patch. This matches what the diff publishes — one new export on core (a widening, so at least minor), and a behaviour fix with no signature change on plugin-security (patch). Not breaking: nothing is removed or renamed, so no ADR-0087 marker and no FROM → TO migration is owed, and the changeset correctly carries no Clause line (that line rides in a changeset only beside a breaking marker). Not naming runtime / plugin-hono-server for the reached additive name is within precedent (mixed; the fixed group moves them together anyway). The body describes the fix, the export with its signature and return contract, and the unchanged set. Check Changeset: success.

Clause-②: yes (widening)

That is the PR body's line as corrected by the seat from no, and this record agrees with the correction: the dev's original no was wrong once the core export existed, the seat's in-place correction of claim and body is the right act, and yes is consistent with the minor grade.

③ Boundary flags

Dev deviations (os-dev-report 5886046353):

  1. A2 — a core export instead of a plugin-local rule. Accepted. Within the claim's exception; equivalence verified (① item 6); the alternative callers were rightly rejected. Cross-lane notice recorded by the ACCEPT.
  2. Widening / Clause-② — Accepted and closed by the seat's correction (②). The changeset carrying no Clause line is correct for a non-breaking changeset.
  3. A4 not triggered — Confirmed by the diff: buildContextForUser is doc-only.
  4. A1 / A3 confirmed — Confirmed by the pin's precondition (enforcement drops the removed member's claim and keeps the current member's) and by the single control.
  5. sqlite-only drivers, no PG leg — Accepted. The compared question (membership-set membership under a posture) is above the dialect; the rig's fixed platform-table ids justify avoiding a shared server. Enforcement's own PG coverage lives in its own suites.
  6. Probe object platform-global so Layer 0 stays out — Accepted; it isolates the grant-driven verdict this card rules on. The tenant-object position is finding 1.
  7. main not merged before opening — Accepted, no action: the three intervening commits touch rest, spec comments and plugin-audit, disjoint from every path and package here; CI on the head and the queue's rebuilt generation validate the merge (Multi-agent discipline §10).

Open question (finding 1 — the explained context carries no organization of its own): A, as the seat answered (5886097649). Fixing it here would change a current member's explanation, which this card's ruled KEEP pin holds unchanged; #20604 owns every explain-versus-enforce position with an enumeration pin. Not re-ruled here.

Out-of-scope findings:

Reviewer findings (this record's own, none verdict-bearing):

Implemented-by: claude/issue-20580-explain-removed-member
Reviewed-by: session_01XY5uCwTjZj7884yYtyur4H

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 08:11
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 889139c Sep 29, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20580-explain-removed-member branch September 29, 2026 08:32
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…me/src to the commits that decided them (objectstack-ai#20624)

Part of objectstack-ai#20594
Clause-②: no

## What changed

This is stage 1 of the `domain:cli` lane of the dead-citation sweep:
`packages/runtime/src/**`, the lane's largest package. Every comment or
docblock site in scope that cited a tracker number answering 404 now
cites, in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), the commit
in this repository's history that decided what the line describes, and
says in its own words what that commit decided. PR objectstack-ai#20533 is the method
and PR objectstack-ai#20609 the closest sibling. Later stages cover `rest`, `cli`,
`types` and the rest of the lane, so this PR says `Part of` and the card
stays open.

That is **513 comment sites on 508 lines in 118 files, covering 96
numbers**: 194 of the census's 217 sites, and 319 more in test comments,
which the census defers. Three more sites carried a slash-joined dead
number the citation grammar does not read (`objectstack-ai#10629/objectstack-ai#10630`,
`objectstack-ai#5811/objectstack-ai#12281`, `objectstack-ai#8421/objectstack-ai#12194`), and they are rewritten too. Each
rewritten line cites one of **95 distinct commits**.

No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/`
records the decision behind any of these numbers. ADR-0126 and ADR-0131
name objectstack-ai#10243 only as the incident, ADR-0126 names objectstack-ai#11513 only for the
flow-clone half, and ADR-0112 names objectstack-ai#12281 only as another card. So
every anchor is a commit. The anchors the landed stages already gave the
same numbers are reused (24 numbers, for example `f19475c0a` for objectstack-ai#14143,
`e2798fab7` for objectstack-ai#6345 and `79c46da90` for objectstack-ai#9934), so each number carries
one anchor across the tree.

Only comments changed. Every touched file keeps its line count (508
lines out, 508 in, over 118 files), so no line citation into these files
moves. Seven of the 508 lines held no census site. Five are the other
half of a sentence that had to change:
`action-governance-scope-divergence.test.ts:6` (「the card names」 to
「that diverged」, because line 4 no longer names the card),
`action-record-load-denied.test.ts:560`,
`dispatcher-5xx-demoted-code-withhold.test.ts:45` (「that card's change」
to 「that commit's change」),
`hook-input-writeback-readonly-provenance.integration.test.ts:380`
(「that card」 to 「that commit」) and
`standalone-stack-seeder-declaration-copy.test.ts:88` (a trailing 「PR」
whose number wrapped onto line 89). Two carry only a slash-joined
number: `dispatcher-plugin.ts:688` and
`meta-compound-arity-mint-door.test.ts:4`. No code token moves (see the
guard below).

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces. No PR number stands on an added
line, and none of the 95 shas is on a removed line.

Twenty-eight dead comment sites are left on purpose:
- **20 in `domains/meta.ts`.** PR objectstack-ai#20615 (objectstack-ai#20590's) opened at
2026-09-29T08:12:40Z, after this stage's claim and first read, and edits
that file. So the file went back to its base blob (`b4ddb362cc`) in
`a5cdfd8a46`, as PR objectstack-ai#20612 did with `authoring-rules.ts`. The anchors
are verified and listed below for the follow-up.
- **8 with no deciding commit, or with a literal reader.** See "The
sites left" below.

One more file: a `patch` changeset for `@objectstack/runtime`, because
the rewritten docblocks ship (see Changeset below).

## Census: `packages/runtime`, before and after

**Instrument.** The gate's own `node scripts/check-issue-citations.mjs
--census --json`, read-only and unchanged, run with the fleet token. Its
surface is comment prose in `packages/**/src/**/*.ts` with string
literals blanked, and it defers `*.test.ts`. The count is its
`allocated-but-absent` findings under `packages/runtime/`. Both runs
enumerated the whole board (185 pages), so neither read a truncated
board.

| reading | tree | board | whole-repo `allocated-but-absent` | runtime
sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `eb4b17c346`, run 2026-09-29T07:55:51Z to 08:05:47Z |
enumerated, 185 pages, frontier objectstack-ai#20614, 18,441 numbers | 2,397 | **217**
| 216 | 29 | 59 |
| after | head `a5cdfd8a46`, run 09:08:23Z to 09:14:11Z | enumerated,
185 pages, frontier objectstack-ai#20623, 18,450 numbers | 2,027 | **23** | 23 | 4 |
12 |

The before count equals the card's 217 at `f11b5f20a2`. The 23 left are
the 20 held `domains/meta.ts` sites and 3 deliberate ones
(`api-exposure.ts:108`, `domains/mcp.ts:360`, `route-ledger.ts:300`).
The whole-repo drop is 370: this diff's 194, plus the 97 and 79 of PR
objectstack-ai#20609 and PR objectstack-ai#20612, which landed on `main` in between and came in with
the merge.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `classifyCitation` over
every `.ts` file under `packages/runtime/src` (373 files), against a
board probed by REST for every number cited there. The lit controls
objectstack-ai#16862, objectstack-ai#16847 and objectstack-ai#17698 answered 200 and the dead controls objectstack-ai#16714,
objectstack-ai#16715 and objectstack-ai#16697 answered 404 in both runs.

| reading | tree | citations | dead | src comment | test comment | src
string | test string |
|---|---|---|---|---|---|---|---|
| before, 08:17:55Z | `eb4b17c346` | 5,364 | **641** | 217 | 324 | 5 |
95 |
| after, 09:24:24Z | `a5cdfd8a46` | 4,851 | **128** | 23 | 5 | 5 | 95 |

Its src-comment column equals the census's 217 and 23, which is the
control on the second instrument. The 4,499 resolving citations, the 195
that resolve as pull requests and the 29 cross-repo ones are the same in
both readings. The drop is 513, exactly this diff's grammar-read sites.

## Per-number table

Sites and files are the dead comment sites in scope at the base, tests
included. `held` is `domains/meta.ts` (see above) and `left` is a site
with no deciding commit or with a literal reader. `strings kept` counts
string-literal sites, which are tokens and stay as they were. Every
anchor was read in its message or its diff, not only in its subject: it
is the commit that made the change the line describes, and its own
message or diff names the number it replaces.

| number | comment sites / files | rewritten | held | left | strings
kept | anchor |
|---|---|---|---|---|---|---|
| `objectstack-ai#6065` | 1/1 | 1 | 0 | 0 | 0 | `026101660` |
| `objectstack-ai#6123` | 1/1 | 1 | 0 | 0 | 0 | `59d1933f9` |
| `objectstack-ai#6206` | 5/2 | 5 | 0 | 0 | 0 | `8e13ca876` |
| `objectstack-ai#6216` | 2/1 | 2 | 0 | 0 | 1 | `f586f1a89` |
| `objectstack-ai#6220` | 1/1 | 1 | 0 | 0 | 0 | `83df2fd73` |
| `objectstack-ai#6238` | 2/2 | 2 | 0 | 0 | 2 | `c8d6f6e08` |
| `objectstack-ai#6259` | 4/2 | 3 | 0 | 1 | 1 | `6968885ef` |
| `objectstack-ai#6265` | 12/2 | 12 | 0 | 0 | 4 | `cfb549db8` |
| `objectstack-ai#6268` | 9/3 | 9 | 0 | 0 | 0 | `68f5eccb1` |
| `objectstack-ai#6287` | 1/1 | 1 | 0 | 0 | 0 | `84c86fb45` |
| `objectstack-ai#6307` | 1/1 | 1 | 0 | 0 | 0 | `293476148` |
| `objectstack-ai#6316` | 6/3 | 6 | 0 | 0 | 0 | `448ac9565` |
| `objectstack-ai#6345` | 10/3 | 10 | 0 | 0 | 0 | `e2798fab7` |
| `objectstack-ai#6361` | 4/2 | 4 | 0 | 0 | 6 | `90bbf2510` |
| `objectstack-ai#6363` | 6/2 | 6 | 0 | 0 | 2 | `17d095413` |
| `objectstack-ai#6483` | 3/2 | 3 | 0 | 0 | 0 | `ee58392e1` |
| `objectstack-ai#8722` | 1/1 | 0 | 0 | 1 | 0 | — |
| `objectstack-ai#8724` | 1/1 | 1 | 0 | 0 | 0 | `ff4ba6a06` |
| `objectstack-ai#8726` | 8/4 | 7 | 1 | 0 | 1 | `e783e163d` |
| `objectstack-ai#8796` | 13/3 | 13 | 0 | 0 | 4 | `a4331227b` |
| `objectstack-ai#8848` | 3/2 | 1 | 2 | 0 | 1 | `4fc4a3c0b` |
| `objectstack-ai#8919` | 1/1 | 0 | 1 | 0 | 0 | `b5378550e` (held file) |
| `objectstack-ai#9934` | 17/7 | 17 | 0 | 0 | 4 | `79c46da90` |
| `objectstack-ai#9967` | 1/1 | 1 | 0 | 0 | 0 | `8f266f1cd` |
| `objectstack-ai#10179` | 1/1 | 0 | 0 | 1 | 2 | — |
| `objectstack-ai#10243` | 40/13 | 40 | 0 | 0 | 9 | `266436a7f`, `02b41232d` |
| `objectstack-ai#10293` | 3/3 | 3 | 0 | 0 | 0 | `92a69d813` |
| `objectstack-ai#10338` | 1/1 | 1 | 0 | 0 | 0 | `d2619fd0c` |
| `objectstack-ai#10340` | 3/2 | 2 | 1 | 0 | 1 | `26f3588fb` |
| `objectstack-ai#10380` | 12/2 | 12 | 0 | 0 | 0 | `dd8172ee2` |
| `objectstack-ai#10485` | 3/3 | 3 | 0 | 0 | 0 | `35ad101bc` |
| `objectstack-ai#10503` | 8/2 | 3 | 5 | 0 | 1 | `67ceb9aef` |
| `objectstack-ai#10537` | 2/1 | 2 | 0 | 0 | 0 | `e634ecf6a` |
| `objectstack-ai#10554` | 1/1 | 1 | 0 | 0 | 0 | `6abc4df03` |
| `objectstack-ai#10629` | 75/23 | 75 | 0 | 0 | 0 | `13a6cb4ad` |
| `objectstack-ai#10630` | 4/1 | 4 | 0 | 0 | 0 | `dd8172ee2` |
| `objectstack-ai#10789` | 2/1 | 2 | 0 | 0 | 1 | `38bc74ed1` |
| `objectstack-ai#10886` | 1/1 | 1 | 0 | 0 | 1 | `809e61221` |
| `objectstack-ai#10888` | 3/3 | 2 | 1 | 0 | 1 | `d806081dd` |
| `objectstack-ai#10961` | 5/3 | 5 | 0 | 0 | 3 | `222d06fc1` |
| `objectstack-ai#10965` | 2/1 | 2 | 0 | 0 | 1 | `ab47f6974` |
| `objectstack-ai#10978` | 1/1 | 1 | 0 | 0 | 0 | `4c9780c7a` |
| `objectstack-ai#10983` | 3/2 | 3 | 0 | 0 | 0 | `6a4e929f5` |
| `objectstack-ai#11006` | 4/4 | 3 | 1 | 0 | 0 | `cccbe51bf` |
| `objectstack-ai#11015` | 3/1 | 3 | 0 | 0 | 0 | `82cb6e849` |
| `objectstack-ai#11166` | 8/3 | 8 | 0 | 0 | 4 | `735f5c709` |
| `objectstack-ai#11333` | 1/1 | 1 | 0 | 0 | 0 | `ea4d16420` |
| `objectstack-ai#11504` | 3/2 | 3 | 0 | 0 | 0 | `f90e82024` |
| `objectstack-ai#11513` | 2/2 | 2 | 0 | 0 | 0 | `e170b0ae5` |
| `objectstack-ai#11703` | 8/3 | 8 | 0 | 0 | 1 | `5cb62d88b` |
| `objectstack-ai#12010` | 1/1 | 1 | 0 | 0 | 0 | `77b91bdb4` |
| `objectstack-ai#12176` | 5/5 | 5 | 0 | 0 | 0 | `7986d973f` |
| `objectstack-ai#12194` | 11/4 | 8 | 3 | 0 | 0 | `311433f6b` |
| `objectstack-ai#12195` | 9/4 | 4 | 5 | 0 | 10 | `7986d973f` |
| `objectstack-ai#12281` | 20/5 | 20 | 0 | 0 | 5 | `0783d7b80` |
| `objectstack-ai#12943` | 7/3 | 7 | 0 | 0 | 0 | `090f2302e` |
| `objectstack-ai#13037` | 8/2 | 8 | 0 | 0 | 5 | `e7dfb1d69` |
| `objectstack-ai#13233` | 5/1 | 5 | 0 | 0 | 0 | `3800e4293` |
| `objectstack-ai#13241` | 5/4 | 5 | 0 | 0 | 1 | `a21d2a9cf` |
| `objectstack-ai#13273` | 11/3 | 11 | 0 | 0 | 0 | `3a86a65e7` |
| `objectstack-ai#13279` | 3/2 | 3 | 0 | 0 | 0 | `6a180e42d` |
| `objectstack-ai#13325` | 3/1 | 3 | 0 | 0 | 0 | `2e0b7b18f` |
| `objectstack-ai#13644` | 5/4 | 5 | 0 | 0 | 1 | `34ce8e7db` |
| `objectstack-ai#13657` | 13/1 | 13 | 0 | 0 | 1 | `b003cf2e8` |
| `objectstack-ai#14143` | 26/8 | 26 | 0 | 0 | 4 | `f19475c0a` |
| `objectstack-ai#14390` | 1/1 | 1 | 0 | 0 | 0 | `9d7f7259f` |
| `objectstack-ai#14398` | 3/1 | 3 | 0 | 0 | 0 | `317132495` |
| `objectstack-ai#14403` | 6/1 | 6 | 0 | 0 | 0 | `93d2d679b` |
| `objectstack-ai#14421` | 2/1 | 2 | 0 | 0 | 0 | `bd8795ea1` |
| `objectstack-ai#14422` | 4/2 | 4 | 0 | 0 | 4 | `dc7c226b9` |
| `objectstack-ai#14423` | 3/1 | 3 | 0 | 0 | 1 | `a56baa2bd` |
| `objectstack-ai#14474` | 1/1 | 1 | 0 | 0 | 0 | `df657d9df` |
| `objectstack-ai#14667` | 2/1 | 2 | 0 | 0 | 0 | `dc7c226b9` |
| `objectstack-ai#14678` | 2/1 | 2 | 0 | 0 | 2 | `73ad0bba7` |
| `objectstack-ai#14683` | 2/2 | 2 | 0 | 0 | 0 | `96326040f` |
| `objectstack-ai#14723` | 1/1 | 1 | 0 | 0 | 0 | `65846bc46` |
| `objectstack-ai#14745` | 1/1 | 0 | 0 | 1 | 0 | — |
| `objectstack-ai#14748` | 1/1 | 1 | 0 | 0 | 1 | `92b5d7f00` |
| `objectstack-ai#14758` | 15/5 | 15 | 0 | 0 | 1 | `84199cb87` |
| `objectstack-ai#14760` | 6/2 | 6 | 0 | 0 | 2 | `ee32e1cb8` |
| `objectstack-ai#14864` | 3/3 | 3 | 0 | 0 | 3 | `066dd3bd0` |
| `objectstack-ai#14878` | 2/1 | 2 | 0 | 0 | 1 | `29db3cd2a` |
| `objectstack-ai#14908` | 3/2 | 3 | 0 | 0 | 0 | `d5cbb44f3` |
| `objectstack-ai#14921` | 2/1 | 2 | 0 | 0 | 0 | `c1d274de7` |
| `objectstack-ai#15063` | 2/1 | 2 | 0 | 0 | 0 | `ad35745e8` |
| `objectstack-ai#15068` | 2/2 | 2 | 0 | 0 | 4 | `8744de9e9` |
| `objectstack-ai#15071` | 5/2 | 5 | 0 | 0 | 0 | `cf6e0a193` |
| `objectstack-ai#16610` | 3/1 | 3 | 0 | 0 | 0 | `316a20fc5` |
| `objectstack-ai#16649` | 4/1 | 4 | 0 | 0 | 0 | `44c917a47`, `613bfbd3d` |
| `objectstack-ai#16755` | 1/1 | 1 | 0 | 0 | 0 | `44c849c7d` |
| `objectstack-ai#16758` | 1/1 | 1 | 0 | 0 | 0 | `6e9bee640` |
| `objectstack-ai#16783` | 1/1 | 1 | 0 | 0 | 0 | `854639b31` |
| `objectstack-ai#16919` | 1/1 | 1 | 0 | 0 | 0 | `2cd4c548e` |
| `objectstack-ai#17038` | 1/1 | 0 | 0 | 1 | 0 | — |
| `objectstack-ai#17039` | 1/1 | 1 | 0 | 0 | 0 | `edf59e359` |
| `objectstack-ai#17041` | 2/2 | 0 | 0 | 2 | 0 | — |
| `objectstack-ai#17114` | 2/2 | 2 | 0 | 0 | 2 | `4af758d47` |
| `objectstack-ai#17147` | 1/1 | 1 | 0 | 0 | 0 | `aaacf1d5c` |
| `objectstack-ai#17148` | 1/1 | 0 | 0 | 1 | 0 | — |
| `objectstack-ai#17195` | 1/1 | 1 | 0 | 0 | 0 | `d2c1d1980` |
| `objectstack-ai#17219` | 1/1 | 1 | 0 | 0 | 0 | `706ad0fcc` |
| `objectstack-ai#19364` | 2/2 | 2 | 0 | 0 | 0 | `ada701220` |
| `objectstack-ai#19394` | 5/2 | 5 | 0 | 0 | 0 | `0862063ba` |

Every cited sha matches exactly one object (`git rev-parse
--disambiguate`, count 1 for each of the 95), is a commit, has one
parent, and is an ancestor of the base (`merge-base --is-ancestor`, exit
0 for all 95). The checkout is not shallow (`--is-shallow-repository`
false), and the control leg `13a6cb4ad` exits 0 too.

**Numbers with more than one anchor, by site:**
- `objectstack-ai#10243` (40 sites): `266436a7f` for the 26 sites that describe the
2026-08-23 ruling it implements (the enablement door joins the
`manage_metadata` write set, with the `trigger` exclusion), and
`02b41232d` for the 14 that name the leak itself (「the leak commit
02b4123 measured」). That commit recorded the measurement over HTTP and
says it is part of that card.
- `objectstack-ai#16649` (4 sites): `613bfbd3d` for the first half (the fourteen
remaining `boot-refusal` rows registered) and `44c917a47` for the second
(the face refusal widened to every published package, and `boot-refusal`
retired).
- `objectstack-ai#12176`, `objectstack-ai#12194`, `objectstack-ai#12195`: the stages of one ruled retirement.
`311433f6b` is stage 1 (the item-name grammar refused at the publish
door) and `7986d973f` is stage 3 (the compound arities un-mounted).
These are the anchors the spec stages gave.

**Wordings to check, each true of its commit:**
- `objectstack-ai#10293` (3 sites) cited the p1 flake whose signature had the
expected-noise lines lifted into it. They now read 「(a vitest teardown
race, fixed by commit 92a69d8)」. `92a69d813` names that number in its
subject and fixed the flake by disarming vitest's console-forwarding
teardown race, which is why the noise pointed the dispatch at the wrong
mechanism.
- `objectstack-ai#16755` and `objectstack-ai#16783` each cited an open PR that held a file at the
time. They now read 「the change that landed as commit 44c849c held
that file」 and 「then held by the change that landed as commit
854639b」. Each commit's diff edits the named file
(`domains/automation.ts`, `seed-loader.test.ts`).
- Quoted rulings keep their words.
`dispatcher-plugin.declared-5xx-prose-withhold.test.ts:13` and
`dispatcher-plugin.declared-user-message.test.ts:35` quote the
2026-08-27 ruling, and
`dispatcher-5xx-demoted-code-withhold.test.ts:281` quotes an older note.
There the commit stands in an editorial bracket (`[commit 79c46da]`,
`[commit 0783d7b]`) in place of the number.
- `objectstack-ai#9934`'s 「second constraint」 and 「third constraint」 now read 「the
ruling's second constraint, commit 79c46da」. That commit's own diff
calls status-agnosticism 「the ruling's second constraint」.
- `domains/packages.ts:841` read 「declares, since objectstack-ai#19364:」 above the
`enabled` line, but that line predates `ada701220` (objectstack-ai#19364's commit). It
now reads 「declares — a key commit ada7012 kept rather than retired:」.
- `route-ledger.ts:288`: 「objectstack-ai#16758 filed the second kind」 now reads
「Commit 6e9bee6 gated the second kind」, because that commit added the
row census after the index-slice incident the sentence goes on to
describe.
- `flow-clone.ts:7` and `domains/automation.ts:2403` cite `e170b0ae5`
for objectstack-ai#11513: the commit that landed 「lock package-declared permission
sets at the save door; clone to customize」, whose changeset names the
number.

## The sites left

**No deciding commit, or a literal reader (8 sites):**
- `api-exposure.ts:108` (objectstack-ai#6259): `api-exposure.test.ts:152` splits this
`@param` block on the literal `'objectstack-ai#6259'`, so rewriting the comment would
change what the test measures. Its deciding commit is `6968885ef`, which
the three test-comment sites of the same number now cite.
- `domains/mcp.ts:360` (objectstack-ai#8722): a wider contract change 「archived
unscheduled」. It never landed, so no commit decided it.
- `domains/meta-state-plural-tolerance.test.ts:130` (objectstack-ai#10179): an untaken
option on a tracking card. The only commit naming the card, `53a48c93f`,
recorded the opposite state.
- `package-door-namespace-conflict-code.test.ts:30` (objectstack-ai#14745): a residue
item on a review card. The only commit carrying the token is the one
that added this file.
- `route-ledger.conformance.test.ts:33` (objectstack-ai#17038): an ablation measured
on a PR whose squash commit, `6a7910abb`, neither records nor performs
it.
- `route-ledger.conformance.test.ts:38` and `route-ledger.ts:300`
(objectstack-ai#17041): a maintainer decision the lines call open.
- `security/artifact-granted-permissions.test.ts:291` (objectstack-ai#17148): a
question the line itself says is unsettled.

**Held with `domains/meta.ts` (20 sites), anchors verified for the
follow-up:** `objectstack-ai#8726` `:116` to `e783e163d`; `objectstack-ai#8848` `:200`, `:1398` to
`4fc4a3c0b`; `objectstack-ai#8919` `:1247` to `b5378550e`; `objectstack-ai#10340` `:1309` to
`26f3588fb`; `objectstack-ai#10503` `:14`, `:1143`, `:1159`, `:1250`, `:1308` to
`67ceb9aef`; `objectstack-ai#10888` `:1337` to `d806081dd`; `objectstack-ai#11006` `:103` to
`cccbe51bf`; `objectstack-ai#12194` `:831`, `:1050`, `:1173` to `311433f6b`; `objectstack-ai#12195`
`:819`, `:827`, `:1046`, `:1167`, `:1960` to `7986d973f`. PR objectstack-ai#20615's
one hunk there is at `:1874`, disjoint from these lines, but the rule is
file-level.

**String sites kept as tokens (100).** 95 are test titles and test-code
strings in 43 files. Five are non-test strings: the `route-ledger.ts`
`note` fields at `:435`, `:441` and `:505`, a string at
`dispatcher-error-vocabulary.ts:349`, and the enablement door's refusal
text at `domains/activation-gate.ts:279`, which ends 「(objectstack-ai#10243).」 (see
Acceptance notes).

## Mechanical guard: no code token moves

The check compares the TypeScript parser's leaf tokens (TypeScript
6.0.3, JSDoc nodes excluded, so template literals are read in context)
of each touched file at base `eb4b17c346` against the working tree at
`a5cdfd8a46`, over all 118 touched `.ts` files. Controls mutate the head
text in memory only, so nothing on disk moved for them.

- Real run: 301,081 base tokens, **0 files with a token change** (exit
0).
- Comment-insertion control (`domains/activation-gate.ts`): 0 files
changed (exit 0).
- Code-insertion positive control (a declaration in the same file):
DIFFER at token 34 (exit 1).
- String positive control (`(objectstack-ai#10243)` to `(objectstack-ai#10244)` inside the kept
refusal string): DIFFER at token 339 (exit 1).

Line balance: every touched file is +N/−N (508/508), and every line
count is equal at base and head. A raw scan of the 119 changed files for
control bytes finds none.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/runtime` is included, in PR objectstack-ai#20609's form and level. It
says only that the provenance comments were re-anchored.

Measured on the built package: `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After `pnpm --filter @objectstack/runtime build`, the
rewritten docblocks reach `dist`: for example `e2798fab7` appears 3
times and `68f5eccb1` 6 times in `dist/index.d.ts`, and `f19475c0a` 4
times in `dist/index.js`. The positive control, the unchanged sentence
「drags `@libsql/client` (native bindings included)」 of the same
`turso-driver-factory.ts` docblock, is in `dist/index.d.ts`, and a
negative control phrase appears nowhere. The only dead number left in
`dist` is the kept refusal string's `objectstack-ai#10243`.

## Gates (head `a5cdfd8a46`)

This host has no `flock`, so `os-verify-lock.sh` ran in its declared
unlocked mode. Its disclosure, verbatim, from each locked run at this
head:

```text
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 5s · declare it in the PR body · pnpm --filter @objectstack/runtime build
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 99s (1m39s) · declare it in the PR body · pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 6s · declare it in the PR body · pnpm --filter @objectstack/runtime exec vitest run --project repo --maxWorkers=2
os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 8s · declare it in the PR body · pnpm --filter @objectstack/runtime typecheck
```

The dependency closure and the whole workspace were built first, at the
merge head `ca6d13d6ab`, the same way: `turbo run build
--filter='@objectstack/runtime...'` (30 tasks, exit 0) and `turbo run
build --filter='./packages/*' --filter='./packages/*/*'` (71 tasks, exit
0). `a5cdfd8a46` differs from that head only in `domains/meta.ts`, which
went back to base bytes, and `@objectstack/runtime` was rebuilt at
`a5cdfd8a46`.

- **Tests:** `vitest run --project local`: 288 files, 4,190 tests
passed, 1 skipped. `--project repo` (which holds the touched
`action-owner-key-single-source.test.ts`): 3 files, 727 tests passed.
Together they cover every touched test file.
- **Typecheck:** `pnpm --filter @objectstack/runtime typecheck` exits 0.
`tsc --listFiles` counts 82 `src` files (no tests) under `tsconfig.json`
and all 291 test files under `tsconfig.test.json`, which
`check:test-typecheck` judges: 27 files, 190 errors, 68 pinned
signatures held.
- **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`)
exits 0 at `a5cdfd8a46` (2026-09-29T09:23:06Z to 09:23:36Z). A narrowed
run over the 118 touched `.ts` files through eslint's API agrees: 118
linted, 0 ignored, 0 errors, 0 warnings.
- **Citation judging:** `node scripts/check-issue-citations.mjs --base
origin/main` exits 0. The diff-scoped run judged 23 citations across 28
files, and all 23 resolve. These are the live numbers that stay on
rewritten lines. It defers `*.test.ts`, so the added-minus-removed count
over the whole diff covers the rest: 0 numbers added.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `a5cdfd8a46` derived 67
families, the same set as at the merge head. All 67 exit 0. `--ran`
reads 「67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN」.
- At the merge head, `check:dual-build-cjs-loads` and
`check:type-check-debt` first exited 3 (PREREQUISITE NOT MET) on a
partly built workspace. After the whole-workspace build both exited 0,
and both exit 0 at the final head.
- Among them: `check:doc-authoring` (the sibling prose-id baseline
holds, 810 pinned sites, no growth), `check:nul-bytes` (9,250 files, no
raw control bytes), `check:route-ledger-census`,
`check:dispatcher-error-vocabulary` and `check:issue-citations`
(self-test, 114 cases in 8 batteries).
- **Artifact rosters:** 38 of the 41 non-self-test roster rows exit 0 at
the merge head. The other three, `check-closing-target-claim`,
`check-partof-closing-keyword` and `check-single-claim-paths`, answer
「NOT WIRED」 (exit 2) without a pull request's context, and are run
against this PR and reported on the card.

## Hypotheses (measured first)

- **H0 holds.** The filtered census answers 217 dead sites at
`eb4b17c346` (29 files, 59 numbers), equal to the card's count at
`f11b5f20a2`: no drift.
- **H1 holds, with the listed exceptions.** After the rewrite the
filtered census answers 23: the 20 sites held with `domains/meta.ts` for
an open PR, and 3 deliberate ones (a literal reader, a card never
landed, an open decision). The supplementary reading adds 5 test-comment
sites of the same two kinds.
- **H2 holds, by the token guard.** A comment-stripped comparison of
every touched file (the parser's leaf tokens, JSDoc excluded) is empty,
and its controls fire. The emitted `dist` is not byte-identical, because
the docblocks ship, which is why the changeset is `patch`.

## Acceptance notes

- **The held file.** The claim's read (07:51Z) and this stage's first
read of the open PRs' file lists (08:06:32Z, 8 open PRs) found none
touching `packages/runtime/src`. PR objectstack-ai#20615 opened at 08:12:40Z and edits
`domains/meta.ts`. The re-read at 09:07:08Z (7 open PRs) found it, and
it is the only open PR touching the package. The file went back to its
base blob in `a5cdfd8a46`, and `git hash-object` equals `b4ddb362cc`,
the blob at the base and at `origin/main`. The 20 anchors above are
ready for the follow-up once that PR lands.
- **Form D, not touched here.** `domains/activation-gate.ts:279` is part
of the enablement door's refusal message and ends 「(objectstack-ai#10243).」. An author
sees it, so it is ruling D's (no number, the lesson in words), a string
change outside this comment-only scope. It needs a form-D carrier. The
other four non-test string sites are ledger `note` data and a gate's own
string.
- **The grammar does not read a slash-joined number.** `CITATION_RE`
refuses a `#` preceded by `/`, so the second number of `#A/#B` is never
judged. In `packages/runtime/src`, 3 such dead numbers exist (`objectstack-ai#10630`,
`objectstack-ai#12281`, `objectstack-ai#12194`), and all 3 are rewritten here. The other 36 distinct
slash-joined numbers there were probed by REST and answer 200. One more
dead one, `objectstack-ai#17219`, stands slash-joined inside a test title, a string,
and is kept. This is the same shape as PR objectstack-ai#20612's slash-joined
`objectstack-ai#5775/objectstack-ai#6629`. It is noted, not filed.
- **Outside the scope and the census surface.**
`packages/runtime/vitest.config.ts:54` cites `objectstack-ai#17853`, which answers
404. The file is outside `src/**`, so it is left for whoever owns the
package's config. The other numbers there, and those in `tsup.config.ts`
and `README.md`, answer 200.
- **Base.** The branch merged `origin/main` once (`ca6d13d6ab`, merging
`c1d8051e0a`) before the `--base origin/main` run, as the dispatch
orders. That merge brought PR objectstack-ai#20609's and PR objectstack-ai#20612's landed stages and
touched none of this diff's files. `origin/main` has since moved to
`ed6f7348f9`, one commit that touches only `packages/cli`, so there was
no second merge.
- **Anchors shared with the landed stages.** 24 numbers keep the anchor
the spec, lint or service-messaging stages already gave them, for
example `f19475c0a` (objectstack-ai#14143), `b003cf2e8` (objectstack-ai#13657), `311433f6b`
(objectstack-ai#12194), `8e13ca876` (objectstack-ai#6206) and `17d095413` (objectstack-ai#6363).

## Deviations

- Three changed lines hold only a slash-joined dead number, beyond the
census's sites (see Acceptance notes). Five more are the other half of a
rewritten sentence (listed under What changed).
- Commit trailers are AGENTS.md's model-free pair (`Claude-Session` plus
`Co-authored-by: Claude`), and the pre-push trailer check passed on
every push. The merge commit carries git's default message.

---
_Generated by [Claude
Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_

---------

Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…mits that decided them (objectstack-ai#20626)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the second stage of the `domain:services` lane of the
dead-citation sweep. It covers `packages/plugins/plugin-sharing/src/**`
and nothing else. By census, it is the largest package in the lane that
no open PR or in-flight claim holds (the claim, `5886159115`, gives the
order). Later stages cover the other packages, so this PR says `Part of`
and the card stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), by stage 1's method (PR objectstack-ai#20609, landed as
`422db788a`). That is **87 sites on 86 lines in 23 files, covering 13
numbers**: the 60 census sites outside the generated headers, and 27
sites in test comments, which the census defers. Each rewritten line now
cites the commit in `origin/main` history that decided what the line
describes, and it says in its own words what that commit decided.

No ADR or ruling-record file records the decision behind any of the 13
numbers (ADR-0131 names objectstack-ai#14484 only as evidence, not as the record of
its ruling), so every anchor is a commit: **13 distinct shas**. No
number was dropped.

Only comments changed. Every touched source file keeps its line count
(87 lines out, 87 in, over 23 files), so no line citation into these
files moves. One of those 87 lines held no dead citation:
`backfill-sys-record-share-organizations.ts:14`, where 「the cliff the
card names」 lost its referent once line 10 named a commit instead of a
card. It now reads 「the cliff that commit pins」, and `3f64fe6c6`'s
backfill test is the one titled 「the cliff」. No code token moves (see
the guard below).

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces. Over the whole diff, added minus
removed is 0 or negative for every number, and no number is new to the
diff. No PR number stands on an added line. The one PR spelling in scope
(`PR objectstack-ai#5973`, dead) became its squash commit.

Nineteen dead sites are left on purpose: 1 string literal, 14 test
titles, 1 verbatim ruling quotation and 3 generated file headers (see
the list below).

One more file: a `patch` changeset for `@objectstack/plugin-sharing`,
because the rewritten docblocks ship (see Changeset below).

## Census: `plugin-sharing`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count below is its `allocated-but-absent` findings under
`packages/plugins/plugin-sharing/`. Each run counts as a reading only
because its board frontier equals the newest issue number, read by a
separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
plugin-sharing sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `422db788a`, run 2026-09-29T08:04:49Z to 08:08:19Z |
enumerated, 185 pages, frontier objectstack-ai#20614 (newest objectstack-ai#20614), 18,441 numbers |
2,300 | **63** | 62 | 14 | 13 |
| after | head `a6d231713`, run 08:27:44Z to 08:31:07Z | enumerated, 185
pages, frontier objectstack-ai#20616 (newest objectstack-ai#20616), 18,443 numbers | 2,240 | **3** |
3 | 3 | 1 |

The before count matches the 63 that census `5884031174` read at
`f11b5f20`. The whole-repo drop is 60, exactly this diff's census sites.
The `resolves` tally is 32,803 in both runs, and
`resolves-as-pull-request` (1,891) and `cross-repo-unjudged` (983) did
not move either. The 3 left are the generated headers below. No run was
truncated or discarded: all three enumerations in this stage (two census
runs and the supplementary board below) read 185 pages at the newest
frontier.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes both. So a second
reading runs the gate's own exported `extractCitations` (whole-file and
comment-prose projections) and `classifyCitation` over every `.ts` file
under `plugin-sharing/src` (74 files). It uses one board, enumerated by
the gate's own `enumerateBoard` at 08:12:18Z (185 pages, frontier
objectstack-ai#20614, equal to the newest).

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `422db788a` | 1,187 | **106** | 63 | 28 | 1 | 14 |
| after, `a6d231713` | 1,100 | **19** | 3 | 1 | 1 | 14 |

Its src-comment column equals the census's 63, which is the control on
the second instrument. The 989 resolving, 87 pull-request and 5
cross-repo citations are the same in both readings.

## Per-number table

Sites and files count all dead sites in scope at the base (comments and
strings, tests included). `rewritten / left` counts the sites rewritten
and the sites left. Each anchor was read in its message and diff, not
only its subject. It is the commit that decided what the line describes:
its own message or diff names the number it replaces, or, for a
squash-merged PR, it is the merge of that PR.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#5973` | 4/2 | 3/1 | `abeb3751f`:
`HierarchyScopeContext.organizationId` is the tenancy authority, and it
is required. `objectstack-ai#5973` was the PR itself; this is its squash commit |
| `objectstack-ai#6206` | 12/7 | 11/1 | `8e13ca876`: the share-link routes hand
enforcement the whole authz envelope, per maintainer ruling A of
2026-08-07. It is the plugin-sharing half; the spec stages anchor the
contract half at `d7e0b4212`. Three sites name the ruling in words, 「the
full-envelope ruling」, beside `aa4b90d9a`, which applied it |
| `objectstack-ai#6523` | 3/3 | 3/0 | `aa4b90d9a`: 36 contract signatures converge on
the full `ExecutionContext`. The same anchor the spec stages gave this
number |
| `objectstack-ai#8710` | 10/3 | 9/1 | `04d03c3a0`: a deactivated `sys_position`
confers no sharing-rule shares. Its message quotes the 2026-08-15
ruling: access-conferring paths filter, addressing paths do not |
| `objectstack-ai#8792` | 2/1 | 2/0 | `83c661d97`: the bulk-write merge's missing
provenance mark is recorded as ruled (2026-08-15), not oversight |
| `objectstack-ai#8836` | 1/1 | 1/0 | `1850ebbb0`: it pins 「no filter object that can
be vouched 'author' may outlive the request that vouched it」, the
invariant the line names. The same anchor the spec stages gave this
number |
| `objectstack-ai#11671` | 5/5 | 2/3 | `09b4f4e4e`: `os i18n extract --source-hashes`
writes the provenance companion (maintainer ruling objectstack-ai#12069 Option A,
which stays cited). The same anchor stage 1 gave it |
| `objectstack-ai#11674` | 4/2 | 4/0 | `1cba33f16`: the seed loader warns at load time
when a required column is deferred, and the ordering constraint is
written at the four pointer-pair sites, these two among them |
| `objectstack-ai#12493` | 2/2 | 2/0 | `aa5994e17`: the Operation Message Catalog
gains `record_write_denied` ahead of its emitters. The same anchor the
spec stages gave this number |
| `objectstack-ai#13279` | 3/2 | 3/0 | `6a180e42d`: a permission-store read that
throws raises `AuthzStoreUnavailableError` (503), and each transport
re-raises it rather than laundering it into a 401 |
| `objectstack-ai#13398` | 1/1 | 1/0 | `953a81f4a`: the class ruling on published
logger sinks, applied at this site. `error` is reachable only because
the sink already declares it; growing `error?` onto a published sink is
forbidden. No record of the ruling exists in the repo, and this commit,
which wrote this heading, is its earliest application in history |
| `objectstack-ai#13608` | 23/3 | 21/2 | `fc9ba76a5`: `publicSharing.eligibility` is
held at redemption, not only at mint. The same anchor the spec stages
gave this number |
| `objectstack-ai#14484` | 36/8 | 25/11 | `3f64fe6c6`: `organization_id` is stamped on
every `sys_record_share` write, the stranded rows are backfilled, and
the object is admitted to the tenancy ledger (the 2026-09-02 ruling,
decision batch objectstack-ai#11 item 3) |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each), and every one is an ancestor of the
base (`merge-base --is-ancestor`, exit 0 for all 13). The history is
complete (`--is-shallow-repository` false, 15,073 commits). A
line-origin pickaxe (`git log -S` on each dead line's exact text) found
each line entering either in its anchor commit or in a later commit that
cites that commit's decision. For example, `65759baca` is the consumer
half that cites `aa5994e17`'s key, and `b70a55d62` cites `3f64fe6c6`'s
ledger admission.

Wordings to check, each true of its commit:
- `backfill-sys-record-share-organizations.ts:5`: 「rows that
`SharingService.grant`, before commit 3f64fe6, stranded」. `3f64fe6c6`
is the writer fix, and this module is its backfill.
- `backfill-sys-record-share-organizations.ts:36` and `:124`: 「the
2026-09-02 ruling commit 3f64fe6 applies (decision batch objectstack-ai#11 item 3,
…)」. The verbatim maintainer quotation on line 37 is untouched.
- `share-link-service.ts:841`: 「(published-sink level ruling, commit
953a81f)」. The heading's body already states the ruling (option C
allowed, option B forbidden).
- `exec-context-annotation.pin.ts:7-8`, `sharing-rule-service.ts:12-13`
and `sharing-service.ts:20`: 「since commit aa4b90d (the full-envelope
ruling: no per-site subset contracts)」. `aa4b90d9a`'s message: 「Apply
the … ruling default (converge on the full envelope, keep no per-site
subset contracts)」.
- `share-link-routes.ts:81`: 「[commit 8e13ca8, full-envelope ruling]」,
so that 「the whole point of the ruling」 five lines down still has a
referent.

## The 19 sites left

- **Non-test string (1 site).** `sharing-service.ts:1674` sits inside
the operator-facing `warn` text for a hierarchy scope that was not
widened (「… resolveOwnerIds, objectstack-ai#5973); …」). It is a runtime string, so it
is form D, not form C, and the shrink-only `doc-authoring-prose-id`
baseline already holds it (`sharing-service.ts` → `objectstack-ai#5973: 1`). Left and
listed, as stage 1 left its refusal strings.
- **Test titles (14 sites).** `describe` titles in
`backfill-sys-record-share-organizations.test.ts:185`, `:274`, `:324`,
`:367`, `record-share-organization-stamp.test.ts:194`, `:239`, `:278`,
`:315`, `:353`, `:436`, `sharing-service.test.ts:1798` (the `objectstack-ai#14484`
titles), `share-link-eligibility.test.ts:607` (`objectstack-ai#13608`),
`share-link-enforcement-context.test.ts:226` (`objectstack-ai#6206`) and
`sharing-rule.test.ts:1898` (`objectstack-ai#8710`). Tokens, left as they were.
- **A verbatim ruling quotation (1 site).**
`share-link-service.test.ts:478` is point 2 of the maintainer's
2026-09-01 ruling, quoted verbatim and untranslated. It carries 「沿
objectstack-ai#13608 先例」. AGENTS.md keeps a quoted Chinese ruling in its original
words, and rewriting the quote would rewrite the ruling. Left.
- **Generated headers (3 sites).** Line 8 of the `es-ES`, `ja-JP` and
`zh-CN` `.source-hashes.generated.ts` files carries 「(objectstack-ai#11671, maintainer
ruling objectstack-ai#12069 Option A, extending objectstack-ai#8765 Option B)」. `os i18n extract`
writes that line from `packages/cli/src/utils/i18n-extract.ts`, so the
fix belongs at the producer, the carrier stage 1 named. The hand-written
`translations/index.ts:26` is rewritten here, with the same wording
stage 1 used.

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes, with comments as
trivia and JSDoc nodes excluded, base `422db788a` against head. Template
literals are therefore read in context. It ran over all 23 touched `.ts`
files.

- Real run: 96,665 base tokens, **0 files with a token change** (exit
0).
- Comment-insertion control in `share-link-service.ts`: 0 files changed,
as expected (exit 0). The first attempt was a no-op: its replacement
still contained the anchor, so `scripts/ablation-replace.mjs` refused it
before the guard ran. It was redone with an anchor the replacement does
not contain.
- Positive control, a code token changed in `share-link-service.ts`
(`Boolean(eligibility),` to `Boolean(eligibility) && true,`): DIFFER
(exit 1).
- Positive control, one digit changed inside the kept
`sharing-service.ts:1674` warn string: DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs`. Each restore
was proven byte-identical to the HEAD blob (`ba7fba2e8199`,
`2833b9a1616d`), with `git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/plugin-sharing` is included. It says only that the
provenance comments were re-anchored.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build, the rewritten comments reach both
halves of `dist`: `3f64fe6c6` appears 6 times in `dist/index.d.ts` and 8
in `dist/index.js`, `fc9ba76a5` 3 and 3, `04d03c3a0` 2 and 2,
`8e13ca876` twice in `index.d.ts`, and `1cba33f16` 4 times in
`index.js`. esbuild keeps only some comments, so the positive controls
are unchanged lines beside rewritten ones that shipped.
`share-link-service.ts:891` is found once in each half, and
`sharing-service.ts:1269` once in `index.js`. A never-written negative
phrase appears nowhere. The only dead number left in `dist` is the kept
`objectstack-ai#5973` warn string.

## Gates (head `a6d231713`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
(self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0:
the diff-scoped run judged 9 citations across 11 files, and all 9
resolve.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0, with the
sibling-package prose ids at their baseline and no growth.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `a6d231713` (re-derived after a
fresh `git fetch` at 09:58Z: the same 65, and none of the 8 new `main`
commits touch anything it derives from) derived 65 commands. They
include all 50 derived at dispatch, plus 15 more. All 65 exit 0. `--ran`
reports 65 run, 0 NOT MEASURED, 0 unrun, and exits 0.
- Three gates first exited 3 (PREREQUISITE NOT MET) because the
workspace was only partly built: `check:dual-build-cjs-loads`,
`check:i18n` and `check:type-check-debt`. A full `turbo run build` of
`./packages/*` and `./packages/*/*` then ran under the shared verify
lock (71 tasks, exit 0), and all three exited 0 on their rerun.
`check:dts-closure`, `check:sourcemap-no-sources-content` and
`check:lean-entry-closure` were rerun too, over 71, 68 and 15 built
packages, and exited 0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/plugin-sharing test`: 37 files and 913
tests pass. That is every test file in the package, the 12 touched ones
included.
- `pnpm --filter @objectstack/plugin-sharing typecheck` exits 0. Its
main `tsc` program reads the 37 non-test files, and its
`check:test-typecheck` program (`tsconfig.test.json`) reads all 74 files
under `src/`, the 37 test files included (`--listFiles`).
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 23 touched `.ts` files gives 23 files, 0 errors and 0
warnings. All 23 are in eslint's own population (`isPathIgnored` is
false for each). `eslint.config.mjs` never enables type-aware linting
(no `parserOptions.project`, as its own line 328 states), so a comment
edit here cannot move the verdict on any untouched file. The repo-wide
`pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 24 changed files for control bytes finds none.

## Acceptance notes

- **The census instrument did not truncate in this stage.** Three
enumerations read 185 pages each at the newest frontier. The truncation
stage 1 saw (1 run in 5) is carried on objectstack-ai#20556, and this stage changes no
instrument.
- **What stays for later stages.**
- The 3 generated `objectstack-ai#11671` headers, whose producer is
`packages/cli/src/utils/i18n-extract.ts`.
- The `objectstack-ai#5973` warn string (form D, held by the `doc-authoring-prose-id`
baseline), the 14 test titles and the verbatim ruling quotation.
- **Anchors the next stages can reuse.** The same numbers stand
elsewhere in `packages/**/src`: `objectstack-ai#6206` at 53 sites and `objectstack-ai#11674` at 46
(the ordering-constraint note has two sibling copies outside this
package, in `sys-approval-request.object.ts` and
`sys-audit-log.object.ts`). `8e13ca876` / `d7e0b4212` / `aa4b90d9a` and
`1cba33f16` are the anchors used here.
- **Base.** The branch is 8 commits behind `origin/main` (`1322cc72c`,
read at 09:58Z). None of them touches `plugin-sharing` or this
changeset, and none re-anchors any of these 13 numbers, so there was no
merge.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/m tests tooling

Projects

None yet

2 participants