Skip to content

fix(tooling): declare kebab-case diagnostic codes a separate vocabulary, in one place - #13130

Merged
os-elon merged 3 commits into
mainfrom
claude/issue-12925-kebab-code-vocabulary
Aug 29, 2026
Merged

fix(tooling): declare kebab-case diagnostic codes a separate vocabulary, in one place#13130
os-elon merged 3 commits into
mainfrom
claude/issue-12925-kebab-code-vocabulary

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#12925

Turns an accidental silence into a declared one, in one place. No grammar is widened, no code is renamed, and no gate verdict changes today.

The seam

The two gates over code-shaped values are designed as a pair that hands work to each other, and a kebab-case code falls in the seam between them:

  • it has a quoted literal at the stamp position, so check-dispatcher-error-vocabulary's objlit/assign shapes delegate a lowercase value to the other gate (lowercase: 'casing-gate');
  • it is lowercase, so it is check-error-code-casing's — but that gate's grammar is [a-z][a-z0-9_]*.

Neither grammar admits a hyphen, so the literal matches no pattern in either gate. Both count the file as scanned; neither has anything to say about it. The bound this gate declares for itself — a value it cannot reduce is REPORTED, never dropped — is honoured for the constant form and quietly violated for the literal form, because a value outside every grammar fires no pattern and so cannot report itself.

What landed

KEBAB_DIAGNOSTIC_VOCABULARY in scripts/check-dispatcher-error-vocabulary.mjs — one exported constant with a docblock, following the two shapes this repo already uses for a declared population boundary (SANDBOX_AUTHORED_LIMB in the declaration file, cited in the printed bounds; isDeclarationFile, a named exclusion pinned by --self-test).

Three properties, deliberately:

  1. One place, covering both gates. The declaration lives in the file that already documents the pair's division of labour, and the other gate's own bounds block already points readers here ("that half belongs to check:dispatcher-error-vocabulary"). check-error-code-casing.mjs is untouched — two gates each carrying half of a boundary is what produced this hole.
  2. A run says it. The printed bounds line is derived from the constant, so it cannot drift from it.
  3. Testable, not prose.--self-test pins it, and imports the other gate's real detector (findViolations) rather than paraphrasing its patterns — so widening either gate trips it. The failure text names the decision: a gate-population change, to be measured and routed to the maintainer on its own card.

Measurements

The five grammars, re-verified on this tree (not relayed): :272 assign, :274 classfield, :280 objlit, :427literalCodeValues, :1088 template shape — none contains a hyphen. Plus all six patterns in check-error-code-casing, grammar [a-z][a-z0-9_]*.

Census, with an independent control. The triage comment flagged its own control as same-population; this is the independent one. Both production instruments were run over three populations:

populationvocabulary-gate SHAPEScasing-gate findViolationskebab detector
packages/sdui-parser/src/validate.ts (subject)007
packages/metadata-protocol/src/build-probes.ts (snake_case diagnostics)660
packages/runtime/src/dispatcher-error-vocabulary.ts (SCREAMING_SNAKE)2500

Both instruments return non-zero on independent populations, so the zeros on the subject are readings and not dead instruments; the kebab detector returns zero on both independent populations, so it discriminates. The subject file is confirmed inside the scanned set (2078 files) — it is read and matches nothing, which is the whole defect.

The family is larger than the card's census, and fully homogeneous. Triage corrected the card's six to seven; decomposed across the whole package it is 24 distinct codes, 24 of 24 kebab, 0 non-kebab:

  • 8 stamped directly as code: '...' — 7 in validate.ts (triage's seven) plus unconsumed-widget-option in dashboard-widget-options.ts;
  • 16 more through the code-carrying helper Parser#error(code, ...) in parse.ts.

Decomposing the 10 raw code positions in the package: 8 value stamps (all kebab) and 2 type positions carrying no value (Diagnostic.code: string, and the helper's parameter annotation).

The inverted incentive is confirmed, and is worse than the card states.deriveSites on a kebab constant yields 0 sites and exactly 1 unresolved-constant, pushed unconditionally in reconcile(). Trying to discharge it with a declaration row does not remove it — the row itself becomes a second finding (stale-row), so the attempt takes the author from 1 finding to 2. Pinned in the self-test.

The ablation, including the leg that falsified my prediction

Predicted before running; the control run must stay GREEN (both gates are already green over the kebab family, so "still green" would prove nothing on its own), each mutation leg must go RED and name the decision. Every mutation is proved on disk by an exact-literal anchor with a match count, and every restore by git diff HEAD empty and blob hash equal to the HEAD blob. No build leg: each subject is executed by node PATH directly — a root .mjs, never through a package exports into dist — and the witness is read as source by the pin.

legmutationresult
controlnoneGREEN, 160 assertions
Awiden this gate's objlit to admit a hyphenRED
Bwiden literalCodeValues to admit a hyphenRED (reducer + the constant now resolving)
Cwiden the other gate's emission patternRED — the proof that one declaration covers both
Da non-kebab code lands in the witness packageRED, naming the file and the value
Ebreak the twin half of the instrumentRED — the twin assertions are load-bearing

Leg A came back GREEN on the first run, against my prediction, and that changed the fix. Widening objlit alone makes a kebab literal match and still report nothing: objlit carries lowercase: 'casing-gate', so the delegation hands the value to the gate whose grammar also excludes the hyphen — back into the same seam, one layer deeper. The first version of the pin watched findings and therefore sat green through exactly the widening it exists to catch. The pin now counts recognizer matches on both gates, which is the actual population question, and the reading is recorded in the declaration because it sharpens the card's claim: matching is not the same as seeing. Leg A is red in the re-run above.

Local verification

Union run after the final commit, at e5481a759d. 18 commands, all exit 0, each verdict read from the gate's own printed line: check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:dispatcher-error-vocabulary, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal, check:nul-bytes, check:pm-dispatch-gates, check-ci-filter-parity, check-shard-attestation, bare-root-worklist --self-test, check-self-test-wired, and the --self-test of both vocabulary gates plus the casing gate's ordinary run. Family derived with dispatch-gates --repo objectstack-ai/objectstack on the synced tree (change set: 1 path, no stale-tree warning); the convention pair for a gate-script edit is included.

check-test-completeness.mjs is named by the derivation but answers a standalone run with exit 1 and a usage line — an invocation error rather than a red gate, already on record as #13110. Not run, not fixed here.

ESLint, narrowed and declared (the repo-wide sweep is CI's run). Targeted: eslint --no-inline-config --format json on the changed file, 1 file linted, 0 errors, 0 warnings. The three pieces of evidence the narrowing needs: the population comes from eslint's own config resolution rather than my guess (it resolved a config and linted the path); the file count is read from the JSON output; and this repo's single eslint.config.mjs never enables type-aware linting for any file — no parserOptions.project, no typed rules — so a one-file diff cannot move any untouched file's verdict.

No changeset, derived rather than asserted: root scripts/ appears in no package's files whitelist, so this releases nothing. check-empty-changeset.mjs names this case itself — "a PR that edits a CI-internal script is the textbook case for that label". The skip-changeset label is applied.

Deliberately not done

  • No grammar is widened to admit a hyphen. Widening would newly resolve constants repo-wide into sites that then need declaration rows, and that blast radius is unmeasured. Measuring it is a gate-population change and belongs on its own card with the maintainer, never as a rider here.
  • The diagnostic family is not renamed to SCREAMING_SNAKE to make a regex see it. Kebab is that vocabulary's convention across all 24 of them.
  • The declaration is not split across the two gates.

Out of scope, filed separately

A second hole surfaced while measuring the helper half of the census, and it is a different defect class — casing-independent, so not a rider on this card. A code-carrying helper that stamps its code through an object-literal shorthand ({ severity, code, message }) rather than an assignment (obj.code = code) is invisible to both gates for any casing. Measured against a control: .code = code inside a helper is detected as codehelper, while the same helper stamping { code } returns 0 from both gates even for an unregistered SCREAMING_SNAKE value; { code: code } longhand is 0 too. That is the #9223/#10918 genre one stamp position later. Parser#error in sdui-parser is a live instance, carrying 16 codes. Filed as a finding; no fix here.

Context, not scope: #12924 reads the same file's diagnostics for an unrelated reason and independently arrives at the same seven in validate.ts. That card is not addressed here and remains open.


Generated by Claude Code

…ry, in one place
Two gates guard code-shaped values and hand work to each other, and a
kebab-case code falls in the seam: it HAS a quoted literal at the stamp
position (so check-dispatcher-error-vocabulary defers it as lowercase),
and it is lowercase (so it is check-error-code-casing's), but that gate's
grammar is [a-z][a-z0-9_]* and no grammar in either gate admits a hyphen.
So a kebab literal matches no pattern anywhere, both gates count the file
as scanned, and neither has anything to say about it.
Coverage of that family was already zero; this changes no verdict. What
changes is that the silence is now DECLARED rather than accidental --
KEBAB_DIAGNOSTIC_VOCABULARY states it once, the printed bounds are derived
from it so a run says it, and --self-test pins it against BOTH gates'
recognizers by importing the other gate's detector instead of paraphrasing
it. If anyone later widens a grammar to admit a hyphen, the pin fails and
names the decision as a gate-population change for the maintainer.
Deliberately NOT done here: widening any grammar (unmeasured blast radius
across every constant in the repo), and renaming the diagnostic family to
SCREAMING_SNAKE to make a regex see it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
… not the finding list
Found by ablating the pin rather than by reading it. Widening `objlit` to
admit a hyphen makes a kebab literal MATCH and still produces no finding:
`objlit` carries `lowercase: 'casing-gate'`, so the delegation hands the
value to a gate whose grammar also excludes the hyphen -- back into the
same seam, one layer deeper. The first version of this pin watched findings
and therefore sat GREEN through exactly the widening it exists to catch.
The population question is whether any published recognizer MATCHES the
value, so that is what is counted now, on both gates' recognizers. The
reading is recorded in the declaration too, because it sharpens the card's
claim: matching is not the same as seeing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33239036507 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test: FAIL test/serve-publishes-bound-port.e2e.test.ts > #13062 the non-zero half — nothing an ordinary boot publishes may move > follows the DEV AUTO-SHIFT onto the port it really ↳ 失败原因: @objectstack/cli:test: Error: ENOENT: no such file or directory, open '/tmp/os-bound-port-home-ezFVcI/runtime.env_local.json'
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 2 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit feb55d3Aug 29, 2026
37 checks passed
@os-elon
os-elon deleted the claude/issue-12925-kebab-code-vocabulary branch August 29, 2026 07:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-elon@claude