Skip to content

feat(spec): a semantic migration names the D2 conversions whose applied edits it judges (SemanticMigration.conversionIds) - #20716

Merged
os-justin merged 3 commits into
mainfrom
claude/issue-20697-semantic-conversion-ids
Sep 29, 2026
Merged

os-justin merged 3 commits into
mainfrom
claude/issue-20697-semantic-conversion-ids

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Closes #20697

SemanticMigration gains an optional conversionIds, naming the D2 conversions whose applied edits the entry judges. A test refuses an id that no registered conversion at or below the entry's step replays. One link is added after reading both sides: flow-decision-edge-branching-first-match → flow-decision-mode-inclusive-explicit.

Clause-②: yes (widening)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1

…ed edits it judges

SemanticMigration gains an optional conversionIds list, the same ids
MigrationApplication.conversionId carries, so a printer of a chain
result can show an entry beside the applied edits it judges.
flow-decision-edge-branching-first-match links
flow-decision-mode-inclusive-explicit, and migrations.test.ts refuses
a link that no step at or below the entry's own replays.

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

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 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 — 137 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 679f95ec5c7c4b0797d10afb60efa8220d92a98a → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 679f95ec5c7c4b0797d10afb60efa8220d92a98a

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

…mantic-conversion-ids

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 32e8411a6ba96f7681120e7c5973724cdcdf8221
Local-runs: none

Inputs read: card #20697 (body and all 4 comments: triage 5897646840, claim 5897876723, os-dev-report 5899359294, ruling 1 5899392601); PR #20716 (body, 5-file list, net diff main...head, 97 insertions, 0 deletions); the 46 check-runs on the head; parent #20620's body and its triage direction 2 (5888087153) to test sentences. Read-only git against the fetched refs; nothing built, run or re-run.

① Derived judgments

Accept-set and public-surface changes the diff implies — each named.

  1. SemanticMigration (exported: src/migrations/index.ts:19 via src/index.ts:227) gains conversionIds?: readonly string[]. The set of valid entry literals widens: an entry may now carry the list; every existing entry stays valid. Right — triage's option A, verbatim.
  2. MigrationTodo extends SemanticMigration, so the chain result's todos[] and each hops[].todos[] may carry the key. chain.ts:102 builds a todo by spreading the entry, so the copy needs no chain change. Right.
  3. os migrate meta --json passes result.todos and h.todos through untouched (packages/cli/src/commands/migrate/meta.ts:600 and :606; emitJson is JSON.stringify, packages/cli/src/utils/format.ts:113). The machine-readable output gains one optional key on one todo. Additive; no CLI code moves, and no pin holds a todo's key set (migrate-meta.e2e.test.ts:204-208 asserts presence of fields; the Object.keys pin at :557 is the top-level payload). Right, and no CLI changeset is owed for a key that rides through the spec's exported type.
  4. The human printer (meta.ts:379-383) prints surface, replacement, reason, acceptanceCriteria and does not print the link. Right — the printer's pairing is [finding][devx] os migrate meta --from 17 buries a project's real findings under 240 generic protocol-18 notices, and marks default-flip conversions as "Applied" when the right action is usually no edit #20620's, and this PR claims nothing about it.
  5. The --stored path is untouched: StoredMigrationTodo is an explicit-field projection of a different todo (metadata-protocol/src/protocol.ts:17234-17241). Right.
  6. Generated artifacts: registry.ts gains the 3 lines the generator concatenates from the entry file (comment carried, 6-space indent, inside step 18's generated region at :11453-11455) — a regeneration, not a hand edit. api-surface/root.json:146 and export-origins/root.json:145 record name and kind only. spec-changes.ts:256-263 and build-upgrade-guide.ts:105-113 project named members only, so spec-changes.json and the upgrade guide do not move. Right that nothing else was regenerated.
  7. Test-time narrowing (not a runtime accept-set): every conversionIds id must be registered AND replayed by a step at or below the entry's major; plus an anti-vacuity floor of at least one link repo-wide. Right — the card's option A sentence, adopted by triage and confirmed by ruling 1; a registered-but-unreplayed id (10 exist: majors 11, 13, 14, 15 in CONVERSIONS_BY_MAJOR, with steps only for 17 and 18) can never meet a MigrationApplication. The floor is a small ratchet (the field can never be dropped from every entry without touching the test); disclosed by the dev, and fitting for a field whose reason to exist is the one link.
  8. One link, on 18.flow-decision-edge-branching-first-match.ts. Tested against both sides: the entry's acceptanceCriteria opens with "Review every flow-decision-mode-inclusive-explicit line the chain replay lists" and gives the three per-edit verdicts; the conversion's docblock (packages/spec/src/conversions/registry.ts, above :12258) calls it "the paired D3 entry"; the conversion is toMajor: 18, in step 18's derived conversionIds. Right, and not prose-derived.
  9. No new check:* gate; no other entry touched; the file surface matches the claim's fence exactly. Right.
  10. The member name conversionIds is reused from MigrationStep with a different meaning (judged vs graduated). As triaged ("the same id name the printer joins on"); the TSDoc's explicit cross-reference to MigrationStep.conversionIds carries the distinction. Right.

Every author-shown or AI-facing sentence, tested against the tree.

  • TSDoc, types.ts:53-72:
    • "Each id names a conversion that this entry's step or an earlier one replays, and migrations.test.ts refuses one that does not" — true on main when this PR lands (the test is in this diff).
    • "an id here is the conversionId of every MigrationApplication that conversion produces" — true: chain.ts:85-94 pushes the step's id string as conversionId.
    • "the chain copies this field onto the entry's MigrationTodo like every other field" — true, chain.ts:102.
    • "a printer of a chain result can show the entry beside each applied edit it judges" — true as a capability ("can"), now; the shipped printer does not yet, and the sentence does not say it does.
    • "the entry is reported as a TODO of its hop whether or not any edit it names was applied" — true, chain.ts:102 maps every step.semantic entry.
    • "never derive one from the entry's prose naming the id" — sourced: triage 5897646840.
    • The parenthetical "(keep the written value, delete it, or narrow the source instead)" lists the shipped link's three verdicts as if they were the general shape of a judgment. True for the one link; a future link over a rename conversion would read past it. Not false; the rule is the clause before it.
  • Entry comment, 18.flow-decision-edge-branching-first-match.ts:87-88: "every mode: 'inclusive' that conversion writes is one decision to keep, delete or narrow, per the criteria above" — true: the conversion's summary says what it writes; criteria (1) delete, (2) keep, (3) narrow.
  • Test comments and messages, migrations.test.ts:75-121: the join "keyed on MigrationApplication.conversionId" — true; the three ways a link pairs nothing (typo, later major, step below the floor) — each true against CONVERSION_IDS and MIGRATIONS_BY_MAJOR; the remedy's gen:migration-registry — a real script (packages/spec/package.json:286); expect(conversion.toMajor).toBe(18) — true; applyMetaMigrations(before, 17, 18) — matches the signature (chain.ts:68-72), and 17 is above the floor of 16.
  • Changeset .changeset/20697-semantic-migration-conversion-ids.md: "exported by @objectstack/spec" — true; "the same conversionId that the conversion's MigrationApplication rows carry" — true; "so objectstack migrate meta --json shows it on that todo" — true on main when this PR lands (the workspace CLI reads the spec's type; objectstack is a declared bin, packages/cli/package.json:23), and for a published CLI at the release that publishes this spec minor, with no CLI change needed; "Nothing is removed or renamed, and every entry is still reported as a todo of its hop" — true; "One link ships" — true.
  • PR body: "Closes spec(migrations): a semantic entry names the D2 conversions whose applied edits it judges (SemanticMigration.conversionIds), so os migrate meta can pair them (the spec half of #20620) #20697" — right (the "Part-of PR must not also close its card" and "The card this PR closes must claim this branch" checks both pass). "a dangling-id test" under-counts: the diff adds two tests (the dangling-id assertion and the end-to-end join). Incomplete, not false. "Clause-②: yes (widening)" — matches the changeset and the claim.
  • Not changed, noted: entries/README.md:53-60 still shows the five-member template and is silent on the new optional member. No sentence there is false; the TSDoc on the type the template imports is the authority. Non-blocking.

② Semver level

.changeset/20697-semantic-migration-conversion-ids.md: '@objectstack/spec': minor, body Clause-②: yes (widening). Matches the diff: one optional member added to an exported interface, nothing removed or renamed, no breaking marker owed (AGENTS.md: yes takes at least minor; (widening) is the arm for it). No other released package publishes a code change. The PR body carries the identical Clause-②: yes (widening) line; the claim's Clause-②: yes (widening) agrees. Check Changeset on the head: success.

③ Boundary flags

Check-runs on 32e8411a6b (read last; 46 runs, deduped by name keeping the newest started_at → 35 names; none still running): 30 completed success, 5 completed skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in)). Success includes Lint & Repo Gates, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Test Core (1/6..6/6 and roll-up), TypeScript Type Check and the four Type Check · gates, Dogfood Regression Gate (1/3..3/3 and roll-up), Dogfood Verify CLI, Temporal Conformance, Build Core, Check Documentation Links, Flag docs affected by code changes, and the three card-claim checks. Legacy status roll-up: success (1 context).

Implemented-by: claude/issue-20697-semantic-conversion-ids
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1

VERDICT: PASS

Adopted and posted by domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-09-29T21:41Z · rendered by the seat's at-tier review subagent on this head. The seat read its served tier family from the subagent transcript before posting.

  • The two wording notes are not false, so they stay as written: the TSDoc parenthetical is true for the one shipped link, and the PR body names one of the two added tests. The entry README's template stays silent on the optional member.
  • ③ The 56 remaining link candidates: the review is right that the entries live under packages/spec/**, so a spec follow-up carries them if more links are wanted. Triage made them optional, so the seat files nothing now.
  • Landing follows once needs:contract-review is stripped and the checks are green again.

Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 29, 2026 21:44
@os-justin
os-justin enabled auto-merge September 29, 2026 21:44
@os-justin
os-justin added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 6afccda Sep 29, 2026
51 checks passed
@os-justin
os-justin deleted the claude/issue-20697-semantic-conversion-ids branch September 29, 2026 22:14
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ommits that decided them (objectstack-ai#20717)

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

## What changed

This is the seventh stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/plugins/plugin-approvals/src/**` and nothing else. By the
seat's census at the claim (`5897866351`), it is the largest package in
the lane that no in-flight work holds. 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 the method of stages 1 to 6 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`). That is **41 sites on 38 lines in 13 files, covering 14
numbers**:

- 24 census sites (every census site this package has);
- 15 sites in test comments, which the census defers;
- 2 sites the gate's citation grammar cannot see, found by a raw scan
(see Acceptance notes): the second number of `objectstack-ai#8287/objectstack-ai#8778`
(`approval-node.test.ts:462`) and 「the option objectstack-ai#8710 rejected」
(`approval-service.ts:2329`), which the gate reads as an option ordinal.

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **13 distinct shas**. No number in this package has an ADR or
ruling record of its own in the repository (a grep of `docs/adr/` for
all 14 finds none, and a grep of the rest of `docs/` finds only an audit
that names `objectstack-ai#11311` as evidence), so every anchor is a commit, per
ruling C's order. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(41 lines out, 41 in, over 13 files), so no line citation into these
files moves. 3 of those 41 lines hold no dead citation: 1 reflow line
and 2 lost-referent lines, listed under Wordings below. 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: `objectstack-ai#8613`
(`approval-service.ts:2295`, `:2363`, `approval-service.test.ts:751`),
`objectstack-ai#8287` (`sys-approval-request.object.ts:138`,
`approval-node.test.ts:462`), `objectstack-ai#10101`
(`backfill-platform-row-organizations.ts:9`) and `objectstack-ai#12069`
(`translations/index.ts:26`). Each answers 200. 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 #N`
spelling in scope (`backfill-platform-row-organizations.ts:9`) became
its squash commit.

Five dead sites are left on purpose, all of them test strings (see the
list below).

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

## Census: `plugin-approvals`, 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-approvals/`. 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-approvals sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `575746371`, run 2026-09-29T20:15:05Z to 20:18:28Z |
enumerated, 186 pages, frontier objectstack-ai#20709 (newest objectstack-ai#20709 before and after),
18,536 numbers | 1,195 | **24** | 22 | 6 | 10 |
| after | head `e698d2393`, run 20:28:39Z to 20:31:58Z | enumerated, 186
pages, frontier objectstack-ai#20716 (newest objectstack-ai#20714 before, objectstack-ai#20716 after), 18,543
numbers | 1,171 | **0** | 0 | 0 | 0 |

The before count matches the seat's census at the claim and A1 (24
sites). The whole-repo drop is 24, exactly this diff's census sites. The
`resolves` tally is 32,971 in both runs, and `resolves-as-pull-request`
(1,984) and `cross-repo-unjudged` (995) did not move either. The after
run was taken on `e698d2393`; the head `708244c2b` adds only the
changeset. No run was truncated or discarded: both enumerations read 186
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 test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `namesThisRepository`
over every `.ts` file under `plugin-approvals/src` (76 files). It takes
its verdicts from the before census's own board reading rather than from
a second enumeration: a number is dead when that census reported it
`allocated-but-absent`, and alive when that census judged it on this
board anywhere (its `--list` extraction) and did not report it. The 18
numbers the census never saw, because they stand only in test files or
strings here, were read one by one on the issues endpoint: 14 answer
200, and `objectstack-ai#8863`, `objectstack-ai#11081`, `objectstack-ai#11286` and `objectstack-ai#11308` answer 404.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `575746371` | 981 | **44** | 24 | 15 | 0 | 5 |
| after, `e698d2393` | 942 | **5** | 0 | 0 | 0 | 5 |

Its src-comment column equals the census's 24, which is the control on
the second instrument. The 902 live citations and the 32 cross-repo
citations are the same in both readings, and the drop of 39 citations is
exactly the rewritten sites the gate grammar sees. Three extracted
tokens are not citations and stay unjudged in both readings: `&objectstack-ai#39;` (an
HTML entity) and two CSS colours, all in `action-link-pages.ts` string
literals. A third, raw reading (every `#` followed by 2 to 6 digits,
whatever surrounds it) finds 46 dead occurrences before and 5 after; the
2 it sees beyond the gate are the two gate-invisible sites above, and
its residue equals the gate's residue site for site.

## Per-number table

Sites and files count every dead occurrence 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, and `git blame` at the base puts every
rewritten line in its anchor commit or in a later commit that descends
from it (`merge-base --is-ancestor` exit 0 for each pair).

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#16709` | 10/2 | 8/2 | `8c7cca1ce`: the three residues of the
stranded-inspection contract review. Item 2 (the PM ruling of
2026-09-08) keeps a row whose third read threw in the report as the
undifferentiated `failed`; item 3 moves `refineFailedRunState` inside
the `try`, so a malformed host verdict costs only its own row. Its
message numbers the items, which is why the lines keep 「item 2」 and
「item 3」. New to the sweep |
| `objectstack-ai#8710` | 6/2 | 6/0 | `04d03c3a0`: a deactivated `sys_position`
confers no sharing-rule shares, filtered at the sharing call site and
never inside the addressing primitive. Its message quotes the 2026-08-15
ruling verbatim, the same sentence the quoted blocks here carry, and its
diff writes the 「a name with no row is untouched」 fallback that
`approval-service.ts:2318` quotes. Stage 2's anchor, and
`plugin-sharing/src/position-graph.ts:42` already reads 「objectstack-ai#8613 / commit
04d03c3」 |
| `objectstack-ai#6523` | 4/3 | 4/0 | `aa4b90d9a`: the 36 enforcement signatures,
`IApprovalService` among them, converged onto the full
`ExecutionContext`. Its subject names it. Stages 2 and 6 and the spec
stage's anchor |
| `objectstack-ai#6206` | 3/3 | 3/0 | `aa4b90d9a`: the same commit, whose body applies
「the objectstack-ai#6206 ruling default (converge on the full envelope, keep no
per-site subset contracts)」. Written as the full-envelope ruling, the
form stages 2 and 6 used |
| `objectstack-ai#8778` | 4/4 | 4/0 | `7901b2dd2`: the stamp-only
`tenancy.organizationField`, Option A of the maintainer's ruling,
declared on `sys_api_key` as `active_organization_id`. The spec,
`plugin-security` and `service-storage` stages' anchor |
| `objectstack-ai#11081` | 5/1 | 5/0 | `c28e4cfae`: the two SqlDriver-backed fixtures
of `objectstack-ai#11081` stop muting their kernel and pin the expected read-refusal
noise with the runtime's shared capture. Its diff writes all five
`[objectstack-ai#11081]` tags. New to the sweep |
| `objectstack-ai#11286` | 5/1 | 2/3 | `b019891cd`: the contract test that pins the
two `managerIsProvablyOutsideOrg` screens to equal verdicts. Its subject
names it. New to the sweep |
| `objectstack-ai#11674` | 2/1 | 2/0 | `1cba33f16`: the seed loader warns at load time
when a seed defers a required column, and the ordering constraint is
documented at the four pointer-pair sites, this object among them. Stage
2's anchor for the same paragraph |
| `objectstack-ai#12493` | 2/2 | 2/0 | `aa5994e17`: the Operation Message Catalog
gains `approval_recall_not_submitter` (and `record_write_denied`) ahead
of their emitters. Its diff names `objectstack-ai#12493` throughout. Stage 2's anchor
|
| `objectstack-ai#8707` | 1/1 | 1/0 | `1408fe385`: audit rows are stamped from the
record's own organization, which its message says the maintainer's
ruling on `objectstack-ai#8287` requires; the line keeps 「honouring objectstack-ai#8287's ruling」.
New to the sweep |
| `objectstack-ai#8863` | 1/1 | 1/0 | `d200b016b`: the two negative pins that assert
the unfiltered position expansion on the approvals side. Its body names
`objectstack-ai#8863`. New to the sweep |
| `objectstack-ai#11308` | 1/1 | 1/0 | `5a916c4d4`: the one-off platform-row
organization backfill, dry run and write, which its body calls the
`objectstack-ai#11308` sweep. New to the sweep |
| `objectstack-ai#11311` | 1/1 | 1/0 | `1272f0a6b`: the squash commit of the pull
request that was `objectstack-ai#11311` (its subject carries the number), which moved
the resolver to `metadata-core` and made the approval and automation-run
writers stamp the subject's organization. New to the sweep |
| `objectstack-ai#11671` | 1/1 | 1/0 | `09b4f4e4e`: the source-hashes provenance
companion. The identical `translations/index.ts` line in
`service-messaging`, `plugin-sharing` and `plugin-security` already
cites it |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 13), 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,129 commits).
Each of the 14 numbers answers 404 on the issues endpoint, read one by
one; `objectstack-ai#11311` answers 404 on the pulls endpoint too.

## Wordings to check

- **The full-envelope ruling, `approval-node.ts:29`,
`approval-service.ts:52-53` and `exec-context-annotation.pin.ts:7-8`.**
「since objectstack-ai#6523 (the objectstack-ai#6206 ruling …)」 became 「since commit aa4b90d (the
full-envelope ruling …)」, word for word the form `plugin-sharing`'s
landed `sharing-service.ts:20` and `exec-context-annotation.pin.ts:7`
use. `approval-service.ts:53` is 1 reflow line.
- **The ruling's record, `approval-service.ts:2295` and
`approval-service.test.ts:751`.** 「Maintainer ruling, 2026-08-15 (objectstack-ai#8710,
inheriting objectstack-ai#8613), verbatim:」 became 「… (commit 04d03c3, inheriting
objectstack-ai#8613), verbatim:」. The quotation under it is the ruling itself and is
untouched; `04d03c3a0`'s message carries the same sentence.
- **`approval-service.ts:2329`.** 「that is the option objectstack-ai#8710 rejected」
became 「that is the option the ruling (commit 04d03c3) rejected」.
- **The test heading, `approval-service.test.ts:749`.** 「the objectstack-ai#8710
carve-out, asserted on THIS side (objectstack-ai#8863)」 became 「the commit 04d03c3
carve-out, asserted on THIS side (commit d200b01)」: the carve-out's
record, and the commit that asserted it here.
- **A PR number, `backfill-platform-row-organizations.ts:9`.** 「objectstack-ai#10101
(landed as PR objectstack-ai#11311)」 became 「objectstack-ai#10101 (landed as commit 1272f0a)」, the
pull request's squash commit.
- **Item numbers, `approval-service.ts:4893`, `:4906` and
`stranded-request-inspection.test.ts:123`.** 「[objectstack-ai#16709 item 3]」 became
「[commit 8c7cca1, item 3]」, and likewise for item 2, beside its 「PM
ruling, 2026-09-08」, which `8c7cca1ce`'s message records under 「Item 2」.
- **Lost referents, 2 lines with no dead site** (every file keeps its
line count): `backfill-platform-row-organizations.test.ts:17` 「the one
thing this card must not do」 became 「the one thing this sweep must not
do」, and `manager-org-screen-parity.contract.test.ts:61` 「the very
decision this card is fenced out of」 became 「the very decision this pin
is fenced out of」. Each 「this card」 pointed at the number the same
comment block opened with, which is now a commit; `b019891cd`'s message
says the pin 「PINS the duplication, it does not remove it」.

## The 5 sites left

- **Test strings, 5 sites**, left as stages 1 to 6 left theirs:
- `describe` / `it` titles:
`manager-org-screen-parity.contract.test.ts:232` (`objectstack-ai#11286`),
`stranded-request-inspection.test.ts:519` and `:642` (`objectstack-ai#16709`);
- a test double's thrown message and an assertion message:
`manager-org-screen-parity.contract.test.ts:107` and `:294` (`objectstack-ai#11286`).
- There is no operator string, generated header or quoted ruling
carrying a dead number in this package. The generated
`*.source-hashes.generated.ts` headers already cite `09b4f4e4e` and are
untouched. The two verbatim quotations of the 2026-08-15 ruling carry no
number and are untouched.

## Mechanical guard: no code token moves

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

- Real run: 36,204 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `sys-approval-request.object.ts` (「who a row is
ABOUT」 to 「whom a row is ABOUT」): 0 files changed, as expected (exit 0).
- Positive control, a code token added in
`sys-approval-request.object.ts` (`referenceVia: 'object_name',` given a
trailing `as const`): DIFFER (exit 1).
- Positive control, one digit changed inside a kept test title
(`stranded-request-inspection.test.ts:642`): DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs`, and each
landed (anchor 1 to 0, blob changed). Each restore was proven
byte-identical to the HEAD blob (`6cb56301a334`, `757ad45900ac`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/plugin-approvals`
(`.changeset/20596-plugin-approvals-provenance-anchors.md`) is included.
Its body is stage 6's, word for word, with the package name changed.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build (a cache miss for this package, so
`dist` is this head's source), the rewritten comments reach `dist`:
`8c7cca1ce` 4 times and `04d03c3a0` 4 times in each of `dist/index.d.ts`
and `index.d.mts`; `04d03c3a0` 4 times, `1cba33f16` twice, and
`8c7cca1ce`, `7901b2dd2` and `1408fe385` once each in each of `index.js`
and `index.mjs`. Positive controls: the unchanged line 「A step routing
to nobody is」, in the same docblock as the shipped rewrite at
`approval-service.ts:2295`, is found once in each of the four files, and
the unchanged line 「itself stays unwalled (`tenancy.enabled: false`)」
beside the shipped rewrite at `sys-approval-request.object.ts:144` once
in each JS file. A never-written negative phrase appears nowhere in
`dist`. None of the 14 dead numbers is left anywhere in `dist`.

## Gates (head `708244c2b`)

- **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 5 citations across 6 files, and all 5 resolve
(`objectstack-ai#8613` twice, `objectstack-ai#8287`, `objectstack-ai#10101`, `objectstack-ai#12069`), each already on the line
it replaces.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `708244c2b` derived 64 commands:
all 57 derived at dispatch, plus `check:dispatcher-error-vocabulary`,
`check:engine-double-contract`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt` and `check:where-matcher`. Each ran with its
exit code captured before any pipe, and all 64 exit 0. `--ran`, fed each
command with its exit code, reports 64 run, 0 NOT MEASURED (a derived
zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*`
and `./packages/*/*` ran first under the shared verify lock (71 of 71
tasks, exit 0), so no gate hit an unbuilt workspace.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/plugin-approvals test`: 51 files pass and
791 tests pass. That is every test file in the package, the 7 touched
ones included.
- `pnpm --filter @objectstack/plugin-approvals typecheck` exits 0 (`tsc`
on `tsconfig.json`, the scripts program, and the test layer on
`tsconfig.test.json`, held at its ledger of 8 files, 324 errors and 27
pinned signatures). `--listFiles`: the `tsconfig.json` program holds the
25 non-test files under `src/`, the 6 touched ones included; the
`tsconfig.test.json` program holds all 76 files under `src/`, the 51
test files and all 13 touched files included.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 13 touched `.ts` files gives 13 files, 0 errors and 0
warnings. All 13 are in eslint's own population (`isPathIgnored` is
false for each; a `dist` file, as the control, is ignored).
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), 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 14 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636). In this package there is one `#N-word` spelling, 「objectstack-ai#3266-era」
(`record-reader-visibility.test.ts:342`), and two `#A/#B` spellings,
`objectstack-ai#8287/objectstack-ai#8778` (`approval-node.test.ts:462`) and `objectstack-ai#8543/objectstack-ai#8580`
(`approval-vocabularies.test.ts:66`): the claim's 3, 1 and 2. `objectstack-ai#3266`,
`objectstack-ai#8287`, `objectstack-ai#8543` and `objectstack-ai#8580` answer 200; the second number `objectstack-ai#8778` is
dead, so that one line is rewritten here.
- **A third spelling the gate cannot see, found by the raw scan.**
`NON_CITATION_HEADS` excuses any `#N` after the word 「option」 as an
option ordinal, so 「the option objectstack-ai#8710 rejected」
(`approval-service.ts:2329`) was never extracted: a dead number there
would pass the diff gate at exit 0 and never enter a census count. It is
rewritten here. Across the gate's declared surfaces at the base, the
only other `option #N` with three or more digits is
`packages/objectql/src/validation/rule-validator.ts:2202` (`option
objectstack-ai#14088`), which answers 200. Same family as objectstack-ai#20636; noted for its
closeout, not a card of its own.
- **A retired key name in this package's prose, not changed here.**
`tenancy.organizationField` left the authorable surface in `502f179cc`,
and limb 0 of the shared resolver now reads
`PLATFORM_STAMP_ORGANIZATION_COLUMNS` in `metadata-core`, keyed by
object name. Comments in this package still name the retired key as what
limb 0 reads (`sys-approval-request.object.ts:143`, the line above a
rewrite; `backfill-platform-row-organizations.ts:35`,
`approval-node.test.ts:463`, `approval-service.ts:2707`,
`backfill-platform-row-organizations.test.ts:50`), and two test fixtures
still declare it on a stub `sys_api_key` (`approval-node.test.ts:467`,
`backfill-platform-row-organizations.test.ts:54`), where it is inert
because the resolver keys by name. The anchor `7901b2dd2` is right for
the key those lines name, and nothing is wrong at runtime. Correcting
the prose would reach past the dead citations, and the fixtures are code
tokens, so none of it is changed here.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#16709` →
`8c7cca1ce`; `objectstack-ai#11081` → `c28e4cfae`; `objectstack-ai#11286` → `b019891cd`; `objectstack-ai#11308` →
`5a916c4d4`; `objectstack-ai#11311` → `1272f0a6b`; `objectstack-ai#8707` → `1408fe385`; `objectstack-ai#8863` →
`d200b016b`.
- **Base.** The branch is on `main` at `575746371`, which is still
`main` at 20:56Z (read into a private ref), 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/s tests tooling

Projects

None yet

2 participants