Uh oh!
There was an error while loading. Please reload this page.
feat(devx): gate CLI command-id string literals outside the CLI package - #12225
Merged
Conversation
) A CLI command id spelled as a string literal outside `packages/cli` has nothing tying it to the command file it names. Rename the command, update the CLI and the docs but not the engine, and `driver-sql`'s drift warning keeps printing a route that no longer exists — a stale hint inside an otherwise-correct message, which no suite reads as wrong: the driver's own pin asserts the message CONTAINS the constant, and the constant still matches itself. The card offered the gate on the argument that the constant "is unlikely to stay the only such string" — a claim about a population nobody had counted. Counted first: 274 command-id literals across 98 files outside the CLI package, 14 of them in `schema-drift.ts` itself. The same warning message that interpolates the named constant also spells "os migrate apply" inline, as a bare literal with no constant and no pin at all. The gate derives ids from declared data (`oclif.bin` plus every `bin` key; `src/commands/**` paths), never a table, and resolves topic directories as well as command files. Its self-test pins the #12016 rename itself: the same literal that resolves before the rename resolves to nothing after it. That case is what caught a plain longest-prefix rule quietly rescuing `os migrate multi-value-columns` as `os migrate` — blind to exactly the failure the gate exists for. Two declared ledgers, both self-retiring: three self-test fixtures, and one real violation the first live run found in `packages/spec` (#12223). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_af22b339-91b5-5814-8b9b-2453fc5b3f68
…lation A checker cannot be its own evidence. Every negative fixture in this file — `os migrate nonexistent-command`, `os demo`, `os nope nope` — is an unresolvable id ON PURPOSE, because that is what a self-test for this gate is made of. Scanning them made the gate red in proportion to how well it was tested, with "delete the tests" as the only way back to green. Found the honest way rather than reasoned out: `populationFiles` reads `git ls-files`, so the gate ran green while its own source was still UNTRACKED and reded on the first run after it was committed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_af22b339-91b5-5814-8b9b-2453fc5b3f68
This was referenced Aug 25, 2026
…ch can name it
`dispatch-gates` derives WHICH gates a card must run by matching path literals
in each gate's source against the card's changed files, and `hintCovers`
REFUSES a bare single-segment literal as too generic — a measured refusal
(+139084 fabricated pairs) that stays. `populationFiles` spelled its admission
predicate as `f.startsWith('scripts/')`, which collapses to `scripts`, so this
gate would have "landed already invisible": never named for a card touching its
own population, scoring the same quiet green for every card in the tree.
Remedy (a), not (b), and the difference is what the gate READS. `'scripts/'`
here is an ADMISSION predicate, not an exclusion or a fixture path: every
tracked source file under that root is in the population with no further test,
and seven of them already carry findings. So the subtree spelling is a true
statement about the scan, and declaring it is the `ROOT_DIR_WATCH_HINTS` idiom
(`check-role-word.mjs`, `check-examples-live-imports.mjs`), not a fabricated
lead.
The roots are hoisted to POPULATION_ROOTS so the predicate and the declaration
cannot drift, and four self-test cases pin both directions: every
separator-less root is declared as `<root>/**`, nothing is declared that the
gate does not walk whole, the declaration is a source LITERAL (a computed
`${r}/**` builds no hint at all), and the population really reaches across the
root. `packages/**` is deliberately NOT declared — this gate opens each
package's `src` subtree and its package.json, not the root entire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_af22b339-91b5-5814-8b9b-2453fc5b3f68…i-command-id-coupling
…i-command-id-coupling
yinlianghui
marked this pull request as ready for review
August 25, 2026 15:25
yinlianghui
enabled auto-merge
August 25, 2026 15:25
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#12016
The card offered three shapes and said plainly "⛔ not a recommendation — triage's call". Triage queued it without picking, and set a go/no-go: count the population first. Option 1's whole argument was that the driver constant "is unlikely to stay the only such string" — a claim about a population nobody had counted. This PR counts it, and the count chose the option.
The measurement that settled it
Command ids derived from the oclif filesystem convention, then every non-comment string literal in non-test source outside
packages/clichecked against them:packages/cli/src/commands/**migrateis both)static aliasesanywhere in the treepackages/clipackages/spec/src/migrations/registry.tspackages/drivers/driver-sql/src/schema-drift.tspackages/drivers/driver-sql/src/sql-driver.ts274, not 1. The card's prediction was right by two orders of magnitude — and it is right inside the finding's own file.
MULTI_VALUE_COLUMN_REMEDY_COMMANDis one of fourteen command-id literals inschema-drift.ts, and the same warning message that interpolates it also spells"os migrate apply"inline on line 756, as a bare literal with no constant, no export and no pin at all. A per-constant pin (option 2, already declined in #12012) would have covered 1 of 274 and left the identical hazard three lines down in the same template literal.So: option 1, on the number.
Option 3 was falsified — it is already done, and it never covered the finding
The card's option 3 was "record the coupling in the docs-audit affected-docs ledger so a rename drags the docs page in". That mechanism exists, and it is already wired:
scripts/docs-audit/affected-docs.mjshas had acommandanchor kind since #9230, derivingos meta resyncfrompackages/cli/src/commands/meta/resync.ts. Verified against this repo rather than assumed:A diff touching the command file already lists
content/docs/deployment/cli.mdx. There was no ledger entry to add. And its declared scope iscontent/docs/**/*.mdx— so it never reachedschema-drift.ts, which is the finding. Option 3 was both already-done and orthogonal.The gate
scripts/check-cli-command-ids.mjs— every command-id string literal outside an oclif package must resolve to a real command path inside one.Everything is derived from declared data, never a table: which packages are CLIs (
oclif.bindeclared, not a hardcodedpackages/clipath — a second CLI package is covered the day it lands), which binary names count (oclif.binplus everybinkey, which is howobjectstackis covered as well asos), and which ids exist (src/commands/**paths,topic/index.tscollapsing to the topic).The delimiter rule is the whole precision story. A candidate must open immediately after a
',"or backtick. That was measured too: the loose form ("a bin name anywhere on a quoted line") produced 9 unresolvable hits of which 6 were noise — Spanish translation prose where\bfired insideenvíos diarios, a Pythonimport os from 'os'example, and the sentence "carry an os validate-clean security posture". Every one has a letter or a space before theos. The delimiter rule drops all six with no carve-out.Bare topics resolve.
os metahas nometa/index.ts, butmeta/exists and oclif serves a topic as topic help, not an unknown-command error — soplugin-auth's'os meta' runprose is correct and flagging it would be the gate fabricating a finding.The self-test caught the gate being blind to its own purpose
Per the dispatch, the gate ships with a self-test pinning a known-bad literal so it can be shown to fail. That self-test earned its keep immediately.
The first draft resolved by plain longest-prefix — two words, else one. Under that rule, rename
migrate/multi-value-columns.tsand'os migrate multi-value-columns'quietly falls back tomigrate, which is a real id. The gate would have stayed green through exactly the rename #12016 exists to catch. The--self-testrename case is what caught it; the fallback is now conditional on whether the first word is a topic (a following word is a subcommand attempt) or a leaf command (a following word is an argument).38 cases, including: the same literal resolving before a rename and resolving to nothing after it; a known-bad
os migrate nonexistent-commandbeside a goodos migrate apply; and all six measured noise shapes staying out.Two ledgers, both self-retiring — and one real defect found on the first live run
Both are keyed by file and exact text, and
mainfails if a listed entry no longer reproduces — so neither can rot into a claim about a line that has since been fixed. That ispackages/cli's ownEXCLUDEDidiom from #10967 / #11465: an exemption that asserts its own cause still holds.FIXTURE_EXEMPTIONS— 3 entries, all negative-case fixtures inscripts/docs-audit/*self-tests where an unresolvable id is the point.BASELINED_VIOLATIONS— 1 entry, printed on every green run. The gate's first live run found a real defect:packages/spec/src/api/endpoint.zod.ts:39tells an author, in a present-tense refusal message, that "publication state is managed byobjectstack publish" — a command retired with the direct-to-environment path. Filed as packages/spec: an author-facing refusal message routes toobjectstack publish, a command retired with the direct-to-environment path #12223 with the two lower-severity sibling sites; not fixed here, becausepackages/specis not this card's lane.⭐ Four invisibility bugs, one shape — and two of them are findings about the tooling
This PR hit the same failure mode four times at four levels. The gate itself is the small half; the pattern is the interesting one.
The gate was invisible to itself.
populationFilesreadsgit ls-files, so it ran green while its own source was untracked and reded on the first run after being committed — every negative fixture in a gate for unresolvable ids is unresolvable by construction. Fixed by excluding its own source, pinned by a self-test case.The gate was invisible to
dispatch-gates— caught only as red CI.hintCoversrefuses a bare single-segment literal as too generic (a measured refusal, +139084 fabricated pairs), andpopulationFilesspelled its admission predicate asf.startsWith('scripts/'), which collapses toscripts. The gate would have "landed already invisible": never named for a card touching its own population. Remedy (a), not (b), and the difference is what the gate reads:'scripts/'here is an admission predicate, not an exclusion or a fixture path — every tracked source file under that root is in the population with no further test, and seven already carry findings. SoROOT_DIR_WATCH_HINTS = ['scripts/**']is a true statement about the scan.packages/**is deliberately not declared: this gate opens each package'ssrcsubtree and itspackage.json, not the root entire, and naming a root the gate does not read is a fabricated lead. Roots are hoisted toPOPULATION_ROOTSso predicate and declaration cannot drift, with four self-test cases pinning both directions — including that the declaration must be a source literal, since a computed`${r}/**`builds no hint at all.⭐ The obligation itself is undiscoverable, and that is measurable. The rule in (2) is enforced by
check:pm-dispatch-gates— which is not derivable for a card that adds a gate underscripts/. Its own tool says so:So the gate that enforces "declare your subtree or land invisible" cannot itself be named for the cards that need it, and the obligation surfaces only as red CI after the PR is open. This is the second undiscoverable landing obligation for a new gate, in a second tool.[finding] "A new gate that assembles a root must record a bare-root verdict" is not derivable — two independent new gates hit the identical CI red within one hour #12074 documents the first —
bare-root-worklist'sTRIAGEFRESH verdict, hit by two devs within one hour. This is a different species (escapable-literal) in a different tool, and [finding] "A new gate that assembles a root must record a bare-root verdict" is not derivable — two independent new gates hit the identical CI red within one hour #12074 does not mention it, so this PR is evidence the class is larger than that card measured.⭐ The branch was invisible to the drift guard that watches exactly this — see Verification below. Local ran a 580-case
dispatch-gates; CI ran the 595-case one; the 15-case gap was the signal and it sat in the report unread. Being written up on [finding] "A new gate that assembles a root must record a bare-root verdict" is not derivable — two independent new gates hit the identical CI red within one hour #12074 alongside (3).Note the shape running through all four: in (1) the gate could not see itself; in (2) the derivation could not see the gate; in (3) the tool that decides which gates a card must run cannot name the gate that would have told the author; in (4) the guard that reports staleness is the one the stale tree predated. (3) and (4) are left as findings, not fixed here —
dispatch-gates's ledger is shrink-only and the tool is another lane's.Relationship to #11465 / #12177 — complementary, not overlapping
The same class was measured inside
packages/clion the same days: 536 invocations in 107 sources, 6 unresolved. That card fixed two and declared two in place, and #12177's declarations say in prose that "a sweep over the documented CLI invocations in this package will flag both of them — leave them."This gate's population starts where that one stops: every oclif-declaring package is excluded from its own scan, so the two never touch the same line and #12177's declared exemptions are out of reach by construction. Two of its lessons are built in rather than rediscovered — a bare topic resolves (its
os datasourcefalse positive cannot occur here), and an exemption asserts its own cause.Scope
Per Zone 1:
packages/cliandpackages/drivers/driver-sqlare untouched,packages/specis untouched (#12223 stays a baselined entry, not a fix here), the string constant is not removed, and nothing imports the CLI from the driver. Option 2 stays declined.Verification
origin/main, and one of them was wrong because of it. That reading is left here rather than swapped out, because it is the point.Round 2 reported⚠️ The remedy was therefore designed against the 580-case program and, until this round, validated against the 595-case one only by CI — the re-run below is what makes it a deliberate result rather than a lucky one.
✓ dispatch-gates self-test: 580 cases pass. The CI run that reded reported1 of 595 case(s) failed. 595 → 580 was not the fix; it was 15 cases the branch did not have. The branch's merge-base was8a7d070dba, and it predated PR #11934 (mergeda187fe612b), which addeddispatch-gates' stale-tree drift guard — "594 includes 15 new cases for the drift guard". CI checks out the merge of head intomain, so local and CI were running different programs: a green measured on a tree that would never be merged, offered as evidence about the tree that would.⭐ And the guard that prints exactly this warning —
— watches
scripts/pm/dispatch-gates.mjsamong its files. The guard that would have caught it is the guard the tree predated. That is invisibility bug (4) above, and the reason this round merged rather than re-ran.origin/mainis now merged in (a merge commit, not a rebase; nothing force-pushed), and everything below is re-measured on the merged tree at9fbb5a29d0— not re-asserted from the stale round. The guard was also used as intended mid-round: after the first merge it fired again naming three newly-changed files, and a second merge cleared it.pnpm check:pm-dispatch-gates(the CI red)✓ dispatch-gates self-test: 595 cases pass, zero✗lines✓ no gate has NEWLY joined the escapable-literal species— now proven against the post-#11934 codeSTALE TREEwarningpnpm check:cli-command-ids✓ … 274 command-id literal(s) across 98 file(s) … all resolve to a real command pathcheck:cli-command-ids --self-test✓ … 38 cases passcheck:nul-bytescheck:type-check-debt, belowPositive control, re-run on the merged tree (the fix proven, not merely un-red): with a brand-new
scripts/file present,dispatch-gatesnames this gatevia gate source 'scripts/**'— the declaration is doing the work, not the gate's own filename. Probe file removed;git statusclean.One did not run:
check:type-check-debtrefuses to measure without the full built workspace closure (turbo run build --filter='./packages/*', which lint.yml performs before that step) — a declared prerequisite, not a finding. Declared narrowing, with its warrant: it enters the derivation only via.github/workflows/lint.ymlas a gate source path, and this diff contains zero.ts/.tsxfiles, zerotsconfig*, and one addedcheck:*line inpackage.jsonthat touches no typecheck wiring — so nothing in its measured population changed and its verdict cannot move. Its sibling legcheck:type-check-coverage, which validates the wiring and needs no built closure, ran green.Second declared narrowing, on process rather than coverage: this round's union was run without the shared heavy-verify lock. A request for it timed out after 9 minutes without ever acquiring (
VERDICT queue-timeout (exit 99)), behind a full-repoturbo run build. These 24 families are light static-analysis scripts — the whole union measured at 65s wall — not the build/test work that lock exists to serialize. Every exit code was still captured to a file before any pipe, and all 25 logs are free ofPREREQUISITE NOT MET. Flagged because an earlier run in this round did silently read a stale/tmp/gate-results.txtfrom beforepnpm installcompleted, which is exactly the shape of error this PR is about; the results file is now truncated per run.skip-changeset:scripts/+.github/workflows/+ onepackage.jsonscript line. Nothing user-visible is published.Back-links: #11535 · PR #12012 (the coupling) · #11465 / PR #12177 (the sibling sweep) · #12223 (the defect this gate found) · #11934 (the drift guard this branch predated) · #12074 (the undiscoverable-landing-obligation class; this PR contributes a second species and the staleness mechanism).
Generated by Claude Code