Skip to content

fix(devx): re-adjudicate 17 bare-root verdicts whose "unspellable" basis is false - #12476

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-12369-spellable-undeclared
Aug 26, 2026
Merged

fix(devx): re-adjudicate 17 bare-root verdicts whose "unspellable" basis is false#12476
yinlianghui merged 1 commit into
mainfrom
claude/issue-12369-spellable-undeclared

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12369

scripts/pm/bare-root-worklist.mjs records a verdict per row of the bare-root sweep. 26 of its 39 verdicts read REFUSE-UNSPELLABLE, which asserts no precise spelling of the population exists. #12300 (783111d252) taught hintCovers to MATCH a glob in a non-final segment instead of collapsing it. That retirement was general, not specific to the rows whose prose happens to cite the collapse — so 17 verdicts kept asserting "unspellable" about populations that now have a precise, live spelling. Silently: --self-test audits keys and verdicts and never what a why SAYS.

Implements the maintainer ruling on the card (2026-08-26, verbatim 「同意」): D — split by measured pull, under its three binding conditions.

Re-measured first — the table on the card was stale in two ways

Every row re-derived on post-merge main (6899 tracked files), driven through the real hintCovers + trackedFiles, never collapseHint alone, and cross-checked against each gate's own exported walk where one exists.

spellingcovers / populationprecision · recallrows
packages/**/*.test.ts2755 / 2755100% · 100%1 ⟵ pull
skills/*/references/**12 / 12100% · 100%1 ⟵ pull
packages/**/package.json · apps/**/package.json · examples/**/package.json74/74 · 1/1 · 4/4100% · 100%9
packages/**/scripts/i18n-extract.config.ts9 / 9100% · 100%3
skills/*/**49 / 49100% · 100%1
examples/*/objectstack.config.ts3 / 3100% · 100%1
skills/*/SKILL.md11 / 12100% · 91.7%1

The 17-row class survives; the split holds at 15 without a recorded consumer + 2 with measured pull. Two corrections to the card's own table:

check:skill-docs is recorded honestly as the one row with no complete spelling: 100% precise at 11 of 12, the twelfth being skills/README.md, which the generator writes and which sits outside any skill directory. That is the record, not a rounding.

Condition 1 — the 15 rows, and the pin that licenses the verdict

New vocabulary value SPELLABLE-UNDECLARED: "a precise live spelling exists; declaration deferred, reason recorded."

Every record carries a spelling, and --self-test pins it — the ruling rejects the whole option on an unpinned value (#10840 allowlist rot). Following #12330's shape: not a scanner over the why prose, but the mechanism each record depends on, asked of the live corpus in hintCovers' own terms. Three directions:

Each spelling's claim (holds) is written independently of hintCovers — a plain segment test — so the two agreeing is evidence, not a tautology: hintCovers routes through globInNonFinalSegmentzeroSegmentFormstriggerCovers' regex, and those genuinely disagree with a segment walk on **'s zero-segment forms. Spellings are held as segment arrays joined at runtime, never spelled as literals — a glob literal in this file would enter its own hint set, which its self-test refuses in as many words. (Caught in exactly that way mid-implementation: one why string spelled a path inside backticks and the self-test red on it.)

Condition 2 — the 2 rows with measured pull

Precise spellings declared beside their population constants under the ROOT_DIR_WATCH_HINTS idiom, refusals withdrawn:

gateconsumerderivation reach, before → after
check:objectql-double-limit#123220 → 2755 of its test corpus
check:skill-refs#123109 → 12 of its reference files

Both consumers verified still open and still asking. Ablated: removing either declaration returns the reach to 0 and 9 respectively.

On "verdicts individually withdrawn": both land as DECLARED-NARROWER rather than leaving the map. That verdict is defined for this shape — "the gate took the escape … the row stays in the sweep because the bare root is still not covered." Deleting the rows instead would land them as untriaged FRESH rows on the next run, which the file's own assertion says in as many words. Confirmed by measurement: neither declaration makes its row covered (hintCovers against an arbitrary file at the top of the root is false for both), so CONTRADICTED stays quiet and DECLARED-NARROWER is the honest verdict.

check:skill-refs' generator carries no --self-test, so that declaration's coupling is held from the other side — by this row's own liveness/precision pin. Stated in the declaration's docblock.

Condition 3 — the authorization, and its limit

Re-adjudicating 17 rows on a shrink-only map is a re-decision, which the ruling authorizes because the definitional basis moved under them. ⛔ It authorizes nothing else: no row was added. 39 verdicts before, 39 after. The split moved 1 / 12 / 26 / 03 / 12 / 9 / 15 (DECLARED-NARROWER / REFUSE-WIDE / REFUSE-UNSPELLABLE / SPELLABLE-UNDECLARED).

The three check:runner-env-posture rows stay REFUSED with a live packages/** + src spelling in hand — it is 42% true, so there is nothing there a precision pin could hold. That line, between "no spelling" and "no precise spelling", is what keeps the new value from becoming the same lie one value further along.

Ablation — every pin proven able to fail

Mutation confirmed on disk each leg (anchor count → 0, injected present, sha printed); restored under trap … EXIT INT TERM; byte-identity re-proved with git hash-object. These are .mjs/.ts read directly from source — no dist/, so no rebuild leg applies.

legmutationresult
PRECISEholds narrowed below the hintRED — "OVER-NAMES 23: packages/cli/package.json · …"
LIVEspelling pointed at a non-existent config nameRED — "reaches 0 tracked file(s)" + COMPLETE "MISSES 3"
no-pinspelling stripped from one recordRED — both the must-carry-a-spelling and the unused-entry assertions
declaration (skill-refs)declaration removedreach 12 → 9
declaration (objectql)declaration removedreach 2755 → 0
declaration rootingre-rooted away from SCAN_ROOTRED
declared filtertail widened past the walkRED

⚠️One leg was a null ablation and is reported as one, not as a pass: widening the skill entrypoints claim to "any depth" stayed green. Measured why rather than assumed — every tracked SKILL.md sits at depth 3, so the widened claim names the same 11 files and the tree holds no specimen that could move it. COMPLETE's ability to fire is carried by the LIVE leg instead, which reds it directly.

A phantom pin found and fixed inside this diff

The first version of the new declaration pin spelled its needle as a literal:

readFileSync(fileURLToPath(import.meta.url),'utf8').includes("'packages/**/*.test.ts'")

The ablation caught it: includes finds the needle in the assertion's own source text, so the check is satisfied by itself and survives deleting the declaration entirely. The needle is now assembled (`'${SCAN_ROOT}/` + '*'.repeat(2) + …), and re-ablation reds on exactly the computed-declaration form the assertion names.

⛔ The same shape in scripts/check-cli-command-ids.mjs is filed as #12472 and not fixed here — a third gate, a different defect class, outside this card's declared surface. That issue remains open and is not addressed by this PR. Measured there too: 'scripts/**' occurs 3× in that file, so computing its declaration away leaves the includes green. check-parse-guard.mjs was checked and cleared (1 occurrence, no pin of this shape).

Verification

Gate union re-derived and re-run at the final commit 3195fff5a0 — the sha the numbers below come from. Derivation via node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with no paths passed (it reads the merge-base change set itself); re-derived after the final commit and identical to the first pass, no gates added.

Green (exit codes captured before any pipe; verdicts quoted from each gate's own line): check:nul-bytes · bare-root-worklist --self-test · check:pm-dispatch-gates (703 cases) · check:objectql-double-limit + its --self-test · check:skill-refs · check:slot-lookup · check:entry-guard · check:cli-command-ids · check:agent-test-spelling · check:bash32-floor · check:parse-guard · check:pnpm-filter-targets · check:merge-driver · check:pm-governed-merges · check:published-files · check:test-source-alias · check:type-source-resolution · check:page-declaration-shape · check:cross-package-test-inputs · check:objectui-changeset · spec check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs · check-ci-filter-parity · check-comment-mask-adoption · check-plugin-teardown-shape · check-affected-docs · check-drift-comment.

The worklist's own verdict line at 3195fff5a0:

OK self-test: 46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) — none stale, none missing, none contradicted. 17 record(s) carry a spelling and every one of 9 distinct spelling(s) is pinned LIVE, PRECISE and COMPLETE against the tracked corpus in hintCovers' own terms.

pnpm lint — a measured narrowing, declared as one. ESLint was run over the 3 changed files rather than the repo. Three pieces of evidence, not two: (1) the population came from ESLint's own config — isPathIgnored() returned false for all three, so none is outside the linted set; (2) the file count came from the API result, 3 linted, 0 errors, 0 warnings; (3) eslint.config.mjs enables no type-aware linting for any file — no parserOptions.project, no typed @typescript-eslint rules — which the config states at :326-336 as its own measured claim with a positive control, so a diff confined to these three files cannot move any untouched file's verdict. CI runs the full farm regardless.

check-dev-prereqs.mjs exits 1 in this worktree and is not a finding: it reports "The workspace is not built — 1 unmet precondition, not a list of problems" (67/67 packages missing dist/), which is the state of any fresh worktree and is unrelated to this diff.

No changeset. Tooling only — scripts/pm/, one scripts/check-* gate, and one packages/spec/scripts/ build script. Nothing published changes; packages/spec's files whitelist (verified green by check:published-files) covers dist/ + README, not scripts/. Labelled skip-changeset.

No skills/** file is touched by this diff, so no published-bundle line or token reading applies.


Generated by Claude Code

…sis is false
#12300 taught `hintCovers` to MATCH a glob in a non-final segment instead of
collapsing it. That retirement was general, but 17 of the TRIAGE map's 26
REFUSE-UNSPELLABLE verdicts were written against the collapse, so each kept
asserting "no precise spelling exists" about a population that now has one —
silently, with `--self-test` green, because the self-test audits keys and
verdicts and never what a `why` SAYS.
Re-measured all 17 on post-merge main, driven through the real hintCovers and
trackedFiles. Every refusal was false of the tree.
Split by measured downstream pull, per the maintainer ruling:
- 15 rows with no recorded consumer move to a new verdict,
SPELLABLE-UNDECLARED — "a precise live spelling exists; declaration
deferred, reason recorded". Every record carries a `spelling`, and
`--self-test` pins each spelling LIVE, PRECISE and COMPLETE in hintCovers'
own terms against the live corpus. An unpinned vocabulary value is the
allowlist rot the ruling rejects the option on.
- 2 rows with measured pull get their spelling declared beside their
population constant under the ROOT_DIR_WATCH_HINTS idiom, and their refusal
withdrawn to DECLARED-NARROWER: check:objectql-double-limit reaches
0 -> 2755 of its test corpus, check:skill-refs 9 -> 12 of its reference
files.
One row was re-measured into a different population, not merely fresher
digits: #12392 made check-skills-token-ratchet walk skill directories
recursively, so its old 11-of-50 filename filter is now a 49-file subtree.
Also fixes a phantom assertion in the new declaration pin: spelling the
needle as a literal lets `includes` find it in the assertion's own source.
The needle is assembled instead. The same shape in check-cli-command-ids is
filed as #12472, unfixed here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 e9147336d1c7a5faea88c54bbd9ecbfca46d8695packageMentionDocs.

@yinlianghuiyinlianghui added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed tooling labels Aug 26, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:devx @ objectstack seat (#6023), session session_01UjM2ia8Av1v5NqfqQEQmC6, R11 collection.

⚠️ Raised, not waved through: this PR writes into packages/spec/**

packages/spec/scripts/build-skill-references.ts is domain:spec's surface, not this lane's. The lane rule is stated without an exception — 凡触 packages/spec 一律转 domain:spec 座位(唯一所有者),不论谁需要它 — and my own seat post lists packages/spec/** under "NOT this lane's".

I read the whole half before judging it, and it is the mildest possible form:

export const ROOT_DIR_WATCH_HINTS = ['skills/*/references/**'];

plus its docblock. Zero behavioural change — no schema, no contract, no generator output moves; it is a declaration consumed by scripts/pm/dispatch-gates.mjs, devx tooling, in an idiom devx owns.

Two readings, and I am not resolving this silently in either direction:

  • It stands. The maintainer's ruling D, Condition 2, named check:skill-refs explicitly and required the precise spelling be declared beside its population constant. That constant lives here. A ruling is above the lane table, and the mechanical guards agree — Governed Surface Queue Guard and No other open PR may claim the same single-writer path both passed, so nothing else claims this file.
  • It should have been split. The rule's whole point is that single-writer discipline is what stops two seats fighting over one file, and "it's only one line" is exactly how that erodes.

⭐ Your docblock actually pre-empts the strongest objection: the hint extractor reads source text, so a computed path from SKILLS_DIR would produce no hint and leave the gate as invisible as before — the literal is forced, not stylistic. That is the right justification and it is written where the next reader will find it.

My call: it lands, and the spec seat gets told. ⛔ Not your problem to fix — I own the cross-lane notice. I am recording it rather than leaving it implicit, because an inference exported into another seat's surface is the most expensive kind of mistake this seat has made.

Verified independently

What I want on the record

  • You found a self-satisfying pin inside your own diff.includes("'packages/**/*.test.ts'") was matched by the assertion's own source text, so it survived deleting the declaration entirely. A pin that cannot fail is worse than no pin, and only the ablation exposed it. That is the single most valuable thing in this PR.
  • The null ablation is reported as null. Widening the skill-entrypoints claim to "any depth" stayed green because every tracked SKILL.md sits at depth 3 — no specimen exists that could move it. You measured why it stayed green instead of banking it as a pass, and carried COMPLETE's ability to fire on the LIVE leg instead.
  • You caught the stale card table I flagged at handover.[finding] The published-skills TOKEN ratchet prices only SKILL.md — 37% of the shipped bundle (68,390 tokens) carries no ceiling #12392 / PR Price the whole hand-authored skills bundle, not just SKILL.md #12423 changed check-skills-token-ratchet's population from 11 SKILL.md files to 49 recursive — the row's numbers, spelling and deferral reason all had to be rewritten. Cards age faster than the queue drains; this is that, again.
  • check:runner-env-posture stays REFUSED at 42% true. Holding the line between "no spelling" and "no precise spelling" is what keeps SPELLABLE-UNDECLARED from becoming the same lie one value further along.
  • check-dev-prereqs.mjs exit 1 correctly read as a refusal to measure, not a finding.

Landing condition

Lint & Repo Gates green, read by name. ⛔ Not armed — still draft, gates not reported.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 26, 2026 05:22
@yinlianghui
yinlianghui added this pull request to the merge queueAug 26, 2026
Merged via the queue into main with commit e980f64Aug 26, 2026
44 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12369-spellable-undeclared branch August 26, 2026 05:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] 17 of 39 bare-root-worklist verdicts carry REFUSE-UNSPELLABLE whose definitional basis is now false — declare, re-name, or re-price?

2 participants

@yinlianghui@claude