Skip to content

fix(spec): answer an axis-silent key with both ends of the range, not the cheaper-spelled one - #19318

Merged
os-steve merged 8 commits into
mainfrom
claude/issue-18572-suggester-opposite-sibling
Sep 20, 2026
Merged

os-steve merged 8 commits into
mainfrom
claude/issue-18572-suggester-opposite-sibling

Conversation

@os-steve

@os-steve os-steve commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18572

Clause-②: no

The card body's mechanism is retracted by its own filer (comment 5707586601) and the grading that recited it is superseded by comment 5747478673. There is no alias table; options (i) and (ii) on the body are void. This PR implements the re-graded, suggester-side scope, and every number below was re-derived here rather than carried from the card.

What is actually wrong

findClosestMatches ranks by edit distance and nothing else. On a shape that declares both ends of a range, a key that names neither end is therefore answered with whichever end happens to be spelled more cheaply:

authored `dateField`  (9 chars, budget max(2, 9/3) = 3)
  -> `endDateField`     distance 3   INSIDE the budget   [ANSWERED]
  -> `startDateField`   distance 5   outside the budget  [UNREACHABLE]

end is a three-letter token and start a five-letter one. That spelling accident is the entire reason the protocol told an author to bind the end of the event. Nobody declared the mapping.

And the suggested key is a declared key the runtime honours, so an author who copied the remedy got a document that parses, with the calendar axis silently on the wrong date. Every objectui read site folds dateField onto startDateField. The trap punishes the reader who did what the protocol said.

The change

One screen, on the guess only. When the candidate the distance fallback would name carries an axis token the authored key does not, and the shape also declares that candidate's opposite-pole sibling, the rename is replaced by a prescription naming both ends:

Unrecognized key(s) on this calendar configuration: `dateField`.
  • `dateField` does not say which end of the range it binds, and this surface declares
    both `startDateField` and `endDateField` — opposite ends of one axis. Write the one
    you mean: both parse, so guessing binds the wrong end silently.
Until these shapes were closed an unknown key was dropped silently — …
  • No alias was declared and the accepted key set does not move. dateField was refused before and is refused after; only the sentence the refusal carries changed. This is the fork the card's stop condition names, and it is not taken — no dateField → startDateField entry exists anywhere in this diff.
  • A declared aliases entry is never screened. A human statement about one spelling outranks the guard; only a coin flip is replaced. this field keeps answering length with maxLength exactly as it declares it.
  • Naming both ends is this repo's own answer, generalised. field.zod.ts already writes it by hand for visible: 「the two answers have opposite polarity … Naming both is the only answer that cannot be acted on wrongly」. What a hand-written entry cannot do is cover the keys nobody enumerated — which is the set a fuzzy suggester answers.

Omission vs typo — the condition that keeps it narrow

The guard fires only when the authored key is at least as close to the candidate minus its axis token as to the candidate itself. Without that condition, axLength — one dropped character in maxLength, with minLength declared beside it — would lose a perfectly good suggestion. Measured:

authored to the candidate to the candidate minus its axis token reads as verdict
axLength vs maxLength 1 2 (length) a typo rename kept
dateField vs endDateField 3 0 (datefield) the axis-silent key both ends named

Census — four instances, not one

Measured over 389 registered strictObject surfaces (the audit's own dedup key — surface + alias table + sorted shape keys; the same walk also reads 421 raw registrations and 388 distinct surface strings), by deriving each declared key's axis-silent spelling and asking the real error map what it answers. Four fuzzy instances exist and all four are fixed here:

surface authored answered before now
this calendar configuration dateField endDateField both ends named
this timeline configuration dateField endDateField both ends named
this gantt configuration dateField endDateField both ends named
this gantt configuration baselineField baselineEndField both ends named

Timeline and Gantt are covered, as the dispatch asked. The gantt baselineStartField / baselineEndField row was found by the census, not by the card.

A fifth row the census surfaced is not a defect and is deliberately untouched: this field answers length with maxLength against a declared minLength, and that is a declared alias sitting beside size: 'maxLength'. It is a decision, so the guard leaves it exactly as written — which is also the precedence pin in the tests.

The axis table is judged, not just declared

POLARITY_AXES has four rows, each attested by a real sibling pair in this package. alias-integrity.test.ts now fails on a row no surface declares both ends of — the same dead-entry judgement it already applies to aliases and guidance — with a lit control so an empty verdict is a reading rather than a walk that matched nothing. It is deliberately not a general antonym dictionary.

Evidence

Bright control (the premise, re-measured every run). The arithmetic is never written down as 3 and 5: the test recomputes the distances and asserts the relation — the wrong end inside the budget, the right end outside it — so a rename, a fold change or a budget change reds and names the measurement. A second leg pins that the unguarded ranking still produces endDateField on all three surfaces, so the main leg cannot pass for a reason unrelated to the guard.

Dark controls. endField (out of budget at 8 chars, exactly as the filer's control table observed without knowing why) and a nonsense key are asserted byte-identical to each other with only the key name differing — pinning the refusal text, not merely the absence of a hint.

Ablation. Guard removed via scripts/ablation-replace.mjs (anchor hit 1 to 0, blob 2fdc252271c2 to f1228fa36275), tests re-run, restored with blob == HEAD and git diff HEAD empty:

Tests  5 failed | 15 passed (20)
  FAIL  main > this calendar configuration: `dateField` names both ends
  FAIL  main > this gantt configuration: `dateField` names both ends
  FAIL  main > this timeline configuration: `dateField` names both ends
  FAIL  main > gantt: the second attested pair on the same surface is covered too
  FAIL  the guard fires on omission and NOT on a typo > suppresses the rename …

The bright, dark, arithmetic and pure-predicate legs stay green under the ablation — they measure different things, and the dark controls really are dark. The new audit was ablated too: adding an unattested axis row reds it by name (expected [ 'zzleftward/zzrightward' ] to deeply equal []).

The declaration-shard diff is controlled, not assumed. Adding a module to the import graph reshuffles TypeScript's declaration-emit order for enum members — 19 declarations across four shards, and the whole diff is three lines (read, edit, update) changing position. With polarity-axes.ts out of the graph and suggestions.zod.ts restored to the merge base, a fresh build reports declaration text unchanged (17 entry points, 5364 declarations). The reshuffle is this branch's, so the artifact is regenerated here.

Verification

leg result
pnpm --filter @objectstack/spec test 501 files / 14664 tests passed
pnpm --filter @objectstack/spec typecheck exit 0 (src, scripts and the test layer)
pnpm --filter @objectstack/spec build exit 0
pnpm --filter @objectstack/spec check:generated all 16 generated artifacts up to date
derived gate families (scripts/pm/dispatch-gates.mjs) 83 derived, 81 run green, 0 unrun
pnpm lint (repo-wide, eslint . --no-inline-config) exit 0, clean, at 9c2725b69d

Two of the 83 are NOT MEASURED, both refusing their own prerequisite with exit 3 rather than reporting a verdict — check:dual-build-cjs-loads (83 packages with no dist/) and check:type-check-debt (27 workspace dependencies unbuilt). Both need a whole-repo build, which is Build Core's output in CI. ⛔ Neither is a pass and neither is a finding. Reconciliation: 83 derived famil(ies) accounted for — 81 run, 2 NOT-MEASURED.

Acceptance notes

  • packages/lint has four direct findClosestMatches call sites that this guard does not reach, because it lives in strictUnknownKeyError and those sites call the ranker directly, over user data field names, with the flat default budget of 3 rather than the length-relative one. Measured against {start_date, end_date, min_amount, max_amount, name}: the_date still resolves to end_date, while amount and date do not reach the trap (a snake_case name charges the separator too). Same mechanism, second read site, narrow but non-zero reachability. Not fixed here: it adds packages/lint to the affected set and a verification surface this card does not own. Filed for triage rather than silently carried.
  • data/object.zod.ts's suggestKey is a second suggester with the same length-relative budget. Measured: ObjectSchema declares 43 top-level keys and zero polarity sibling pairs, so the trap is unreachable there today and nothing guards it if a range pair is ever added. noted, not filed — 承接者: the next card that adds a range pair to ObjectSchema.
  • this field's length: 'maxLength' beside a declared minLength reads as an axis collision but is a declared alias, deliberately placed next to size: 'maxLength'. noted, not filed — 承接者:无, it is a declaration rather than a defect, and the guard's precedence is pinned so it stays one.
  • packages/spec/src/ui/view.zod.ts was not touched — a concurrent card ([finding] FormFieldBaseSchema.scale(view.zod.ts:2768)是第三处无上界的 scale —— 撞同一个渲染面天花板,而 #18972 只覆盖了 field.zod.ts 的两处 #19088) holds it. The calendar, timeline and gantt shapes are read by the tests, never edited.

Seat correction, 2026-09-20T13:10Z — the surface count in this body was wrong and is fixed above. It read 136; re-derived with alias-integrity.test.ts's own instrument at head 72bf22a2e1, the reading is 389. The 136 came from a one-off census script written for this card whose forcing walk was weaker in four ways: it imported only *.zod.ts plus index.ts (225 of 1012 modules), returned early on function-valued schemas, capped the walk at depth 12 instead of 40, and never invoked a deferred error map — so every surface that registers on first use never registered. Re-running that old script at this head still prints 136, so the figure is reproducible from the wrong instrument and from nothing else.

⛔ The conclusion is unchanged: four fuzzy instances, all guarded; one declared-alias row (lengthmaxLength) left alone. The larger population surfaced one row the undercount had hidden — on this object-timeline, an authored date draws data, a declared key one edit away and not a pole, so the guard is correctly silent — and it is not this card's class. The attestation table gained the pairs the undercount hid (min/max is 11 pairs, not 3); all four axes stay attested.

The docblock and the changeset were corrected by the implementer on 72bf22a2e1. This body is the seat's to edit — the dev's contract writes a PR body once and ⛔ never PATCHes it, and it flagged that conflict rather than quietly choosing a side, which was the right call.


Generated by Claude Code

… a range

`findClosestMatches` ranks by edit distance alone, so on a shape declaring both
ends of a range an axis-silent key is answered with whichever end is spelled
more cheaply. `dateField` lands on `endDateField` (distance 3, inside a 9-char
key's budget of 3) while `startDateField` sits at 5, structurally unreachable —
and the suggested key PARSES, so an author who follows the protocol's own
correction binds the wrong end of the event and is told nothing.

Screen the fallback's answer: when the candidate carries an axis token the
authored key does not and the shape also declares its opposite-pole sibling,
replace the rename with a prescription naming BOTH ends. Declared `aliases`
entries are never screened. The accepted key set does not move.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
…s about

An axis row no shape declares both ends of can never match, and a row nothing
can match reads as coverage of a trap this protocol does not have — the same
dead-entry shape `alias-integrity.test.ts` already refuses for `aliases` and
`guidance`. Judge it in the same walk, with a lit control so an empty verdict
is a reading rather than a search that matched nothing.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
Clause-②: no — the accepted key set does not move; only the refusal's
suggestion text does.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
Adding a module to the import graph reshuffles TypeScript's declaration-emit
order for enum members: 19 declarations across four shards move `read`, `edit`
or `update` to a different position. Those three lines are the WHOLE diff —
nothing is added, removed or retyped, and a structural object type does not
depend on member order.

Controlled, not assumed: with `polarity-axes.ts` removed from the graph and
`suggestions.zod.ts` restored to the merge base, a fresh build reports
"declaration text unchanged (17 entry points, 5364 declarations)". The
reshuffle is this branch's, so the artifact is regenerated here rather than
left for the next card to find.

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

github-actions Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

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

What this run could not see
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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 — 136 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 c334ba0f3a6dc2a107ffca06bc28fb27775cba28packageMentionDocs.

Which tree this was computed on

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

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

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

os-steve commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 75/75 CONTRACT_REVIEW_TIER
Head-sha: 9c2725b69d0afb4d04fd7ceab359236f941cbc72

Served-tier reading: the reviewer's own subagent transcript file, per-request harness model stamp, read at posting time — 75 stamped requests, 75 at the constant's value, 0 at any other value. The identifier stays in the transcript (AGENTS.md lets none land in a comment). The dispatching session's transcript is a different file at a different tier, which is why this review is isolated.

Sources read: card #18572 body (retracted mechanism), retraction 5707586601, re-grading 5747478673 (the live scope and the stop condition), os-dev-report 5749401816; PR body, 9 file rows, 6 commits, 35 check runs at head; the tree at the head and at the merge base e3b3cdd2df (separate worktree). Nothing was inherited from the dispatching seat.

① Derived judgments

  1. Stop condition — NOT reached, verified independently. The diff adds no aliases entry anywhere; the only aliases: line it adds is on a synthetic surface inside opposite-pole-suggestion.test.ts. packages/spec/src/ui/view.zod.ts is not in the file list. Accepted key set unmoved: measured at the merge base and at the head, CalendarConfigSchema, TimelineConfigSchema and GanttConfigSchema each REFUSE a probe carrying dateField with unrecognized_keys, and each ACCEPT the canonical startDateField/endDateField document. Before: the refusal carried Did you mean dateField → endDateField? on all three; after: it names both ends. Only the sentence moved. Dev's judgment RIGHT.
  2. Declared aliases are not swallowed. suggestions.zod.ts computes declared first and calls the ranker only when no alias matched (guessed = declared ? undefined : findClosestMatches(...)); the screen is applied to guessed alone. Measured at head: a synthetic surface with aliases: { dateField: 'startDateField' } and both poles declared answers Did you mean dateField → startDateField?; the real this field answers length with Did you mean length → maxLength? at base and at head, unchanged. No contract regression. RIGHT.
  3. Publication — no new published export; Clause-②: no stands on the mechanical floor. files[] ships src/**/*.zod.ts only; npm pack --dry-run at head lists 2042 files, zero non-.zod.ts sources, zero paths matching polarity or alias-probe. exports has 19 fixed keys and no subpath pattern; shared/index.ts does not re-export the module; tsup bundles per entry, so its code is inlined into dist/shared/index.* unexported, exactly as the precedent alias-probe.ts already is. oppositePoleAmbiguity, oppositePolePrescription and POLARITY_AXES are absent from all 17 api-surface-declarations/*.txt and api-surface/*.json at head, and Lint & Repo Gates (export-origins and authorable-surface gates) is green at head. check-widening-tells --declaration no over the PR's file rows: no tell (2 judged, 7 NOT MEASURED by design). RIGHT.
  4. Arithmetic — recomputed, never the literals. The arithmetic leg computes both distances with the live levenshteinDistance and asserts the relation (wrong end within budget, right end outside it, gap equal to len('start') minus len('end')); my own reading at head: budget 3, to endDateField 3, to startDateField 5. Precision note: the BUDGET is a local transcription in the test (budget(), since the live one is a non-exported inline in strictUnknownKeyError), so changing the live formula does not red the arithmetic leg itself. Ablated at head: widening the live budget to floor(len/2) reds the three dark-control legs (endField gains a rename); narrowing it to floor(len/4) reds the three main legs and two synthetic legs. A budget change DOES red the suite in both directions, but it is the surface legs that name it, not the arithmetic leg. Tree restored clean after each ablation. The PR body's "reds and names the measurement" is slightly overstated for the budget; RIGHT in substance.
  5. Census — the fuzzy result is reproduced exactly; the population figure is not. With the same forcing-walk instrument as alias-integrity.test.ts, over every registered surface at head, the pole-carrying keys whose axis-silent spelling the UNGUARDED ranking answers with one pole and no alias covers are: dateField on calendar, timeline and gantt (all to endDateField) and baselineField on gantt (to baselineEndField). Four, as reported. The fifth row is classified correctly: on this field the ranking alone answers length with NOTHING (budget 2, maxlength at distance 3); the maxLength rename comes solely from the declared length: 'maxLength' alias beside size: 'maxLength' — a declaration, left alone by design. The figure "136 registered surfaces" (PR body, changeset, polarity-axes.ts docblock) is NOT reproduced: the walk registers 389 unique surfaces (421 raw declarations; 124 register on import alone; 388 distinct surface strings) — none of these is 136. The conclusion survives because the four-instance result holds over the larger population; the figure is a labeling defect, flagged in ③. Dev's census RIGHT; its headline count unreproduced.
  6. Shard reshuffle — re-derived as a pure reorder. For each of api.txt, kernel.txt, root.txt, security.txt: line count equal at base and head, and the SORTED base and head files are byte-identical (md5 equal). The raw diff is read/edit/update enum members changing position. Nothing added, removed or retyped; no contract movement. RIGHT.
  7. Precedence relative to guidance and guidanceSets is unchanged (their continue precedes the guess). The observable change is refusal text only.

Tests at head: opposite-pole-suggestion.test.ts and alias-integrity.test.ts — 40/40 pass. Check runs at head: 35, every one success or skipped (Console Pin Gate, Build Docs, Packed-tarball smoke skipped), none red.

② Semver level

patch — matches the changeset .changeset/18572-suggester-opposite-pole.md ('@objectstack/spec': patch, Clause-②: no). The accepted key set is unmoved, no export is added, the only observable change is refusal text. --pair 19318 reads Clause-②: no on both limbs (card claim 5748997999 and PR body) and exits 4 on C6 only — no record on this head — which this comment is.

③ Boundary flags

  • packages/lint direct call sites (dev: to file) — leaving it out was RIGHT, and it IS owed a card. Confirmed at head: validate-action-body-writes.ts:444, validate-flow-node-writes.ts:309, validate-hook-body-writes.ts:1005, validate-predicate-path-refs.ts:654 and validate-component-types.ts:161 call findClosestMatches directly at the flat default maxDistance = 3, never through strictUnknownKeyError. Reproduced against {start_date, end_date, min_amount, max_amount, name}: the_date resolves to end_date, and the_amount to min_amount; the shipped predicate WOULD fire on both if wired, so the remedy is portable. Different package, different ranker entry, different verification surface — correctly outside this card. Owed: one finding card with the dev's dedupe words; note for it that date resolving to name (distance 3 on a 4-character key) shows the flat budget's own noise at those sites.
  • suggestKey in data/object.zod.ts (noted, not filed) — agree: zero polarity pairs on ObjectSchema today, unreachable; the stated 承接者 is acceptable. Nothing structural guards it, and the attestation test does not see that suggester.
  • this field length to maxLength (noted, not filed) — agree, and measured (①.5): the rename is purely the declared alias; a decision, not a defect.
  • No governed surface (noted, not filed) — confirmed from the 9 file rows: no docs/adr, .claude, skills, AGENTS.md, CLAUDE.md; view.zod.ts untouched, read by the tests only.
  • open_questions: [] — nothing to answer.
  • Reviewer-raised, non-blocking: (a) the "136 registered surfaces" figure in the polarity-axes.ts docblock, the PR body and the changeset is unreproduced (389 by the test's own instrument); correct it the next time the file is touched — a fresh push would move the head and reopen this review, so it is not asked for now. (b) The arithmetic leg's transcribed budget (①.4) — comment-level precision, no change requested.
  • Record handling: Reviewed-by: is left at the placeholder per the dispatch instruction; the adopting seat replaces it with its own session id before --pair can clear the carriers. Verbatim adoption or whole voiding only.

Implemented-by: claude/issue-18572-suggester-opposite-sibling
Reviewed-by: session_01AmH9bKvGoLjiY86Q4Z3og2

VERDICT: PASS


Generated by Claude Code


Generated by Claude Code

…easured 389

The figure 136 does not reproduce. It came from a one-off census script whose
forcing walk was weaker than the audit's in four ways: it imported only
`*.zod.ts` plus `index.ts` (225 of 1012 modules under `packages/spec/src`),
returned early on function-valued schemas, capped its walk at depth 12 instead
of 40, and never invoked a deferred error map, so surfaces that register on
first use never registered at all. Re-measured with `alias-integrity.test.ts`'s
own instrument copied verbatim: 389 unique surfaces, 421 raw registrations, 388
distinct surface strings. Those are three different facts, so each citation now
says which one it quotes.

The conclusion is unchanged and was re-derived over the larger population: the
same four fuzzy instances, the same single declared-alias row left alone. The
attestation table gains the pairs the undercount had hidden (min/max 11 rather
than 3, input/output and read/write 2 each).

Comment-only in the source: `check:api-surface-declarations` reports
"declaration text unchanged (17 entry points, 5364 declarations)" and
`check:generated` all 16 artifacts up to date.

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

Copy link
Copy Markdown
Collaborator Author

⛔ 达档契约复核暂时起不来 —— 本 PR 在队列外等档,⛔ 不降档、⛔ 不绕

domain:spec seat 4(session_01AmH9bKvGoLjiY86Q4Z3og2),⏱️ 2026-09-20T13:22Z。

事实

本 PR 的 head 在 2026-09-20T13:05Z 前后从 9c2725b69d 移到 72bf22a2e1(那一次提交只改了「136 → 389」这个面数,散文与 changeset,源码为注释级)。⇒ 已在案的 PASS 记录(评论 5749945332)点的是旧 head,check-clause2-carriers --pair 19318 在新 head 上重新欠一条记录(C6)。

本席据此起了一次增量达档复核。它没能开始,平台原因逐字:

You've reached your Fable limit. Switch to another model, or manage usage
credits at claude.ai/settings/usage

CONTRACT_REVIEW_TIER 的值正是该模型(scripts/pm/dispatch-gates.mjs:11903),所以这不是「换个子代理重试」能解决的,是档位本身当前不可用

处置,按章程原文

references/contract-review.md 的〈降档保险丝〉逐字:

读数不达档 ⇒ 改走转录核验达档复核子代理;起不来即无复核,标签原样、队列外等档。

契约复核 ⛔ 不适用额度耗尽豁免降档:豁免对象是派发,复核 ⛔ 不随派发档位免除。

⇒ 本席:⛔ 在别的档位跑这次复核;⛔ 援引「已在旧 head 上 PASS」把新 head 当作已复核;⛔ 动任何标签。本 PR 停在这里等档。

⚠️ 本 PR 另有一处与档位无关的待办,记在这里免得被这条盖过:mergeable_statedirty。解冲突不需要达档档位,但本席先不动——等复核档位回来再一并处理,免得再推一次又把记录点向另一个新 head。

⭐ 这次耗尽的成因,如实记下

本席今日按维护者当日指令「后续优先派需要 fable 的任务」执行,本轮先后起了五次达档复核(#19024#19296#19313#19318#19302),这正是额度耗尽的直接原因。⛔ 这不是对该指令的异议 —— 指令被执行了,代价随之出现,而代价该被看见而不是被绕过。


Generated by Claude Code

…ggester-opposite-sibling

Resolves 4 delete/modify conflicts in packages/spec/api-surface-declarations/
(api.txt, kernel.txt, root.txt, security.txt) by taking main's deletion:
#19024 retired the whole declaration-text snapshot mechanism (script,
package.json scripts, check:generated gate, files) and this branch had only
modified those now-retired files. Verified check:api-surface and
check:generated still pass, and that the branch's new polarity-axes.ts
exports are not part of the public barrel (not re-exported from
shared/index.ts), so no export-recording artifact needs a change.

Co-Authored-By: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4e2406481e0153723dae7fc91293a22d85de34c9

Isolated reviewer, round 3 (the head moved twice since the PASS at 9c2725b69d: the 136 → 389 correction 72bf22a2e1, then the merge of main 1a2bb9ec2d). Sources read first-hand: card #18572 body and all 8 comments (retraction 5707586601, re-grading 5747478673, claim 5748997999, dev report 5749401816, follow-up 5750002563); PR body, all 3 PR comments (the earlier record 5749945332 and the wait notice 5750067960 included), 5 file rows, the check runs at head; the tree at 4e2406481e in my own detached worktree (installed, built) and at its main parent 1a2bb9ec2d in a second one. The earlier PASS was an input judged, not a rule enforced — every reading below was retaken at this head. Exit codes were captured before any pipe.

① Derived judgments

  1. The merge — reconstructed, not read. Driverless probe (git clone --bare --shared, no merge.os-regen.driver set, merge-tree --write-tree 72bf22a2e1 1a2bb9ec2d): exactly four modify/delete conflicts — api-surface-declarations/{api,kernel,root,security}.txt — and no other. The merged tree differs from its main parent in exactly the PR's five files (.changeset/18572-suggester-opposite-pole.md, polarity-axes.ts, suggestions.zod.ts, alias-integrity.test.ts, opposite-pole-suggestion.test.ts); those five are byte-identical between 72bf22a2e1 and 4e2406481e, and main did not touch them between e3b3cdd2df and 1a2bb9ec2d (both git diff --stat empty). So the head is precisely main plus the PR's five files: the whole revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 retirement (17 shard deletions, the generator script, package.json, check-generated.ts, sharded-artifacts.ts) came through, and nothing of the branch survived outside its five files. None of the five carries a merge=os-regen attribute (control: api-surface/root.json and api-surface-signatures.json do), so the merge owed no regeneration deferral. Dev's resolution RIGHT.

  2. The dropped contract delta — nothing was owed, and the instrument that would say otherwise is live. The branch's only change to the four deleted files was a 19-declaration enum reorder (read/edit/update) inside a carrier main retired in 2277d1fcd1. The two carriers that remain: api-surface/*.json (export existence per entry) and api-surface-signatures.json (27 defineX factory hashes over checker.typeToString()). At head, after a fresh pnpm --filter @objectstack/spec build: check:api-surface exit 0 — "public API surface + factory signatures unchanged" — and gen:api-surface rewrites neither path (git status clean, git diff --stat empty). check:generated 15/15 current (the 16th of the dev's count was the retired snapshot). Barrel measured, not reasoned: shared/index.ts has no export line naming ./polarity-axes (anchored regex, exit 1; lit control finds export * from './suggestions.zod' on line 18); src/index.ts re-exports only suggestFieldType, findClosestMatches, formatSuggestion from that module; the new module's only importers are suggestions.zod.ts and the two tests. Ablation on the barrel: appending export * from './polarity-axes'; to shared/index.ts, rebuilding and re-running check:api-surface → exit 1, 0 breaking (removed/narrowed), 4 added, naming OppositePoleAmbiguity, POLARITY_AXES, oppositePoleAmbiguity and oppositePolePrescription on ./shared; barrel restored (blob 16502c491b equals HEAD), worktree clean. So the answer does depend on the barrel, the remaining gate sees the barrel case, and at this head the barrel does not carry it. RIGHT.

  3. 136 → 389 — reproduced, and it is the right kind of count. My own copy of the alias-integrity.test.ts forcing walk at head: 1014/1014 modules imported, 421 raw strictObject registrations, 389 unique by the audit's key (surface + alias table + sorted shape keys), 388 distinct surface strings — the three figures in the docblock, the changeset and the corrected body, to the digit. What the sentence has to count is the population whose unrecognized_keys answer flows through strictUnknownKeyError, because that is where the guard lives. Measured: every in-repo caller is strictObject/strictObjectError (the audit's AST scan refuses any other direct caller); the only other key suggester in the package, object.zod.ts's suggestKey, ranks ObjectSchema's 43 keys, which hold 0 polarity pairs; the global objectStackErrorMap answers unrecognized_keys with a candidate-free "check for typos" sentence; the pre-parse ranker in data/authoring-key-lint.ts (flat budget 3) returned [] on calendar, field, object and stack probes including plain typos — quiet on strict-posture shapes by its own rule, and every trap surface is strict — a zero I state with its radius rather than claim as a clean sweep. 389 unique surfaces is the guard's full reach, so the number is accurate and counts what the sentence claims. RIGHT.

  4. Widening / narrowing — measured over the whole population on both trees. Same instrument at the main parent and at head: for every declared pole-carrying key on every unique surface, derive the axis-silent spelling and ask the real error map. At main, 4 probes were answered with one pole (dateField on calendar, timeline and gantt → endDateField; baselineField on gantt → baselineEndField). At head all 4 carry the both-ends prescription. Every other class is identical on both sides: no-hint 12, rename-to-non-pole 2 (datedata on object-timeline), declared alias 2 (lengthmaxLength on this field). No probe that had a hint lost it; no accepted key moved (dateField refused with unrecognized_keys on both trees; the canonical document parses on both). Concrete before/after on the calendar surface, authored dateField (both taken from the real error map, first line of each):

BEFORE (1a2bb9ec2d): Unrecognized key(s) on this calendar configuration: `dateField`. Did you mean `dateField` → `endDateField`? Until these shapes were closed …
AFTER  (4e2406481e): Unrecognized key(s) on this calendar configuration: `dateField`.
  • `dateField` does not say which end of the range it binds, and this surface declares both `startDateField` and `endDateField` — opposite ends of one axis. Write the one you mean: both parse, so guessing binds the wrong end silently. Until these shapes were closed …

Text only. Not a widening; not a narrowing. RIGHT.

  1. The PR's ablations, re-run here. Guard block removed (anchor hit 1, blob 2fdc252271c29c8c0f5491): Tests 5 failed | 15 passed (20), the five names the body lists; restored with git checkout HEAD --, blob back to 2fdc252271c2, status clean. Unattested row ['zzleftward', 'zzrightward'] added: the attestation leg fails by name (expected [ 'zzleftward/zzrightward' ] to deeply equal []); restored, status clean. At head: opposite-pole-suggestion, alias-integrity, strict-object, suggestions — 4 files / 98 tests pass; typecheck exit 0 (test layer under tsconfig.test.json included); eslint exit 0 on the four TS files; check:exported-any, check:dual-source-exports, check:entry-nameability, check:llms-txt exit 0; check-empty-changeset, check-changeset-no-major, check-adr-0087-registration exit 0 against 1a2bb9ec2d; check-widening-tells --declaration no over the diff: 2 judged, no tell, 3 NOT MEASURED by design.

  2. Two precision findings the PR does not state — non-blocking. (a) Of the four axis rows, only start/end can fire today. At the budget max(2, floor(len/3)) no axis-silent spelling of any min/max, input/output or read/write pair is within reach (lengthmaxlength is 3 against a budget of 2; mapping, scope, rows, zoom, date likewise; bare read/write/min/max are single-token and guard-exempt by design). Measured: guard fires per axis at head = start/end 8 (4 probes × 2 directions), the other three 0. The attestation leg judges "both ends declared", not "the guard can reach it", so the docblock's "a row nothing can match is a row nothing judges" holds only half-way for those three rows. (b) The module's "two readers must agree" rationale is honoured for POLARITY_AXES but not for the tokenizer: keyTokens (unexported) and the test's tokens are two copies of one regex chain; a divergence would let attestation pass on pairs the guard cannot see. Neither moves a contract.

  3. Self-narration checked. The body's census sentence now reads 389 (seat correction applied); docblock and changeset carry the same three readings with their meanings distinguished. The dev's packages/lint count is 4; measured 5 direct findClosestMatches( sites at head (validate-flow-node-writes:309, validate-action-body-writes:444, validate-hook-body-writes:1005, validate-component-types:161, validate-predicate-path-refs:654) — the earlier review's figure — still owed a card, still correctly outside this diff. Check runs at head: 35 check runs, 32 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke — opt-in/skipped by design), 0 failed, 0 in progress at 2026-09-20T15:0xZ; every one of the seven required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core ×6, Dogfood Regression Gate ×3, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) reads completed/success.

② Semver level

patch, and the changeset agrees ('@objectstack/spec': patch, Clause-②: no). No export added, removed or renamed (check:api-surface green; both carriers unmoved under regeneration); no factory signature changed; the accepted key set is identical on both trees; the only observable movement is the sentence a refusal already carried, on four surfaces. strictUnknownKeyError keeps its signature and stays published; an external caller passing knownKeys by hand receives the same screen, which is behaviour inside an existing contract, not a new one.

③ Boundary flags

  • Clause-② on the carriers — no SURVIVES this head. Card claim 5748997999 and the PR body both declare Clause-②: no; check-clause2-carriers --pair 19318 at head reads DECLARED no on both limbs and exits 4 on C6 only (no record naming 4e2406481e) — which this comment is. The declaration is true by measurement: accepted set unmoved, no authorable key acquires a meaning, no alias declared (the only aliases: line the diff adds is on a synthetic test surface), no new public export. The stop condition in 5747478673 is not reached.
  • Governed surfaces: none. Five file rows (GitHub files equals the local diff): no docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md. packages/spec/src/ui/view.zod.ts untouched — read by the tests only.
  • packages/lint direct ranker sites (dev: to file) — 5 at head, confirmed; same mechanism, different ranker entry, different package and verification surface; correctly outside this card; the finding card is the seat's to file, with the dedupe words the dev gave.
  • suggestKey in data/object.zod.ts (noted, not filed) — agree: 43 keys, 0 polarity pairs, measured; the stated 承接者 is acceptable.
  • this field lengthmaxLength (noted, not filed) — agree: a declared alias, identical on both trees.
  • Reviewer-raised, non-blocking: ①.6 (a) and (b). Also data/authoring-key-lint.ts:248 is a third in-package ranker at the flat budget 3 that the acceptance notes do not name; measured quiet on every strict surface (the trap population) by its posture rule; not a defect today; worth one line on the packages/lint card so both flat-budget sites are filed together.
  • open_questions: [] — nothing to answer.

Implemented-by: claude/issue-18572-suggester-opposite-sibling
Reviewed-by: session_01AmH9bKvGoLjiY86Q4Z3og2

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Landing provenance — contract review PASS, landing preconditions measured, 2026-09-20T14:52Z

The at-tier contract review of this PR is on the record as comment 5750520501, judging head 4e2406481e0153723dae7fc91293a22d85de34c9 — the current head — with VERDICT: PASS. Adopted verbatim by this seat; ⛔ not rewritten.

Independence pair, machine-readable on the record:

line value kind
Implemented-by: claude/issue-18572-suggester-opposite-sibling branch — the dev ran mode:subagent
Reviewed-by: session_01AmH9bKvGoLjiY86Q4Z3og2 the adopting seat; the isolated reviewer holds no session

Distinct kinds, so no SELF-REVIEW is reported.

Carriers. Neither this PR nor card #18572 carries needs:contract-review, and the head has not moved since the record was written — so under 「PASS + 无标 + head 未动 = 已清标不是被剥」 there is nothing to strip and nothing was written. Recorded so the absence is legible rather than ambiguous.

The three landing preconditions, each measured first-hand at 2026-09-20T14:52Z

gate reading
① at-tier contract review PASS on record comment 5750520501, Served-tier: CONTRACT_REVIEW_TIER, Head-sha in a span of its own matching this head, all six required lines present once, zero angle brackets
check-clause2-carriers.mjs --pair 19318 exit 0 — both carriers agree, a review of record names this head, no widening tell
③ every check green or an expected skip check-expected-skips.mjs --pr 19318 exit 0: 35 check-runs, 32 success · 3 skipped · 0 other, and all three skips in the roster — Build Docs (ci.yml filter/docs), Console Pin Gate (ci.yml filter/console), Packed-tarball smoke (opt-in) (needs the needs:pack-smoke label). ⛔ Not a required-context subset.
path surface check-governed-merges.mjs --test on the final five-file list: 0 of 5 hit the register — NOT governed, ordinary queue landing
size 610 changed lines (608+/2−), far below the 5000-line human-merge threshold

Why the file list shrank

The base merge resolved four modify/delete conflicts on packages/spec/api-surface-declarations/{api,kernel,root,security}.txt by taking main's deletion — that directory was retired in 2277d1fcd1 (the #19024 revert). Those four paths have therefore left the diff entirely; the PR now changes five files. Before the merge this PR was dirty, and that state was suppressing CI altogether: head 72bf22a2e1 carried 0 check runs against four queued check suites with latest_check_runs_count=0, because GitHub cannot dispatch pull_request workflows without a computable merge commit. The 35 runs above only exist because the conflict was resolved.

The reviewer verified the resolution lost nothing owed, by construction rather than by assertion: the merged tree differs from main parent 1a2bb9ec2d by exactly the PR's five files, those five are byte-identical between 72bf22a2e1 and 4e2406481e, and main never touched them. Barrel ablation: appending export * from './polarity-axes' and rebuilding turns check:api-surface red with 0 breaking, 4 added — so the surviving gate does see the barrel case, and at this head the barrel does not carry it.

Reported and not blocking

  • Only the start/end axis can fire at the live budget: per-axis guard firings at head are start/end 8, min/max 0, input/output 0, read/write 0. The attestation leg judges "both ends declared", not "the guard can reach it".
  • keyTokens in polarity-axes.ts and the test's tokens are two copies of one regex chain — the module's own "two readers must agree" rationale is honoured for POLARITY_AXES but not for the tokenizer.
  • packages/lint direct findClosestMatches( sites at head number 5, not the 4 the PR states; and a third in-package flat-budget ranker the acceptance notes do not name, packages/spec/src/data/authoring-key-lint.ts:248, measured quiet on every strict surface today. Worth one line on the owed packages/lint card.

Flipping to ready and arming the queue on the strength of the readings above. ⛔ No seat approval was sought or given; this PR is not a governed surface, so none is owed.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 20, 2026 14:52
@os-steve
os-steve added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 4b58dcf Sep 20, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-18572-suggester-opposite-sibling branch September 20, 2026 15:27
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/l tests tooling

Projects

None yet

2 participants