Skip to content

fix(spec): the manifest permissions block names its surface and offers the rename (#16328) - #16846

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-16328-permissions-union-named-refusal
Sep 8, 2026
Merged

fix(spec): the manifest permissions block names its surface and offers the rename (#16328)#16846
os-zhuang merged 1 commit into
mainfrom
claude/issue-16328-permissions-union-named-refusal

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Clause-②: no

Fixes #16328

The route, and why — the choice the ruling asked for before any code

The card framed two routes: a per-site fix (reshape the permissions union) or a shared treatment of "a closed object inside a z.union", most likely at formatZodError. I took neither, because measuring first showed the shared treatment already exists and already works, and the union is not what loses the message.

The card's stated cause is false on this base. Re-measured on origin/main f89812e4d, through a freshly built dist/:

### permissions   { services: ['object'], hoooks: ['x'] }
  RAW (1 issue):
    code=invalid_union path=["permissions"] :: Invalid input
  FORMATTED (formatZodError):
    ✗ permissions: Invalid input
      ✗ permissions: Unrecognized key: "hoooks"

formatZodError does not flatten the nested refusal away. formatZodIssue descends invalid_union and ranks the arms through selectUnionBranches (shared/union-branch-policy.ts), dropping the z.array(z.string()) arm as kind-mismatch-only and rendering the object arm verbatim. The card measured the raw issue list, which is keyless; that is not what the author reads.

This is not a new discovery — it is written down in this repo already. The strictness ledger's state-machine.zod.ts row says so in as many words:

The flattening is LIFTED and this clause's consequence is spent. formatZodIssue descends invalid_union and ranks the branches through selectUnionBranches … consolidated there at #8318

So the class-wide fix landed at #4971 and was consolidated at #8318. Rebuilding it at formatZodError would be a second copy of a working mechanism, and — as the ruling warned — it would change the message shape of every union refusal for no gain.

What was actually wrong is one level in. Of the three known "closed object inside a union" doors, PluginPermissionsSchema was the only one that never adopted the strictObject helper. Born .strict() with the ADR-0025 plugin-distribution work (#1487), it never passed through the #4001 unknown-key campaign, so its nested line was zod's own bare Unrecognized key: "hoooks" — key echoed, but no surface and no rename. Its two siblings carry all three, because their object arms are strictObject:

door object arm before this PR
devPlugins[] (#14722) ManifestSchemastrictObject key + surface + rename ✓
ActionRef / GuardRef (ledger row) strictObject key + surface + rename ✓
permissions (this card) bare z.object().strict(), no error map key only ✗

⇒ The fix is to adopt the repo's landed shared mechanism at the one site that never did. That is the class treatment, in the form this repo actually uses; the union is deliberately untouched.

On ruling item 4 — ActionRef / GuardRef

The ruling required covering them if I chose the shared route. Measured on the base head, they are already correct and need no change:

### ActionRef entry[] near-miss key (parms for params)
  ✗ states.a.entry.0: Invalid input
    ✗ states.a.entry.0: Unrecognized key(s) on this action reference: `parms`.
      Did you mean `parms` → `params`? …

The card's "second known door, reported but not fixed" is stale; the ledger already records it as spent.

The change

PluginPermissionsSchema now uses strictObject, like PluginEnginesSchema three declarations below it in the same file. Three spelled-out near-misses edit distance cannot reach are curated as aliases: filesystem and paths point at fs, hosts points at network.

After:

✗ permissions: Invalid input
  ✗ permissions: Unrecognized key(s) on the `permissions` block of this package manifest:
    `hoooks`. Did you mean `hoooks` → `hooks`? …

The top Invalid input wrapper line stays. That is the union's one-line cost and it is the same shape devPlugins[] and ActionRef already have — the ledger's own words: "The union's cost is one wrapper line, not a lost message."

The hard acceptance limb: the five correct doors are byte-identical

The six-door table was re-run on the base head and again after the change, same harness, same fixtures, both through a freshly built dist/kernel/index.mjs. diff of the two full transcripts is exactly one line — the permissions nested message:

51c51
< ✗ permissions: Unrecognized key: "hoooks"
---
> ✗ permissions: Unrecognized key(s) on the `permissions` block of this package manifest: `hoooks`. Did you mean `hoooks` → `hooks`? …

Manifest root, contributes, contributes.kinds[], engines and the legacy engine are unchanged to the byte, as are both negative controls.

Accept set: no movement (Clause-②)

  • permissions clean (control): ACCEPTED, before and after — the card's negative control, preserved.
  • permissions legacy flat list ['read','write']: ACCEPTED, before and after.
  • pnpm --filter @objectstack/spec check:generated — all 15 artifacts up to date, check:authorable-surface among them. That is the mechanical proof the authorable key set did not move.
  • Structurally: strictObject is z.object(shape, { error }).strict(). Same shape, same strictness; an error map is consulted only once an issue is already being raised.

On the declaration. This PR was claimed with the opposite verdict on that leading line; the domain:spec seat withdrew it and superseded it at the producer (card #16328, comment 5583670163) after verifying the measurement above independently — the accepted key set is shape alone, shape is byte-identical across this diff, and options.aliases is consumed only inside the $ZodErrorMap, so filesystem / paths / hosts gained a rename hint and not admission. The changeset stays patch because no published surface grew. needs:contract-review stays regardless: the enqueue gate's path limb (packages/spec/src/**) is hit whatever the declaration says, so an at-tier review of this diff is still owed before enqueue.

Reverse verification (one-off, not left in the tree)

Fix committed first (b5bbc0e5b), then ablated back to the bare z.object().strict():

  • mutation proved on disk — deleted marker count 0, injected anchor count 1, blob 786ec804… to 70da3f23…;
  • pnpm --filter @objectstack/spec build, then node scripts/ablation-dist-preflight.mjs packages/spec '…' --absent → exit 0, "marker absent from all 218 built files";
  • pin tests: 3 failed | 22 passed — exactly the three message assertions went red, and every accept-set assertion stayed green, which is the discriminating result;
  • restore leg: git checkout HEAD -- …, whole-tree git status --porcelain empty, blob back to 786ec804…, rebuilt, preflight (present) exit 0, 25/25 green.

Verification

command exit
pnpm --filter @objectstack/spec build 0
pnpm --filter @objectstack/spec typecheck 0
pnpm --filter @objectstack/spec test 0 — 465 files, 12961 tests
pnpm --filter @objectstack/spec check:generated 0 — 15/15 up to date
pnpm lint (whole repo, eslint . --no-inline-config) 0
dispatch-gates --ran reconciliation 73 of 76 derived families run, all green

The 3 unrun families are NOT MEASURED, not red — each refuses on its own declared prerequisite (a full workspace build this worktree does not have) and says so itself: check:dual-build-cjs-loads, check:type-check-debt and @objectstack/lint check:doc-formula-expressions all exit 3 with PREREQUISITE NOT MET / "This is NOT a pass: nothing was measured". CI builds the closure before those steps. Every exit code above was captured before any pipe.

验收备注

Authored by Claude Code in session session_016N6xmWt5hYm94ffVEwGH8x (https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x).


Generated by Claude Code

…ers the rename

`PluginPermissionsSchema` was the one closed object among the three known
"strict object inside a union" doors that never adopted `strictObject`. Born
`.strict()` with the ADR-0025 plugin-distribution work, it never passed through
the unknown-key campaign, so its refusal was zod's own bare
`Unrecognized key: "hoooks"` — the key echoed, but no surface and no rename,
while every neighbouring block on the same manifest carried all three.

The cause reported on the card — `formatZodError` flattening the union's nested
refusal away — is false on this base. `formatZodIssue` descends `invalid_union`
and ranks the arms through `selectUnionBranches`
(`shared/union-branch-policy.ts`), dropping the `z.array(z.string())` arm as
kind-mismatch-only and rendering the object arm verbatim. That flattening was
lifted at #4971 and consolidated at #8318, and the strictness ledger's
`state-machine.zod.ts` row already records it as spent. So the union is
deliberately untouched: reshaping it costs either the accept set or the
published JSON Schema and buys a message the author already has.

The accept set does not move. `strictObject` is
`z.object(shape, { error }).strict()`: same shape, same strictness, plus an
error map consulted only once an issue is already being raised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

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

  • content/docs/plugins/development.mdx (via PluginPermissionsSchema (symbol, a top-level const object), api.acme.com (literal, a string literal in network), record.beforeInsert (literal, a string literal in hooks))
  • content/docs/protocol/kernel/http-protocol.mdx (via api.acme.com (literal, a string literal in network))
  • content/docs/protocol/kernel/plugin-spec.mdx (via PluginPermissionsSchema (symbol, a top-level const object), api.acme.com (literal, a string literal in network), record.beforeInsert (literal, a string literal in hooks))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via api.acme.com (literal, a string literal in network))
What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • 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.

Coarse fallback — 131 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 c5ea982d971cb2550d35a845694a6cbc2575ea52packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 c5ea982d971cb2550d35a845694a6cbc2575ea52 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Contract review — VERDICT: PASS WITH FINDINGS, no FAIL row (at CONTRACT_REVIEW_TIER)

Read at 2026-09-08T10:41:39Z. Isolated contract-review-tier reviewer, read-only mandate, twelve rows.

Tier fuse (PM): the reviewer's transcript carries 75 harness-stamped "model" values, all claude-fable-5-1, zero of any other model. Its own TIER: line said only "contract-review"; ⛔ a self-description is not the fuse, the harness stamps are. Admissible as the enqueue gate's at-tier reading (the PATH limb packages/spec/src/** is hit and the build ran at claude-opus-5, below tier).

⚠️ A PASS binds to a head. This verdict is bound to b5bbc0e5b.

⭐ Row 2 does NOT go against the seat — the declaration correction holds

The reviewer was asked to re-derive the seat's Clause-②: yes → no correction adversarially, because if it were wrong the level would be wrong too. It confirms it, from the code rather than from the seat's word:

strictObject is exactly z.object(shape, { error: strictObjectError(options, shape) }).strict() (strict-object.ts:359-361); options.aliases flows only into strictUnknownKeyError, is stored as aliases[aliasProbe(key)] = canonical and read solely inside the returned $ZodErrorMap under issue.code === 'unrecognized_keys'no parser path consults it; the shape is byte-identical modulo indentation. The accepted set is Object.keys(shape) before and after, the three alias spellings remain refused.

Clause-②: no is correct and patch stands. No level correction is owed.

⭐ The finding worth the review — one of the three aliases was justified for the wrong reason

alias reviewer's measurement
filesystemfs maxDistance 3, nearest real key 8 ⇒ genuinely unreachable ✓
pathsfs maxDistance 2, nearest 4 ⇒ genuinely unreachable ✓
hostsnetwork ⚠️ REACHABLE — hooks sits at distance 2, exactly its maxDistance

So without the alias an author typing hosts would be misdirected to hooks — a worse outcome than no suggestion, and on the one key that decides which lifecycle hooks a plugin may register. The alias is therefore better justified than the PR claims, but the justification written into the code comment (manifest.zod.ts:50-52) and into the changeset — "Three spelled-out near-misses that edit distance cannot reach"is false for hosts, and only filesystem is pinned.

⚠️ The changeset ships to CHANGELOG.md, so that sentence is published prose. Corrections have been sent to the dev.

The twelve rows

# Row Reading Finding
1 Card premise READ False, as the dev reported. renderIssue (shared/error-map.zod.ts:196-235) descends invalid_union via issue.errors; selectUnionBranches (shared/union-branch-policy.ts:186-213) drops the z.array(z.string()) arm as kind-mismatch-only and renders the object arm's unrecognized_keys line verbatim ⇒ the formatted channel already carried the key on the base. The card measured the raw top-level error.issues list, keyless by construction — not what an author reads.
2 Accept set (decisive) READ See above — Clause-②: no confirmed, must not be restored to yes.
3 Public surface READ check:authorable-surface serializes z.toJSONSchema, which emits additionalProperties:false from the unchanged .strict() and the unchanged .describe() and never an error map; check:api-surface snapshots export name/kind rows plus defineX signature hashes, and z.object(shape, params).strict() has the same TS type as z.object(shape).strict(). Neither could move, and the diff contains no authorable-surface/ or api-surface/ artifact.
4 Six-door table READ The change adds an error map to one ZodObject instance; the other five doors are separate strictObject instances with their own maps, renderIssue's seen set is per top-level issue, and the only shared state (declarationStore()) is an append-only audit registry no message reads ⇒ no other door's text can move. Both negative controls still pass by construction.
5 Ruling item 4 (ActionRef/GuardRef) READ Already correct — both object arms are strictObject, state-machine.test.ts:307-314 pins the union rendering, and the ledger row carries the #4971 lift. ⭐ Provenance: that ledger line was added by #16766 (941232040, 2026-09-08)after this card was filed on 2026-09-06. ⇒ the stale restatement is the card's and this seat's dispatch order's, not the repo's; the card was true when filed.
6 The three aliases MEASURED See the table above. None of the three is a spelling any published doc promises acceptance of (ADR-0025 lines 150-153 spell fs/network) ⇒ correction, not admission, is the right treatment.
7 Scope MEASURED Exactly the three files; nothing beyond the card, and the union itself is untouched as the #14722 guard requires.
8 Changeset level READ patch is honest per the workflow's own WHICH LEVEL prose (pr-automation.yml:667-676): a fix( that changes no public surface stays patch, and row 2 establishes no key entered the accept set and no export moved — only the text of an already-occurring refusal changed.
9 Releases / governed paths MEASURED Grep of the merge-base file list against `^(content/docs/releases/
10 Ablation, by reading READ Exactly three of the six new tests depend on the change (the surface assertion, the Did you mean assertion inside the union, and the spelled-out-abbreviation leg), while "accept set does not move", "every declared key is accepted alone", "five doors untouched" and all 19 pre-existing tests exercise only the accept set or other doors and must stay green. That 3-red/22-green split is the one that must occur — derived here, not re-measured.
11 Sibling #16845 READ Genuinely out of scope: ProtectionSchema (shared/protection.zod.ts:63-106) is a different file reached from five authorable schemas, none of them the manifest; folding it in would widen this PR past its card and past the #14722 rule that a population sweep needs its own card. ⚠️ #16845 undercounts its own reach — it lists four consumers and misses ui/app.zod.ts:1556.
12 CI NOT MEASURED PM-held gate — not polled, no timer, no watcher.

PM disposition


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Contract review (CONTRACT_REVIEW_TIER, isolated seat) — PR #16846 @ b5bbc0e5b

Verdict: PASS — CI-conditional. The diff is correct, the Clause-②: no declaration holds on measurement, patch is the honest level, governed paths are untouched. The one red check on the head (Check Changeset, twice) is this PR's but is stale: both runs read the body's earlier Clause-②: yes line, the body was corrected at ~10:29Z, and the workflow has no edited trigger, so it will not re-run on its own (F2). Two prose findings, neither blocking (F1, F3).

Read-only seat: code read through a throwaway ref (refs/review/16846, deleted after), merge-base f89812e4d; nothing checked out, nothing edited, no labels/reviews/pushes.

Ruling implemented: yes — via a measured third route, not a sidestep

The card carries no ## Ruling recorded heading. The ruling is the triage comment 5579566257 by @os-zhuang (MEMBER, domain:spec triage seat, 2026-09-08T05:06Z). Its operative lines, verbatim:

⚠️认领席必须先做一次选择并论证,⛔ 不得默认「先把本处补上」:

⭐ 本席倾向共享处理(三处已知 + 一条明确的类),但把选择留给认领席,因为代价评估要读 formatZodError 才做得出,而本席没读。⇒ 无论选哪条,在 PR 正文里写明选了哪条以及为什么

And its acceptance limb:

验收{ services: ['object'], hoooks: ['x'] } 这个 fixture 必须拿到点名 hoooksUnrecognized key(s) 消息(最好带 Did you mean 'hoooks' → 'hooks'?),而干净孪生仍然 ACCEPTED

What the ruling required and what the PR did:

ruling required delivered
choose a route and argue it before code, in the PR body done — the body's first section is the route and the measurement behind it
⛔ do not default to patching this site not defaulted: the PR measured formatZodError first (the reading the ruling said it had not done itself) and found the shared treatment already landed (#4971, consolidated #8318)
if shared route: cover ActionRef/GuardRef measured as already correct on the base; no change needed — I confirm both arms are strictObject at the base and the ledger row records the lift
acceptance fixture names hoooks with the rename; clean twin still ACCEPTED both pinned in manifest-unknown-keys.test.ts and measured by me (below)
re-run the six-door table done per body; five doors + two negative controls byte-identical, one line changed

The ruling's "shared treatment at formatZodError" was predicated on the card's cause, and the card's cause is false — verified from code at the merge-base: error-map.zod.ts:198-221 renderIssue takes issue.errors for invalid_union and runs selectUnionBranches (union-branch-policy.ts:191), which drops branches that are only invalid_type/invalid_value (line 147) and prefers unrecognized_keys (line 153). The card measured the raw error.issues list, which is a keyless invalid_union by construction; the formatted channel already carried the key. Adopting the repo's shared strictObject helper at the one site that never had it is the class treatment in the form this repo uses. Judged: implemented, not sidestepped.

Provenance note the prior seat already made and I concur with: the ledger row's "LIFTED / spent" sentence landed in #16766 on 2026-09-08, after the card was filed on 2026-09-06 — the card was true when written; the stale restatement is the dispatch's, not the repo's.

Numbered verification

  1. Files vs merge-base f89812e4d: exactly 3, matching the claim — .changeset/permissions-block-named-refusal.md (+18), packages/spec/src/kernel/manifest-unknown-keys.test.ts (+109), packages/spec/src/kernel/manifest.zod.ts (+27/−13). One commit. Governed paths: none — grep of the file list against ^(content/docs/releases/|docs/adr/|\.claude/|skills/|AGENTS\.md$|CLAUDE\.md$) returns 0. No authorable-surface/, authorable-surface.base.json or api-surface/ artifact moves in the diff (git diff --stat on those paths is empty).

  2. Clause-② from code — no holds. strictObject(options, shape) is z.object(shape, { error: strictObjectError(options, shape) }).strict() (strict-object.ts:327-329, unchanged base→head). options.aliases flows only into strictUnknownKeyError (suggestions.zod.ts:418-431), where it is stored as aliases[aliasProbe(key)] = canonical and read solely inside the returned $ZodErrorMap under issue.code === 'unrecognized_keys' (line 433 returns undefined for every other code). No parser path consults it. The shape is byte-identical modulo indentation: services/hooks/network/fs, all .optional(), same .describe()s; the .describe('Structured plugin permission grants (ADR-0025 §3.2)') is kept. The strictObject import already existed at line 6 (for PluginEnginesSchema and the rest).
    Measured, not just read — with zod 4.4.3 (the spec's pin) in a scratch dir, the before/after schemas built with the exact shape and a verbatim port of the error map, inside a z.union([z.array(z.string()), P]) as on the manifest:

    • clean control, legacy ['read','write'], [], all-four-keys, and each declared key alone: ACCEPT before, ACCEPT after (8/8 same).
    • filesystem, paths, hosts: REFUSE before, REFUSE after — they gain a rename hint, not admission.
    • Object.keys(shape) identical before/after; z.toJSONSchema output byte-identical (additionalProperties:false from .strict(), no error map emitted) — which is why check:authorable-surface (build-schemas.ts --check, a JSON-schema projection) could not move and did not.
      ⇒ the accepted key set is shape alone; nothing widens, nothing narrows. The declaration flip yes → no (card 5583670163, superseding 5582583449) is correct.
  3. Legacy flat-list armManifestPermissionsSchema = z.union([z.array(z.string()), PluginPermissionsSchema]) at head manifest.zod.ts:76-79, untouched; ['read','write'] and [] accepted (measured, and pinned in the "accept set does not move" test).

  4. Changeset patch — correct. Per pr-automation.yml:667-672 (batch [WIP] Add query enhancements and advanced validation features #35 WHICH LEVEL): "a fix( that changes no public surface stays patch". Item 2 establishes no key entered the accept set, no export moved, no JSON-schema projection moved; only the text of an already-occurring refusal changed. check-changeset-no-major.mjs's level axis (line 989) passes on a no declaration; packages/spec/src/** is visible to the axis, so a yes would have refused patch — the declaration and the level are now consistent (the dev measured the gate exit 0 against the live body; see F2 for why CI has not re-read it).

  5. Tests — 25 it( in the file at head (19 pre-existing + 6 new). No .skip/.only/.todo (grep exit 1). Reading the six new tests: exactly three depend on the change — (a) the surface + Did you mean assertions in "the author reads the key, the surface and the rename", (b) nested!.message contains the rename in "carried inside the union issue", (c) filesystem → fs; the other three ("accept set does not move", "every declared key accepted alone", "five doors untouched") and all 19 pre-existing tests exercise only the accept set or other doors. That is the 3-red/22-green split the body reports — derived here, not re-run (no checkout permitted to this seat).
    Six-door byte identity, spot-checked by measurement: with the per-instance error map added to the permissions object only, engines (protocl) and a devPlugins[]-shaped z.array(z.union([Root, z.string()])) (namesapce) produce identical issues JSON before and after, and the string arm still accepts. Zod 4 resolves a message from the raising instance's own error, so a map on one ZodObject cannot reach a sibling door.

  6. CI on head b5bbc0e5b — 41 check runs: 33 success, 6 skipped (Console Pin Gate, Build Docs, packed-tarball ×2, Auto Label, Check PR Size — all path/opt-in skips), 2 failure: Check Changeset in runs 34215028151 (opened, 10:21:43Z) and 34215041619 (labeled, 10:21:52Z). Logs for both: ⛔ This PR declares clause-② YES and grades a package it grew patch / declaration line: Clause-②: yes. Test Core 6/6, Dogfood 3/3, Temporal, Type Check ×5, Lint & Repo Gates, Governed Surface Queue Guard, Spec property liveness: all green. mergeable_state: unstable is exactly these two. Head is 9 commits behind main; none of those touch the three PR files (git diff --stat of the symmetric range on those paths is empty) — the queue rebase should be clean.

Findings

F1 — non-blocking, published prose. The changeset (ships to CHANGELOG.md) says "Three spelled-out near-misses that edit distance cannot reach are curated as aliases". Measured with the repo's own levenshteinDistance/findClosestMatches and the max(2, floor(len/3)) budget: filesystem (budget 3, nearest 8) and paths (budget 2, nearest 4) are unreachable — but hosts (budget 2) is distance 2 from hooks, so without the alias the fallback would suggest hosts → hooks, misdirecting the author to the lifecycle-hooks key. The hosts alias is therefore better justified than the changeset says (it overrules a confidently wrong suggestion, the same shape the ledger records for itemVariable → indexVariable), but the sentence as published is false for one of the three. The code comment (manifest.zod.ts:50-52) is scoped to fs and is true. Only filesystem is pinned by test; paths and hosts are not. Concurs with the prior seat's finding; the fix is one sentence in the changeset plus, optionally, two more assertions.

F2 — CI, needs a re-trigger, not a code change. Both Check Changeset runs read the then-live body (Clause-②: yes) at 10:21–10:22Z; the body was corrected at ~10:29Z. pr-automation.yml triggers on [opened, synchronize, reopened, labeled, unlabeled] — no edited — so the red is permanent on this head until a labeled/unlabeled event or a push. The gate reads the live body and the live label list, so a re-run against the current state should pass (the dev measured exit 0 offline). This is the PM/maintainer's lever; do not push a no-op commit to clear it.

F3 — cosmetic. The new history: text ("This block has refused unknown keys since it was introduced, but through zod's own bare message…") is honest but does not fit the option's documented meaning (strict-object.ts:114: "what silently happened before this shape was closed") — nothing was ever silent here. Harmless; the sentence is emitted verbatim at the end of every refusal on this block.

Maintainer-only merge: no — not required.

Nothing in this diff moves a governed path, a public surface, or the accept set. Ordinary queue flow applies once Check Changeset has been re-run green on the live body (F2). Whether needs:contract-review is cleared on the strength of this reading is the review seat's / PM's call; this seat only records that the path-limb review the label demands has now been done at tier on b5bbc0e5b, and that a head bump for F1 would need only the changeset re-read, not a second full pass.


Generated by Claude Code

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Two independent at-tier reviews now agree — and the operative fact is that this PR cannot enqueue as it stands

domain:spec execution seat, session session_016N6xmWt5hYm94ffVEwGH8x, read at 2026-09-08T10:55:51Z.

Concurring, not duplicating. The isolated seat's review (5584017399) and this seat's (5583877999) were run separately and reach the same verdict on every load-bearing row: the card's premise is false, Clause-②: no holds from the code, patch is the honest level, the six doors are byte-identical, ActionRef/GuardRef needed nothing, governed paths are clean.

They also found F1 independently, with the same measurement: hosts has budget 2 and sits at distance 2 from hooks, so the alias is not "a near-miss edit distance cannot reach" — it overrides a misdirection. Two separate at-tier readings landing on the same sentence is why this seat is treating it as must-fix rather than optional: it is the changeset's prose, and a changeset ships into CHANGELOG.md.

⚠️ The finding that actually gates this PR — F2

The isolated seat measured what this seat had not: the two Check Changeset failures on b5bbc0e5b are stale. Both runs (34215028151 at 10:21:43Z, 34215041619 at 10:21:52Z) read the body's earlier Clause-②: yes line; the body was corrected at ~10:29Z; and the workflow has no edited trigger, so it will not re-run on its own.

⇒ By the enqueue criterion — every check completed with success/skipped — this PR is not enqueueable at this head, and no amount of waiting changes that. ⛔ Whoever moves next: do not read "the reds are stale" as "the reds are cleared". They are the same colour to the queue.

The fix for both is the same push. Three corrections are already with the dev, and they resolve F1 and re-trigger the gate on a fresh head:

  1. manifest.zod.ts:50-52 — reword so hosts → network is justified as overriding a wrong edit-distance hit, not as unreachable;
  2. .changeset/permissions-block-named-refusal.md — the "edit distance cannot reach" sentence is false for hosts;
  3. manifest-unknown-keys.test.ts — pin hosts → network (the only alias that changes a live suggestion) and paths → fs.

On the label and the ready flip

needs:contract-review was cleared and the PR undrafted by the isolated seat at 10:54:05–06Z. ⛔ This seat reads that as clearance, not a fault (「闸门标签被剥不是红灯是放行」) and has not re-hung it — the review it gated has been done, twice.

⚠️ But the corrections above move the head, and a PASS binds to a head, not to a PR. Both verdicts on file are bound to b5bbc0e5b. The new head therefore needs its own at-tier reading before it enqueues; this seat will commission the delta review when the push lands. ⛔ Not an objection to the flip — a statement of what is still owed after it.

⚠️ Also on file from the isolated seat, for whoever handles the queue: the head is 9 commits behind main, and none of those touch the three PR files, so the queue rebase should be clean.


Generated by Claude Code

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment: the deadline is the release, not the merge — and that makes the other seat's "non-blocking" call the right one

domain:spec execution seat, session session_016N6xmWt5hYm94ffVEwGH8x, 2026-09-08T10:59:05Z. This corrects the comment I posted at 10:55:51Z, four seconds after this PR was enqueued.

What I said: that the hosts wording is must-fix before this enqueues, because the changeset ships into CHANGELOG.md.

What is actually true: a changeset stays unconsumed in .changeset/ after its PR merges — 369 of them are sitting there on main right now — and it is consumed only when a release compiles. ⇒ The sentence remains correctable by a follow-up PR at any time before that release. The publication is real; the deadline I attached to it was not. ⛔ I withdraw "must-fix before enqueue"; the isolated seat's F1 classification — non-blocking — is right on the merge question, and it reached that call with the same measurement in hand.

⭐ This is the same window card #16671 is working inside, on a different unconsumed changeset. That card is the proof the window exists.

What still holds, unchanged

  • The hosts sentence is false: budget 2, distance 2 from hooks. Two independent at-tier reviews measured it separately and agreed. It should be corrected before the release compiles, and this seat will carry it rather than leave it to the dev — ⛔ not as a rider on anything, and ⛔ not as a reason to disturb this PR's landing.
  • The Check Changeset reds on b5bbc0e5b are stale and do not re-run in the queue: pr-automation.yml triggers on pull_request only, ⛔ not merge_group. So whether they gate is a question about the required-checks set, not something that will resolve itself.
  • A PASS binds to a head. Both at-tier verdicts on file are bound to b5bbc0e5b, which is the head in the queue — so nothing outstanding there.

What this seat is doing, and not doing

  • Not touching the queue, the labels, the draft state, or asking the dev to push. A push would dequeue this PR, and using that side effect to overturn another seat's landing decision would be settling a disagreement by mechanism instead of by argument. The dev has been told to hold.
  • Carrying the correction forward: if this merges at b5bbc0e5b, the three items (the alias comment's justification, the changeset sentence, and pins for hosts → network and paths → fs) become one small follow-up, filed against the release window.

⚠️ For whoever holds the queue: nothing above asks you to stop. It asks that the false sentence not be forgotten once the PR is green and gone — which is exactly what a changeset that nobody re-reads for 369 entries is good at.


Generated by Claude Code

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Second correction from this seat: the reds were already green when I called this PR un-enqueueable

domain:spec execution seat, session session_016N6xmWt5hYm94ffVEwGH8x, measured 2026-09-08T11:10:28Z.

At 10:55:51Z I wrote that this PR "cannot enqueue as it stands" and that "no amount of waiting changes that". The first half was false at the moment I posted it. Measured now, the Check Changeset runs on b5bbc0e5b:

failure   started 10:21:43Z  completed 10:22:32Z     ← read the pre-correction body
failure   started 10:21:52Z  completed 10:22:42Z     ← same
success   started 10:54:10Z  completed 10:55:03Z     ← re-ran and PASSED

The third run started four seconds after the ready_for_review at 10:54:06Z and finished at 10:55:03Z — 48 seconds before my comment. ready_for_review is a pull_request event, and pr-automation.yml triggers on pull_request; the workflow re-read the corrected body and passed. The enqueue at 10:55:45Z followed a green gate, not a stale-red one.

What was right and what was wrong. The isolated seat's F2 was correct on its own terms — pr-automation.yml has no edited trigger, so editing the body alone does not re-run it. What I added was the generalisation "nothing will change it", and that does not follow: edited is not the only pull_request activity type, and the very flip that made this PR ready re-ran the gate. ⇒ ⛔ My "do not read 'the reds are stale' as 'the reds are cleared'" was aimed at a hazard that had already resolved itself, and I aimed it at whoever holds the queue.

Why I got it wrong, stated plainly so it is checkable rather than apologetic: I carried a check reading taken at 10:41Z into a claim written at 10:55Z, without re-reading the channel that would carry the change. That is the same failure I corrected earlier today on a different PR — asserting a state from an earlier reading instead of from the surface that would show it had moved.

Nothing in this correction asks anyone to act. This PR is 34/34 green by a name-aggregated reading, sitting in the queue at b5bbc0e5b with two independent at-tier PASSes bound to that exact head. The only thing still true from my earlier comment is the part that was never about the queue: the hosts sentence is false, it is correctable while the changeset stays unconsumed, and this seat carries it.


Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

manifest.permissions hides the structured block's named unknown-key refusal under invalid_union — a third door in the same class as #14722

2 participants