Skip to content

feat(adr-0087): retire a deleted published package whole (PACKAGE#*, D9 amendment) - #26766

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-t1c00n
Oct 28, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-t1c00n

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #26220
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:ADR-0087 D9 加整包删除条款:已发布的包在 HEAD 被删除时,runtime-export-retired 的操作数写成 包名#*;门禁 check-adr-0087-registration 加对应谓词,自测新增 WP 电池 55 条。
  • 为什么改:执行 PR refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365 评论 6439038287 的裁决 A(维护者「同意」)。被删的包在 HEAD 没有入口可读,@object-ui/plugin-ai 的下线(refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365)在这个门禁上会一直红。
  • 风险与回滚:只放行整包删除这一种新形态,而且条件要全部成立:基点上是已发布的工作区包,HEAD 上没有 package.json、pnpm-workspace.yaml 不再列它的目录、不在 changesets 的 fixed 组。其他判定不变,原有 627 条自测照样全绿。回滚即 revert 本 PR。
  • 先看:docs/adr/0087-metadata-protocol-upgrade-contract.md 里 D9 下面的第二条 2026-10-27 修订说明;scripts/check-adr-0087-registration.mjs 的 verifyWholePackageRetired 和谓词 2 的整包分支(packageReference)。
  • 两分钟验证:node scripts/check-adr-0087-registration.mjs --self-test,预期以 0 退出并打印 682 assertions。
  • 需要你做的:无。本 PR 等主任席位的契约复核(CONTRACT_REVIEW_TIER)PASS 和席位 ACCEPT,在那之前不需要你操作。

What changes

ADR-0087 D9, amended (Tier H). A second dated note under D9's unmappable-entry paragraph, plus a Status-line entry, records the ruling on PR #26365 (comment 6439038287, letter A, the maintainer's 「同意」 2026-10-27T10:07Z). A published package deleted at HEAD is retired whole with a third operand kind, PACKAGE#*:

  • Predicate 1 (retired-from-entry) is proven about the package. At the merge base it must be a workspace member, not private, and its published entry must map as D9 already requires. At HEAD it must be absent from the workspace in all of the ruling's three senses: no package.json (the base manifest is gone, and no workspace manifest carries the name, so a package that moved or was renamed is not deleted), no pnpm-workspace.yaml glob naming its directory literally, and no fixed group in .changeset/config.json listing it. A missing or unreadable config is refused, never read as an empty group.
  • Predicate 2 (no-metadata-surface) asks the package what it asks an export, at the merge base: none of the package's files is a metadata surface, and no metadata surface outside it names it in code (comment spans masked): an import specifier, by its root or a subpath, or any other occurrence of the package name, a string value included.
  • Predicate 3 (from-operands-named): the FROM → TO block names the package on a single line, not one per export, by its package name. Its exports take no line of their own.

An export or member operand on a deleted package is still refused at predicate 1, and the refusal now names the PACKAGE#* spelling. The closed-vocabulary blocks of the 2026-08-13 and 2026-10-12 addenda list the third marker line, and the 2026-10-12 addendum now reads "three operand kinds".

scripts/check-adr-0087-registration.mjs.

  • parseExportOperand accepts PACKAGE#*.
  • verifyWholePackageRetired sits beside sourceEntryAt and holds predicate 1's package proof. It names every HEAD condition that fails, not only the first.
  • packageReference and a whole-package branch hold predicate 2. It refuses on the first comment-masked occurrence of the package name outside the package's own files, and reports an import specifier as IMPORTS.
  • Predicate 3 reads a FROM operand equal to a #* package's name as that operand.
  • The header comment carries the new operand.

Why

The ruling closes the last open question on #26220. Without the clause, a whole-package retirement could only land red. Its alternatives were registered, which would name a conversion that never happens, and not-required (unpublished), which reads private: true at HEAD, a field a deleted package cannot carry.

Tests

--self-test at 994c3279: exit 0, 682 assertions over real temp git repos (627 at base dc9d1ff8). The new battery WP (#26220) has 55 assertions, and the roster floor goes from 57 to 58 batteries. Each red differs from WP-G1 in one respect.

WP cases
  • G1: the refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365 shape. G1b: the verdict names #* and the manifest. G2: the one line written as an arrow removal. G3: a metadata surface that names the package only in comments.
  • R1: still in the fixed group. R2: private: true at base. R3: not a workspace member at base. R4: not deleted. R5: moved to another directory under the same name. R6: pnpm-workspace.yaml still names the directory. R7: no changesets config at HEAD. R8: no mappable published entry at base.
  • R9: the package carries a *.zod.ts. R10: a metadata surface outside it imports it.
  • R11: no FROM line. R12: per-export FROM lines instead of the package line. R13: an export operand on the deleted package points at #*.
  • R14: a metadata surface outside the package names it as a string value, with no import.
  • U1–U5: operand grammar; packageReference reads a subpath import, skips the package's own files, does not match a name that only shares the prefix (plugin-xy), and reports a string-value mention with its line while skipping a comment.

First consumer, measured with this branch's gate against origin/main dc9d1ff8:

  • PR refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365's head bde1e722, unchanged: exit 1. Each of its three export operands is refused at HEAD, and each refusal now names @object-ui/plugin-ai#*.
  • A local probe commit on that head that changes only its changeset (the marker becomes runtime-export-retired @object-ui/plugin-ai#*, and its three per-export FROM lines become one - FROM `@object-ui/plugin-ai` TO … line): exit 0, verified: @object-ui/plugin-ai#* (package, ui/packages/plugin-ai/package.json).

So #26365 needs exactly those two changeset edits. They belong to that PR and are not made here.

Acceptance notes

  • Wording beyond the ruling's text. The ruling fixes predicate 1's proof and predicate 3's one line. It says nothing about predicate 2. D9 requires all three predicates for every operand, so the note restates D9's own predicate 2 for the package: no metadata-surface file in the package, and no metadata surface outside it naming the package in code (contract review 6457699551, must-fix 1). "Its exports take no line of their own" follows from predicate 3's existing "exactly the named operands".
  • The fixed group is read literally. Only fixed is read, as the ruling names it. A glob entry names no package, and linked / ignore are not read.
  • No ablation. os-dev excludes scripts from ablation. Each red asserts the specific refusal text it expects, and refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365's before/after pair above is the end-to-end witness.
  • No changeset. Nothing publishes: the root package is private, and docs/adr/** is not shipped. The PR carries skip-changeset.

Verification

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 994c3279 (merge base dc9d1ff8, 2 paths): 44 commands derived, 44 run, all exit 0. Each ran with stdin from /dev/null, and each exit was captured before any pipe. check:doc-formula-expressions ran after turbo run build of @objectstack/formula and @objectstack/lint under the verify lock. --ran reconciliation: 44 derived, 44 run, 0 NOT-MEASURED, 0 UNRUN. No test file names the edited script (git grep over *.test.* / *.spec.*: 0 hits).

Tier H (docs/adr/**). The director seat's contract review at CONTRACT_REVIEW_TIER is PASS on 994c3279 (comment 6458009682), after a FAIL on 75285daf (6457699551). Landing waits for the seat's ACCEPT.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No packages/spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran; lint is CI's run.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset. (This one publishes nothing: skip-changeset.)
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (维护者速读 is Chinese by house rule.)
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…ge>#*`

ADR-0087 D9 gains a whole-package operand for `runtime-export-retired`,
per the ruling on PR #26365 (comment 6439038287, letter A). A published
package deleted at HEAD has no entry to read an absence from, so the
operand is the package itself: predicate 1 proves it was a published
workspace member at the merge base and is absent at HEAD (no
package.json, no workspace glob naming its directory, not in the
changesets fixed group); predicate 2 asks the package for a metadata
surface or a metadata-surface importer; predicate 3 reads its one FROM
line by package name. The gate gains that predicate and a 50-assertion
WP battery.

Claude-Session: https://claude.ai/code/session_01JQwxbX6swE1AuYRHTZsPqo
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Oct 27, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review October 27, 2026 15:09
@os-zhuang
os-zhuang enabled auto-merge October 27, 2026 15:09
@objectstack-fleet
objectstack-fleet Bot disabled auto-merge October 27, 2026 15:10
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat domain:ui#1 · session_01JQwxbZeBv2y91towxasSUT · 2026-10-27T15:10Z: auto-merge is off on this PR, held for the contract review.

This is Tier H (docs/adr/**). The maintainer's standing word for this project (2026-10-27, on #26566 / #26570): a Tier H PR goes to approvers only after the director seat's contract review PASS and the seat's ACCEPT; #26617 records why the relay's reviewer request at pr_create is not that signal. Neither record exists here yet. The approval stays on file and is not dismissed; once the contract review PASS and the seat's ## 席位核对 ACCEPT are posted, the seat re-arms auto-merge itself.

Two wording points beyond the ruling's literal text are flagged in the PR body's Acceptance notes for that review (predicate 2's package reading; a deleted package's exports take no FROM line of their own).


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 75285daf4f1e4acd3475391b33c4a318d4671616
Local-runs: none
Scope: docs/adr/0087-metadata-protocol-upgrade-contract.md + scripts/check-adr-0087-registration.mjs (the gate the D9 note binds; read whole at the head and on main), and the cited code those hunks make claims about, read unchanged: scripts/js-comment-mask.mjs (maskComments), .changeset/config.json, pnpm-workspace.yaml, ui/packages/plugin-ai/package.json (the first consumer's base manifest), .github/workflows/pr-automation.yml (the job that runs --self-test and --base). The diff has two files and both are in scope; no remainder goes to the lane seat at TIER_DEFAULT.

Inputs read: card #26220 body and all 14 comments (the claim 6444671327, the dispatch order 6444498687, the ruling pointer 6439050525, the four os-dev reports); PR #26766 body, file list, its one comment 6445680015 and its one review; the net diff against main at the head (one commit, +302/-10, 2 files, matching files.txt); 46 check-runs on the head; the ruling 6439038287 on PR #26365, plus that PR's body and its two earlier Check Changeset comments (6431333164, 6435952905), fetched read-only.

Gate verdicts on the head (the check-runs ARE the verdicts): 46 runs, all completed; 29 success, 17 skipped (UI lanes and opt-ins filtered), 0 failures, 0 in progress. Check Changeset (success, 15:00:50Z) is the pr-automation job whose step "Require an ADR-0087 disposition on a declared-breaking changeset" runs node scripts/check-adr-0087-registration.mjs --self-test then --base "$MERGE_BASE", so the gate's self-test exit 0 on this head is CI-measured, not only claimed. Lint & Repo Gates, Lint · docs & code guards, Check Documentation Links, Governed Surface Queue Guard (both runs) and PR-body guards are green.

① Derived judgments

Each accept-set and public-surface change the diff implies, named right or wrong:

  1. The gate's accept-set widens by exactly one shape and nothing else — right, with one wrong limb (item 3). parseExportOperand admits PACKAGE#* only when the spelling is literally * (#**, #*x and a bare #* stay refused: WP-U2); every other operand parses as before with whole: false. The whole-package branch runs only on op.whole; the export and member paths are untouched except for the appended #* hint on the "cannot be read at HEAD" refusal, which fires only when packagesAt(head) lacks the package (WP-R13). The 627 pre-existing assertions and their pins are unchanged in the diff. No existing green becomes red, no existing red becomes green.

  2. Predicate 1's package proof matches the amended text, and the text stays inside the ruling — right. Ruling: "in the workspace with a published entry at the merge base; absent from the workspace at HEAD (no package.json, not in pnpm-workspace.yaml, not in the changesets fixed group)". verifyWholePackageRetired reads, at the base: packagesAt(base) membership (enumerated from that rev's own pnpm-workspace.yaml via workspaceGlobsAt, fix(scripts): ADR-0087 registration gate reads ui workspace packages and .tsx modules #26319), private refused, sourceEntryAt mappable (the D9 mapping, src/index.ts or src/index.tsx, both-present refused as ambiguous). At HEAD: the base manifest path gone (showOrNull(head, atBase.file) === null), no workspace manifest of that name at another path, no literal glob equal to the package directory in pnpm-workspace.yaml, and .changeset/config.json present, parseable, with no fixed group containing the name; a missing or non-array fixed is refused, never read as empty. Every failing HEAD condition is listed, not only the first (WP-R4 asserts two). Each elaboration beyond the ruling's nine words refuses more and admits nothing more: "not private" and "maps as above" are what D9 already means by "published entry" ("A private package has no published entry"; "an entry that cannot be mapped is refused"); "no glob names its directory literally" is the only reading of "not in pnpm-workspace.yaml" under which the ruling's own first consumer can exist (plugin-ai sits under ui/packages/*, which stays); the config-missing refusal is the gate's standing check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 posture. Measured on main: .changeset/config.json lists every fixed member by literal name (108 entries, @object-ui/plugin-ai among them), linked is [], no glob entries — so the literal reading loses nothing today, and it matches the condition's purpose (a stale literal name breaks changeset version; a glob cannot). Right.

  3. Predicate 2's package reading narrows D9's existing predicate 2 — wrong (must-fix 1). The note writes "no metadata surface outside it imports it in code, by its root or a subpath", and packageImporter reads exactly that: a from, bare import, import( or require( specifier naming the package or a subpath, comment spans masked, own files skipped (WP-U3/U4, G3, R10 pin it). But D9's predicate 2 for an operand is "no metadata surface references it in code: the D7 primitives", and the D7 primitive the gate shares for exports, metadataSurfaceReference, refuses on an import or on a bare code mention — with string literals deliberately left unmasked (gate header: "STRING LITERALS ARE NOT MASKED, on purpose … the conservative direction here is to REFUSE (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690) and a string is where an authorable value lives — a name in one may be doing work … a future widening to comment || literal has to be argued rather than slipped in"). A package name can appear in code only inside a string, so for this operand "references in code" collapses to "names it in any non-comment string"; the amendment keeps only the import-specifier subset and admits a metadata surface that carries the package name as a value (a plugin id, a package reference in an object definition). The ruling fixed predicates 1 and 3 and said nothing about 2, so it grants no authority to narrow 2; the PR body's own claim that predicate 2 "asks the package what it asks an export" is not what the code does. Measured on main: the four @object-ui/plugin-ai mentions in metadata surfaces (ui/packages/types/src/zod/ai.zod.ts ×3, zod/index.zod.ts) are all comment spans, so the first consumer is unaffected either way, and the fix is one predicate plus one sentence plus one red case.

  4. Predicate 3 reads the package's FROM line as the operand's spelling — right, one wording point (③-c). A FROM operand whose canonicalFromOperand is null and whose tick-stripped text equals a #* package's name is re-read as that spelling; "exactly the named operands" then runs unchanged, so a per-export FROM line is out of range (WP-R12) and a missing package line is "no FROM side" (WP-R11). The FROM-line and arrow forms both reach it (G1, G2). Right.

  5. Refusal text — right. An export or member operand on a package deleted at HEAD keeps its predicate-1 refusal and gains the PACKAGE#* remedy, as the note says.

  6. Public surface: none — right. The root package is private; scripts/** and docs/adr/** are not published; packages/spec is untouched (no schema, no accept-set of any published artifact moves). parseExportOperand's return shape gains whole and a nullable exportName, and packageImporter is newly exported; git grep over the head finds no importer of either outside the script. Internal only.

  7. Self-test roster — right. SELF_TEST_BATTERY_FLOOR 57 → 58, battery WP (#26220) pinned at 50; the pins are floors (a battery reds only when its count falls below the pin), and the diff's own arithmetic is 3 greens + G1b + 13 reds (13 + 29 regexes) + U1–U4 = 50 exactly, 627 + 50 = 677 as the body states. CI measured exit 0; the counts are consistent with the diff.

  8. ADR text, Status line — right. One new Amended (2026-10-27, #26220, ruling A on PR #26365, comment 6439038287 …) entry, pointing at "the second 2026-10-27 amendment note under D9's unmappable-entry paragraph", which is where the note sits. The note's provenance line names the comment, the letter and the maintainer's 「同意」 time.

  9. ADR text, the closed vocabulary — wrong (must-fix 2). The 2026-08-13 addendum's code block ("The vocabulary is closed") and the 2026-10-12 addendum's block ("two operand kinds that may be mixed in one marker") still list only PACKAGE#Export and PACKAGE#Type.member, while parseExportOperand at the head accepts a third spelling. The grammar the ADR declares closed is now narrower than the grammar the gate enforces; the new note is the only place the spelling appears.

② Semver level

skip-changeset is correct: nothing in the diff publishes (root private, scripts/** and docs/adr/** unshipped, packages/spec untouched), so no package's version moves and no changeset is owed. Clause-②: no is correct — no published accept-set narrows or widens; Metadata-protocol: none is correct — no protocol major, no conversion, no deprecation entry. The os-dev deviations entry explaining why the dispatch order's "Changeset in the root .changeset/" was not followed is right on the facts.

③ Boundary flags

Every dev flag and every open_questions entry, answered or escalated:

  • (a) Predicate 2's package reading (PR body "Acceptance notes", report deviation 4). Judged against the ruling: outside it — the ruling is silent on predicate 2, so the note may only restate D9's predicate 2 for the new operand, not re-cut it. Judged against D9's existing text: narrower — "references it in code: the D7 primitives" became "imports it", dropping the code-mention limb the shared primitive applies to every other operand. Not accepted; must-fix 1 (text + gate + one red case: a metadata surface outside the package that names it in a non-comment string). Expected text: "no metadata surface outside it names it in code — an import specifier, by its root or a subpath, or any other non-comment occurrence of the package name (comment spans masked, as metadataSurfaceReference reads)". Expected gate: packageImporter (or a sibling) refuses on the first comment-masked occurrence of the package name outside the package's own files, reporting the line; WP-G3 stays green, WP-R10 stays red, a new WP red pins the string-value mention.
  • (b) "Its exports take no line of their own" (same two flags). Judged inside the ruling and inside D9: the ruling says the block "names the package on one line", and D9's predicate 3 already refuses any FROM operand the marker does not name; with the package as the operand, an export's FROM line is out of range by existing text. The sentence states a consequence, not a new rule. Accepted.
  • (c) "on one line" (text) vs the gate. The gate does not count: two FROM lines both naming the package pass, exactly as predicate 3 has always tolerated a repeated FROM line for an export. The text's emphasised "one" reads as "one line rather than one per export", which the gate enforces; if a count is meant, the gate does not enforce it. Should-fix with must-fix 1 or 2: drop the emphasis or say "a single line, not one per export". Not blocking.
  • (d) "a package that moved or was renamed is not deleted". The proof sentence is "no workspace manifest carries its name", and that is what the gate reads (WP-R5). It excludes a same-name move (a directory rename). A package renamed to a new name in a new directory has no manifest carrying the old name and passes as deleted — correctly, since the old published name is gone for every consumer and the FROM line's TO side names the successor; but the parenthetical over-reads the proof. Should-fix: "moved (its directory renamed)". Not blocking.
  • (e) The fixed group read literally (PR body). Accepted: see ①-2. linked and ignore are not named by the ruling and are rightly not read.
  • (f) No ablation (PR body). Accepted for a scripts/** change under os-dev's rule; each WP red asserts its refusal text, and the first-consumer pair (refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365 head bde1e722 exit 1 with each refusal naming @object-ui/plugin-ai#*; probe f4d766b5 exit 0) is the end-to-end witness. That pair is the dev's measurement, not re-run here (read-only); it is consistent with the code path and with main (plugin-ai: non-private, types: dist/index.d.ts, src/index.tsx present, listed in fixed, imported by no metadata surface).
  • (g) open_questions: none in report 6445436951. The two earlier open questions on the card (D8 predicate 4; whole-package removal A vs B) are closed by decision A (6433464805) and ruling A (6439038287). Nothing left to escalate from the reports.
  • (h) Out-of-scope finding (report): refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365 needs two changeset edits. Correctly left to that PR; the ruling itself assigns them there.
  • (i) Draft and approval state — escalated to the seat, not a diff matter. The PR body says "The PR is draft" and the claim says "Tier H: draft only", but pr.json reads draft: false, and one APPROVED review (os-zhuang, 2026-10-27T15:09:21Z, at this head) is on file — the relay's pr_create reviewer request (fleet-write: relay pr_create asks the governed approvers to review a Tier H draft before any contract review exists, so an approver can approve and merge it unreviewed (#26566, #26570) #26617) produced an approval before this record existed. The seat's comment 6445680015 says auto-merge is off and the approval "stays on file and is not dismissed"; with this record FAIL the seat must not re-arm auto-merge, and the approval does not survive the must-fix push in any case (a new head).
  • (j) Reviewer-request deviation (report deviation 2). Noted; nobody was asked to approve by the dev. Consistent with (i).

Must-fix, before a PASS on a new head:

  1. Predicate 2 for PACKAGE#*: restore D9's reading. Text: "no metadata surface outside it names it in code (comment spans masked): an import specifier, by its root or a subpath, or any other occurrence of the package name". Gate: refuse on the first comment-masked occurrence outside the package's own files (the import-specifier reader becomes the special case that reports the specifier line), plus one WP red where a *.zod.ts or object definition carries the package name in a string value, G3 (comment only) staying green. Then re-pin the WP floor.
  2. The closed vocabulary: add a third runtime-export-retired marker line with the PACKAGE#* operand (spelled exactly as the existing two lines spell their placeholders) to the 2026-08-13 addendum's code block and to the 2026-10-12 addendum's code block, and change "two operand kinds" to three, each with a short "(amended 2026-10-27, check-adr-0087-registration: PACKAGE_ROOTS omits ui/packages, ui/apps and ui/examples, so no @object-ui package can use the runtime-export-retired disposition #26220)" tag, so the grammar the ADR declares closed is the grammar parseExportOperand accepts.

Should-fix in the same push: ③-c and ③-d wording.

Implemented-by: claude/project-thread-t1c00n
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: FAIL


Generated by Claude Code

…, not only an import

Contract review patch round. Predicate 2 for `<package>#*` returns to
D9's reading, "no metadata surface references it in code" with comment
spans masked: `packageReference` refuses on any non-comment occurrence
of the package name outside its own files, an import specifier being
the case reported as `import`. New WP-R14 (a string value in an object
definition) and WP-U5; WP floor 50 -> 55.

The ADR's two closed-vocabulary blocks (2026-08-13 and 2026-10-12
addenda) gain the `<package>#*` marker line and the "three operand
kinds" wording, and the D9 note's predicate 2, "single line" and
"moved, its directory renamed" wording follow the review.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 994c3279776d3e1e1ed75eda0b0424f63a9149d1
Local-runs: none
Scope: docs/adr/0087-metadata-protocol-upgrade-contract.md + scripts/check-adr-0087-registration.mjs (the gate the D9 note binds; the whole file read at the new head, the delta 75285daf..994c3279 read hunk by hunk), and the cited code those hunks make claims about, read unchanged: scripts/js-comment-mask.mjs (maskComments), .changeset/config.json, pnpm-workspace.yaml, ui/packages/plugin-ai/package.json and ui/packages/types/src/zod/ai.zod.ts + index.zod.ts (the first consumer's base manifest and the only metadata surfaces on main that name it), .github/workflows/lint.yml and pr-automation.yml (where the gate's --self-test and --base run). The diff is two files and both are in scope; no remainder goes to the lane seat at TIER_DEFAULT.

Re-review of the head that answered the FAIL record on 75285daf4f1e4acd3475391b33c4a318d4671616 (comment 6457699551). Inputs read: everything the first record read, plus the one new commit 994c327977 ("fix(adr-0087): a whole package's predicate 2 refuses any code mention, not only an import"; ADR 31 lines changed, gate +60/-31; merge base still dc9d1ff8), the 41 check-runs and 8 workflow runs on the new head with the job and step conclusions of PR Automation and Lint & Type Check, the PR's current metadata, review and comments, all fetched read-only.

Gate verdicts on the head: 41 check-runs; 26 success, 14 skipped, 1 still queued — Test Core, the aggregating job; its three shards Test Core (1/8), (2/8), (3/8) are success. No failure. Check Changeset is skipped on this head: the job-level skip-changeset exemption (pr-automation.yml changeset-check, and lint.yml's comment at 2894-2906 names exactly this inversion); it ran on the old head only because that PR Automation run began before the relay applied the label. The gate's --self-test on this head is CI-measured elsewhere: Lint · docs & code guards → step "Changeset-family gate self-tests" (pnpm check:changeset-gate-self-tests, which runs check-adr-0087-registration.mjs --self-test) → success, lane verdict success. The gate's --base scan did not run on this head; the diff carries no changeset, so it would judge nothing. Lint & Repo Gates, Lint · docs & code guards, Check Documentation Links, Governed Surface Queue Guard, PR-body guards, every Type Check lane: success.

① Derived judgments

The two must-fix items, checked exactly.

  1. Must-fix 1 (predicate 2 narrowed D9) — fixed, right. The note now reads: "no metadata surface outside it names it in code (comment spans masked, as metadataSurfaceReference reads): an import specifier, by its root or a subpath, or any other occurrence of the package name, a string value included." The gate's packageReference (renamed from packageImporter; no importer of either name outside the script) reads each metadata surface outside the package's own directory through the shared maskComments (one masker — the same scripts/js-comment-mask.mjs export metadataSurfaceReference uses; strings deliberately left standing), tries the import-specifier regex first and reports kind: 'import', else a package-shaped token regex and reports kind: 'mention', each with the offending line. That is D9's "references it in code" at D7's posture, for a name that can only live in a string. There is no second copy of the masking primitive; what is package-specific is only the token boundary, and that is necessary rather than duplicative: metadataSurfaceReference is identifier-shaped (\b word boundaries, local-declaration and member homonyms, import attribution to a declaring module) and cannot take a scoped package name — \b before @ demands a preceding word character, which a quoted specifier never has — so calling it would have been wrong, not merely awkward. The new WP-R14 red pins the string-value case (an object definition with plugin: '…', no import) and asserts both the "names … IN CODE" verdict and the offending line; WP-U5 pins the unit: a string value is a mention with its line, a comment alone is not; WP-G3 (comment-only mention) stays green; WP-R10 (import) and WP-U3/U4 (subpath import; plugin-xy is not plugin-x) stay as they were.

    Is the gate wider than D9's predicate 2? No. It refuses exactly what the D7 primitive refuses for an export — a comment-masked code occurrence, string literals included — translated to a package name. Is it narrower than the amended text? By one token-boundary residual, named here and graded should-fix, not blocking: the mention regex's negative lookbehind (its excluded set is a word character, @, ., / or -) also excludes an occurrence glued to a preceding /, so a path- or URL-embedded occurrence (…/node_modules/@object-ui/plugin-x, https://npm.im/@object-ui/plugin-x) inside a string is not read, while the text's "any other occurrence" covers it. The other exclusions are right (a longer package name sharing the prefix or suffix is another package). D7's export scan (\bSym\b) would read a path-embedded symbol, so this is a package-reader-only gap. The realistic shapes — import, a value string, a version spec pkg@1.0.0 — are all refused. Fix is one character and a U-case; the text needs no change.

  2. Must-fix 2 (closed vocabulary) — fixed, right. Both code blocks now carry the third spelling — the 2026-08-13 addendum's block (line 756) and the 2026-10-12 addendum's block (line 1047) — and each is dated in the prose beside it: "runtime-export-retired — the seventh, added 2026-10-12, whose whole-package operand PACKAGE#* was added 2026-10-27 (check-adr-0087-registration: PACKAGE_ROOTS omits ui/packages, ui/apps and ui/examples, so no @object-ui package can use the runtime-export-retired disposition #26220)" under block one; "three operand kinds that may be mixed in one marker (the third, PACKAGE#*, amended 2026-10-27, check-adr-0087-registration: PACKAGE_ROOTS omits ui/packages, ui/apps and ui/examples, so no @object-ui package can use the runtime-export-retired disposition #26220 — see the second 2026-10-27 amendment note under D9)" above block two. "Two operand kinds" is gone. The grammar the ADR declares closed is now the grammar parseExportOperand accepts. The new line is written without the [, ...] tail; mixing is stated in the prose and the gate parses a list, and the one mix that must not pass — an export operand of the same deleted package beside #* — is refused at predicate 1 (WP-R13).

The two should-fix wording points.

  1. "on one line" → "on a single line, not one per export" — fixed as asked; the text no longer implies a count the gate does not take, and "Its exports take no line of their own" follows.
  2. "moved or was renamed" → "moved, its directory renamed" — fixed as asked; the parenthetical now matches the proof it glosses ("no workspace manifest carries its name", WP-R5).

The delta in its own right.

  1. Refusal text — right. Predicate 2's whole-package refusal now reads "IMPORTS pkg IN CODE" or "names pkg IN CODE" with the line; the "comment spans are already excluded" sentence stands. No other message changes.

  2. First consumer stays green — right, as far as main shows. On origin/main, every metadata surface (the gate's classifier: *.zod.ts, packages/spec/src/contracts/**, *.object.ts, objects/*.ts, tests excluded) naming @object-ui/plugin-ai does so inside a comment span only: ui/packages/types/src/zod/ai.zod.ts lines 14, 33, 57 (inside the /** … */ opened at line 9) and ui/packages/types/src/zod/index.zod.ts:444 (a // line). maskComments blanks all four, so neither the import nor the mention limb fires; plugin-ai's own directory carries no metadata surface. The old head's measured pair (refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365 bde1e722 exit 1 naming #*; probe f4d766b5 exit 0) is unaffected by this delta on those facts. Not re-run here (read-only), and refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365's eventual merge base may differ from today's main.

  3. WP battery count — right. Counted mechanically from the file text at both heads (a static count of green/red/assert calls, one plus the regex count per red, not a run): old head 3 greens + 13 reds (13 + 29) + 5 asserts = 50, the figure CI measured there; new head 3 greens + 14 reds (14 + 32) + 6 asserts = 55, equal to the new pin. The roster floor stays 58. The total the self-test prints is therefore 682 (677 + 5); CI's step success on this head confirms every battery met its floor.

PASS-side judgments from the first record, re-confirmed on the new head. The delta touches only predicate 2's whole-package reader, its refusal text, the WP battery and pin, and the ADR prose named above; git diff 75285daf..994c3279 shows no other hunk. So these stand unchanged: predicate 1's package proof matches the amended text and stays inside the ruling (verifyWholePackageRetired untouched; .changeset/config.json on main still all literal names, linked []); predicate 3's package-line reading is untouched; the export/member paths and their 627 assertions are untouched; the #* remedy on a deleted package's export operand is untouched; the Status-line entry is untouched and still points at the second 2026-10-27 note under D9; no public surface moves (packages/spec untouched, nothing shipped; the renamed export packageReference and the kind field are script-internal).

② Semver level

Unchanged and correct: skip-changeset (root private, scripts/** and docs/adr/** unshipped, no package version moves), Clause-②: no (no published accept-set narrows or widens), Metadata-protocol: none.

③ Boundary flags

  • (a) Predicate 2's package reading (the first record's must-fix 1). Now inside D9 and inside the ruling: the ruling is silent on predicate 2, and the note restates D9's predicate 2 for the new operand at the D7 posture instead of re-cutting it. Accepted, with the should-fix residual in ①-1 (drop / from the mention lookbehind so a path- or URL-embedded occurrence counts; add a U-case). Not blocking: the class the FAIL named — a string value naming the package — is refused and pinned; the residual is a path segment, admit-direction but marginal, and the text already covers it.
  • (b) "Its exports take no line of their own". Unchanged from the first record: inside the ruling and D9. Accepted.
  • (c), (d) The two wording points. Both fixed as asked. Closed.
  • (e) The fixed group read literally; (f) no ablation; (g) open_questions: none; (h) refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365's two changeset edits left to that PR. Unchanged. Accepted / closed.
  • (i) Test Core is still queued on this head at review time. Its three shards are green and the delta changes no tested package (two repo-internal files), so no reason to expect otherwise, but this record does not assume it: the seat's ACCEPT and any re-arm wait for it to complete success.
  • (j) PR body is stale against this head — escalated to the seat, should-fix by the dev, not a contract matter. The body still says the self-test prints 677 assertions (now 682), names packageImporter (now packageReference) in 「先看」, "What changes" and the WP U1–U4 bullet, carries no R14/U5 line, cites 75285daf as the measured head, and still says "The PR is draft" while draft: false. The 「两分钟验证」 promise is the one a maintainer acts on, so it should be corrected before approval.
  • (k) Approval state. The one APPROVED review (os-zhuang, 2026-10-27T15:09:21Z) is at 75285daf, not at this head; mergeable_state is blocked; auto-merge is off (seat comment 6445680015). Tier H approval must be re-given at 994c3279 after the seat's ACCEPT; this record does not substitute for it.
  • (l) Check Changeset skipped by the skip-changeset label on this head. Not a defect of the PR: lint.yml 2894-2906 documents the inversion and moved the self-tests to the Lint lane for exactly this case; that lane ran them and is green. Named so nobody reads the skip as the gate's verdict.

Should-fix, none blocking: ①-1's lookbehind residual (one character plus a U-case); the PR body refresh in ③-(j).

Implemented-by: claude/project-thread-t1c00n
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(席位终稿)

  • 改了什么:ADR-0087 D9 加整包删除条款:已发布的包在 HEAD 被删除时,runtime-export-retired 的操作数写成 包名#*。门禁 check-adr-0087-registration 加对应谓词,自测新增 WP 电池 55 条,ADR 两处封闭词表也补上了第三种写法。
  • 为什么改:执行 PR refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365 评论 6439038287 的裁决 A(维护者「同意」)。被删的包在 HEAD 没有入口可读,@object-ui/plugin-ai 的下线(refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365)在这个门禁上会一直红。
  • 风险与回滚:只放行整包删除这一种新形态,三个谓词要全部成立。第一,基点上是已发布的工作区包;HEAD 上没有它的 package.json,pnpm-workspace.yaml 不再列它的目录,它也不在 changesets 的 fixed 组。第二,包外的元数据面在代码里不提它的名字,导入和字符串值都算。第三,FROM 行用一行写包名。其他判定不变,原有 627 条自测照样全绿。回滚即 revert 本 PR。
  • 先看:docs/adr/0087-metadata-protocol-upgrade-contract.md 里 D9 下面的第二条 2026-10-27 修订说明;scripts/check-adr-0087-registration.mjs 的 verifyWholePackageRetired 和谓词 2 的整包分支(packageReference)。
  • 两分钟验证:node scripts/check-adr-0087-registration.mjs --self-test,预期以 0 退出并打印 682 assertions。
  • 需要你做的:请 @hotlong 或 @os-zhuang 在 head 994c3279 上批准。os-zhuang 之前的批准在 75285daf 上,早于契约复核,所以请在本记录之后重新批准。批准后席位挂自动合并入队。

席位意见:契约复核第一轮 FAIL(谓词 2 被收窄为"只看导入"、封闭词表未补),两项必改均在 994c3279 修好,复审 PASS;席位核对的 diff 与复审范围一致,同意落地。

席位核对

ACCEPT · seat domain:ui#1 (holding #26220 as #26365's blocker) · session_01JQwxbZeBv2y91towxasSUT · head 994c3279 · 2026-10-28T01:22Z

  1. Shape: Fixes #26220; 2 files, +335/−14, merge base dc9d1ff8: docs/adr/0087-metadata-protocol-upgrade-contract.md (Status-line entry, the closed-vocabulary lines in the 2026-08-13 and 2026-10-12 addenda, and the second 2026-10-27 note under D9) and scripts/check-adr-0087-registration.mjs. Labelled skip-changeset, because nothing published moves.
  2. Premise measured: the seat read the ADR diff on the head. The note states the three predicates for <package>#* as the ruling and D9 require. Predicate 2 now reads "names it in code (comment spans masked …), a string value included", which is must-fix 1. Both code blocks carry the third marker line, with "three operand kinds", which is must-fix 2. Both should-fix wordings are in: "a single line, not one per export" and "moved, its directory renamed". For the gate side the seat relies on the director's line-by-line read in both contract records (6457699551 FAIL on 75285daf, 6458009682 PASS on 994c3279), which is the governing review for a Tier H surface.
  3. Gates / check-runs: PR check-runs on 994c3279: 28 success, 17 skipped, 0 failure, none pending at the seat read (Test Core completed after the PASS record). Check Changeset is skipped by the label, and the gate's --self-test ran green in Lint · docs & code guards, as the PASS record notes. Dev's second report (6458079201 on check-adr-0087-registration: PACKAGE_ROOTS omits ui/packages, ui/apps and ui/examples, so no @object-ui package can use the runtime-export-retired disposition #26220): self-test 682 assertions exit 0, 44/44 derived gates exit 0, and the refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365 probe still exits 0.
  4. Accepted deviations: the director's should-fix ①-1 (the package-mention lookbehind also excludes a /-glued, path- or URL-embedded occurrence) does not ride this PR, so the PASS stays pinned to its head. It goes to a follow-up card after the merge. The PR body's stale lines (③-(j)) were corrected by a seat-applied amendment (13 replacements, read-back identical).
  5. Out of scope: refactor(ui)!: retire @object-ui/plugin-ai and its three node types #26365 then needs its two changeset edits: the marker becomes @object-ui/plugin-ai#*, and its three per-export FROM lines become one package line. They are owed there, as the ruling assigns. Skills-docs: none — ADR + repo gate.
  6. Landing: Tier H (docs/adr/**). This record is pinned before any ready or enqueue. The PR carries needs-user-decision, and a re-review is requested from both GOVERNED_APPROVERS. ⛔ The seat does not arm auto-merge on the pre-review approval. After a governed approval given after this record, the seat readies the PR and arms auto-merge.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot requested a review from os-zhuang October 28, 2026 01:23
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 28, 2026
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 skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants