fix(tooling): the ADR-0087 prescription detector reads real prescriptions, not the FROM/TO placeholder (#6419) - #6432
Merged
os-project-manager merged 1 commit intoAug 7, 2026
Conversation
…ions, not the FROM/TO placeholder (#6419) `hasMigrationPrescription` had an English branch and a Chinese branch and BOTH required the literal uppercase words `FROM` and `TO`. So it detected the shape of one historical changeset (#6048's `迁移:FROM → TO` template) rather than the presence of a prescription -- and because a useful prescription names real identifiers, the better an author wrote one, the more invisible it became. The `no-migration-prescription` exemption is a self-contradiction check that rests entirely on this detector, so a detector blind to real prescriptions made that category trivially abusable. Widened to a union of two branches. Branch 1 is the old pattern, verbatim, so the detector is a strict SUPERSET -- measured, not asserted. Branch 2 fires on a framing-anchored rewrite: `X → Y` with code-ish operands on both sides, on a line carrying migration framing or under a heading that carries it, in both languages. Measured over the 1342-changeset stock (235 declared-breaking): old 113 hits / 87 breaking, new 122 / 92, +9 / -0. Seven of the nine are real prescriptions the old pattern missed; two are false positives on changesets that declare no breaking change and are therefore never judged. Zero changesets in the tree claim the category, so no verdict changes. Reverse-verified with the direction predicted first: ablate branch 2 and the five real-prescription pins plus both scan-level cases go red (14 failures), while the label branch and the false-positive floor stay green. Self-test 46 -> 67 assertions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
os-project-manager
marked this pull request as ready for review
August 7, 2026 19:01
os-project-manager
deleted the
claude/issue-6419-adr0087-never-declared-category
branch
August 7, 2026 19:12
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 free
to 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 #6419
Scope: Ruling 2 only. The
CATEGORIESvocabulary stays three entries, byte-identical — Ruling 1 (a fourthnever-declaredcategory) was withdrawn on the thread and is not touched here. The ledger /registeredpath is likewise byte-identical (verified: the only diff line matchingregisteredis a new test string).The defect
hasMigrationPrescriptionhad an English branch and a Chinese branch, and both required the literal uppercase wordsFROMandTO:迁移:FROM → TO(the template placeholder)迁移:`aggregate:` → `aggregations:`(a real prescription)So it detected the shape of one historical changeset (#6048's), not the presence of a prescription — and because a useful prescription names real identifiers, the better an author wrote one, the more invisible it became. Since
not-required (no-migration-prescription)is a self-contradiction check that rests entirely on this detector, the category was trivially abusable.Premise re-verified against
origin/main(e8dc61e) before implementing: the regex is exactly as the issue describes, the Chinese branch迁移[^\n]{0,12}FROM\s*(?:→|->)\s*TOrequires the same two uppercase words, and the detector is consulted at exactly one site — theno-migration-prescriptionarm ofscan().The fix
A union of two branches.
## FROM → TOx16,FROM → TO:x15,**FROM → TO**x11,Migration (FROM → TO):x4.X → Ywhere both sides are code-ish (a backticked span, or a dotted/slashed identifier path), appearing either on a line carrying migration framing or anywhere under a heading that carries it. The framing vocabulary is derived from the stock, not invented — Chinese迁移/改写/改名/升级(from## 迁移,**迁移面**:,## 破坏性变更 · 迁移,## 升级说明,应改写为), Englishmigrat*/rename*/rewrit*/upgrade*, word-anchored.Word-anchoring the English alternatives is load-bearing: without
\bthe token matches insidesys_migration_journal,renameConfigKey,migrations/registry.ts,onUpgrade, all of which appear in prose next to arrows. Measured, it removed one false positive and no true positive.findMigrationPrescription()now returns the evidence line and which branch matched, and the refusal message quotes it. An author who believes their changeset has no prescription can now see exactly what the gate read — the thing nobody could see when #6419 was filed.Measurement (old detector vs new, both run over the real corpus)
Both versions were imported as modules (CLI trailer stripped, functions byte-identical to the shipping ones) and run over the current
.changesetstock, parsed with the gate's ownparseChangeset/breakingDeclaration.-0is the superset property, measured rather than argued.Corpus reach: the current tree's
.changeset/*.mdset — the whole v17 train's accumulated stock, 1342 files. Historical changesets consumed by past releases are not included: they are deleted at version time, so reaching them means replayinggit logover deleted paths across the whole history, which is not cheap and would measure a corpus written under older conventions. The v17 stock is 1342 files against the gate's own design sample of 400 first-parent commits, so it is already the larger measurement.Hand-checked classification of all 9 differences
client-delete-result-success.md`r.deleted` → `r.success`. That is the whole migration.close-out-sweep-inert-keys.mdthe protocol-17 `topics`→`sources` rename is absorbedretiredKeyprescriptions andos migrate metadual-source-cross-form-convergence.md**Renamed — `./contracts` `ShareRecipientType` → `RecordShareRecipientType`:**http-method-defkey-collision.md改名 `HttpMethodSchema` → `HttpMethodSubsetSchema`sharing-rule-recipient-reconcile.mdrename `group` → `team`hook-condition-previous-binding.mdmigration table maps `OLD.x` → `previous.x`object-parse-path-strict.mdthe semantic renames … (`capabilities` / `features` → `enable`)strict-unknown-key-history-last.md[ Did you mean `k1` → `canonical`? ] fix, channel 1 (renames)patchchangesetwidget-contract-theme-token-vocabulary.md`colors` 出门时的改名(`surface` → `--card`…False positives: 2, both individually justified. Neither changeset declares a breaking change, so
breakingDeclaration()filters both out before the detector is ever consulted — their flag is visible only in the--listaudit column. Verdict impact on the current tree: zero.Caution the drivers lane raised on the thread, and it is right: this corpus was itself written under the broken detector, so "historical zero false positives" would prove nothing about safety. That is why the classification above is per-changeset by hand, and why the two false positives are named rather than netted out.
Residual blind spot, measured and documented in the script
Framing-anchored means an unframed rename table is still missed. Measured over the same stock: 128 changesets carry a code-to-code rewrite this detector does not flag, 21 of them declared-breaking — typically the
unknown-key-strictness-*andadr-0112-*batches, which listold→newpairs under a plain heading with no migration word near them. The numbers and the class are written into the script's header so the next person to widen it starts from them.Two wider rules were measured and rejected:
The asymmetry that decides it: a false positive here has no honest escape. The author is refused an exemption they are entitled to and the closed vocabulary offers them nothing else — which is the #6419 shape itself, one category over. So the honestly-scoped detector wins, with its blind spot stated rather than hidden.
Tests
The gate's pin tests live in the script's own
--self-test(real temp git repositories driven through the shippingscan()), per the #6342 template. 46 -> 67 assertions.New scan-level cases (real repos, real
scan()):no-migration-prescription矛盾检查匹配的是占位符FROM/TO,不是真实处方 —— 写得越好的处方越照不到 #6419 shape: a real Chinese prescription (**迁移**:`aggregate:` → `aggregations:`) claimingno-migration-prescription. Must be RED.## 迁移followed by a rewrite list. Must be RED; a line-only rule would read the list as unframed prose.analytics→engine,16 → 17,runtime → objectql,→ 400) under the catch-all. Must stay GREEN.New unit pins: P9-P13 (real prescriptions in both languages, inline and under a heading), P14-P17 (the prose shapes that must still be refused — all real corpus text), P18-P21 (the evidence branch each shape reports).
Reverse verification — direction predicted first
Prediction (recorded before running): ablate branch 2 (delete the framing-anchored limb, leaving the placeholder-only detector) and expect red — P9-P13 and P18-P19 fail on the unit side, R10/R11 report "expected RED, got green" plus their message assertions; and expect P1-P5, P14-P17, P21, G7, R2, G5 to stay green, since branch 1 is unchanged and the false-positive floor is independent of the fix.
Result — exactly as predicted, 14 failures:
Nothing else moved — which is the second half of the proof: the label branch and the false-positive floor are green under both versions.
Observable behaviour on the current tree
Requirement: no changeset in the tree may currently claim
no-migration-prescriptionwhile carrying a real prescription. Measured:The gate landed today (#6148 / PR #6342) and no changeset in stock carries a marker yet, so no verdict anywhere in the tree changes. There is no live self-contradiction to report and nothing to file. The only observable difference on stock is the
--listaudit column (prescription=yes/no) for the 9 changesets above.Gates run
Enumerated from
.github/workflows/lint.ymland.github/workflows/pr-automation.yml, not from memory. Everycheck:*step in both, one by one — all PASS.ESLint job (30 steps):
pnpm lint,slot-lookup,query-options-erasure,nul-bytes,doc-authoring,docs-audit-scope,role-word,quick-reference-counts,adr-anchors,org-identifier,authz-resolver,service-providers,route-envelope,error-code-casing,wildcard-fallthrough,meta-type-normalized,init-service-contract,durability-log-level,startup-registry-verdict,objectui-changeset,release-notes,release-body,node-version,workflow-status-functions,shard-attestation,published-files,engine-double-contract,resume-authority-declared,merge-driver,spec-parsed-alias.TypeScript Type Check job:
type-check-coverage,driver-conformance,stall-guard, spectsc --noEmit, speccheck:generated --reconcile-only,skill-docs,spec-changes,upgrade-guide,authorable-surface,docs,skill-refs,skill-frame-sync,skill-compatibility,react-blocks,type-check-debt,api-surface,exported-any,dual-source-exports,skill-examples, lintdoc-formula-expressions,i18n,i18n-coverage.Check Changeset job:
check-empty-changeset.mjs(self-test + base),check-adr-0087-registration.mjs(self-test + base),check-changeset-no-major.mjs.Two of these needed a build before they said anything, and both reported the prerequisite honestly rather than passing:
check:i18n/check:i18n-coverage("PREREQUISITE NOT MET — the workspace CLI is not built" / "COULD NOT MEASURE"), andcheck:type-check-debt, which reported@objectstack/spec-monorepodrifting 80 -> 84. That last one was checked rather than assumed: 84 with this change and 84 with it reverted, so it is not this diff — it was an unbuilt-tree artifact, and afterturbo run build --filter='./packages/*' --filter='./packages/*/*'the gate passes. (tsc --showConfigalso confirms the root program hasallowJsoff and does not includescripts/*.mjsat all, so no TypeScript program reads this file.)Why
skip-changesetOne file changed,
scripts/check-adr-0087-registration.mjs— a zero-dependency CI gate script that no workspace package imports and that ships in no published tarball. It releases nothing, so it writes no changeset and needs theskip-changesetlabel.Generated by Claude Code