Commit 46559f6
fix(gate): one discriminator for the ADR-0087 label-vs-mention decision, both directions pinned (#18933)
Fixes #18494
Fixes #18493
Clause-②: no
Two cards, one predicate: `findMigrationPrescription`'s label-vs-mention
decision in
`scripts/check-adr-0087-registration.mjs`, misclassifying in
**opposite** directions. They are
folded because the repairs are **coupled**, not because they are
adjacent — see "The coupling,
measured" below, which is the reason this is one PR and one battery.
- **#18494** — a real **label** read as a **mention**, so a changeset
shipping **no rewrite** was
admitted under `not-required (no-migration-prescription)`. Too
permissive; the cost is a
published break reaching consumers with neither a prescription nor a
ledger entry.
- **#18493** — a **mention** read as a **label**, so a concrete rewrite
was demanded that is not
owed. Too strict; the cost is a hard-blocked PR whose evidence line
names the author's own
sentence.
## What changed
Commit 1 (#18494) — `LABEL_COLON_RE`, and one arm in `labelPositioned`.
The in-line arm reads the
ONE character LEFT of the placeholder (`GOVERNING_WORD_RE`), which is
structurally blind to a label
whose own words are a noun phrase: `**Schema Migration FROM → TO:**
delete the block` has an
ordinary letter there and is a label all the same. The signal the left
side cannot supply is on the
right — the convention's own colon, which a running sentence never puts
on its object. The class is
derived from the corpus, not invented: over 1322 tracked `md`/`mdx` at
`625db0e85` (1007
occurrences), 321 are closed by a colon and the markup that may stand
between the placeholder and
that colon is exactly three runs — nothing (295), a closing paren (25)
and a bold marker (1). The
full-width colon rides along as this file's ordinary Chinese parity at a
measured population of
zero, on the precedent `REMOVAL_INSTRUCTION` states for its own Chinese
arm. Asked AFTER the
heading arm, so punctuation cannot overturn a denial (#17357).
Commit 2 (#18493) — two failures of the #17864 transparency rule to
compose:
1. `FRAMING_TAIL_RE` carried **no word anchor** while its sibling
`MIGRATION_FRAMING_RE` anchors
its English alternatives, so a framing word that is merely the tail of
an **identifier** was
stripped as a word of its own: `the sys_migration FROM → TO is
documented elsewhere` stripped to
`the sys_`, exposed an underscore instead of the letter that was really
there, and answered
"nothing governs this". The anchor is a **negative lookbehind** rather
than a consuming
boundary, because the match is fed to `String.replace` and a consumed
delimiter would be deleted
with the word.
2. The in-line arm is computed within ONE line, so a hard wrap put the
governor out of its reach in
both directions. A prefix that is **entirely** a framing word now falls
through to the
cross-line arm — transparency means the word is not there, so the
placeholder opens its line —
and the cross-line arm strips a framing tail off the line above before
asking
`WRAPPED_GOVERNOR_RE`.
## The reversal clause — #18493's half admits inputs the gate refuses
today
That is legitimate only as the removal of a **mis-refusal**, and the
gate's own docblock on
`withoutFramingTail` said so before the anchor existed, in these words:
> The identifier shape is a SEPARATE false positive, stated rather than
hidden and out of this
> repair's reach: `the sys_migration FROM → TO is documented elsewhere`
is governed by `the` and
> still reads as a label, because what the strip exposes is `_` rather
than the letter that was
> there.
and, on the wrap, `labelPositioned`'s own statement of the rule:
> A FRAMING word between the governor and the placeholder is
transparent, and transparency is a
> claim in BOTH directions (#17864). It does not make an occurrence a
label — `the Migration FROM →
> TO is documented elsewhere` is the same mention as `the FROM → TO is
documented elsewhere`, one
> framing word apart
Nothing beyond those documented mention shapes is admitted: the change
adds no vocabulary and no
new exemption, it composes two rules the file already states.
Counter-evidence for that claim is
the residue, stated in the docblock rather than reached for — a framing
word after a hyphen or a
dot is still stripped, exactly as a word boundary reads it. Measured
over the same tree, the
population of EVERY in-line framing tail, of any delimiter, is **0**, so
no reading here separates
those classes, and widening what the gate ADMITS on an unmeasured shape
is the maintainer's floor.
Filed as a finding instead.
## Per-card criteria, each demonstrated independently
Through the **real CLI** on a **committed** probe changeset in a temp
repo, with the gate's
pre-change blob and its post-change blob staged into the same fixture,
reading the process exit
code (`base` = `625db0e85`, `head` = this branch):
| case | base | head |
|:---|:---|:---|
| **#18494** probe: `**Schema Migration FROM → TO:** delete the block`,
no concrete rewrite | exit **0** | exit **1**, `Evidence
(from-to-label)` naming that label |
| #18494 control: the same sentence with no colon (the #17864 mention) |
exit 0 | exit 0 |
| **#18493** T1 mention: `the sys_migration FROM → TO is documented
elsewhere` | exit **1** | exit **0** |
| **#18493** T1 label FLOOR: `sys_migration FROM → TO: delete the block`
(F9/R2) | exit 1 | exit **1** |
| **#18493** T2 W1: governor ends the line, framing word opens the next
| exit **1** | exit **0** |
| **#18493** T2 W2: governor and framing word above, placeholder below |
exit **1** | exit **0** |
#18494's card lists six label shapes, all reading `null` on current
main; the whole list was
treated as the target set and all six now read `from-to-label`, as do
the card's own three fairness
controls (a noun-phrase label with no framing word in it at all — the
same defect one word
shorter, which this discriminator cannot and should not separate).
## The coupling, measured
Word-anchoring `FRAMING_TAIL_RE` takes the gate's **own F9 floor** with
it —
`sys_migration FROM → TO:` stopped being a label by an accident of the
strip — unless the colon
that actually closes that label is holding it up. That is not an
argument; it is ablation A below,
where removing the #18494 arm reds **F9**, a pin neither card touches.
Reverse verification, each leg through `scripts/ablation-replace.mjs`
(anchor must hit, on-disk
counts and blob hashes asserted, restore proven by blob-equals-HEAD and
an empty `git diff HEAD`):
| ablation | what is removed | self-test |
|:---|:---|:---|
| A | the colon arm | **15 red**: L1-L7, L-E2E-R, X2, SS1, SS3, SS7 —
and **F9**, the pre-existing floor |
| B | the identifier word anchor | **3 red**: X1, SS4, SS8 |
| C | the entirely-framing fall-through | **2 red**: X4, X10 |
| D | the wrap-side framing strip | **2 red**: X5, SS5 |
Anti-see-saw: `SS1-SS8` pins the two directions as **minimal pairs in
ONE battery** under **one**
floor — each pair differs by one character or one line break and asserts
opposite verdicts, so a
tuning that buys one direction with the other reds a pair, and deleting
half the pairs to silence
that drops the battery below its pinned count. `L1-L13` and `X1-X11`
carry each card's own reds and
floors.
## The gate I am editing judges this PR
Before: `exit 0`, self-test `405` assertions. After: `exit 0`, self-test
`441` assertions — the
battery grew by 36 and shrank nowhere.
## Gates
Derived from the change set by `node scripts/pm/dispatch-gates.mjs
--repo objectstack-ai/objectstack`
(1 path, three-dot semantics, merge base `d5ccfb4e3`), each run with its
exit code redirected to a
file and read back, then reconciled with `--ran`: **32 derived, 32 run,
0 NOT-MEASURED, 0 UNRUN,
all exit 0** — a derived zero, not a claimed one, and no family exited
3.
Stock measurement, the one #18494's second direction owes: over **429**
changesets and **1322**
tracked `md`/`mdx` at `625db0e85`, **zero** verdict deltas for the whole
change, with the
before/after instrument **lit 3/3** by the three probe shapes. `pnpm
lint` narrowed and the
narrowing declared: 1 of the 6855 files eslint's own config selects, 0
errors 0 warnings, exit 0
(`--format json`); the config states that this repo "never enables
type-aware linting
(no `parserOptions.project`, no typed rules) for ANY file", so a
one-file diff cannot move the
verdict on a file it did not touch. Readings taken at `b0678d370`.
`origin/main` merged once before
this PR opened.
## Acceptance notes
Noted, not filed — observations, no repro, no contract text:
- `MIGRATION_FRAMING_RE`'s docblock names `migrations/registry.ts` among
the identifiers its word
anchor keeps out, but a word boundary treats a slash as a boundary too,
so that one alternative
of the claim does not hold as written. Successor: the next PR that
touches that docblock.
- The in-line framing-tail path has population 0 over the whole corpus,
in every delimiter class,
so both arms repaired here are pins against a future author rather than
against today's stock —
exactly as both cards state.
## 维护者速读(草稿)
**改了什么** —— 一个门禁的判别器:changeset 正文里的 `FROM → TO` 到底是「这里就是改写清单」(标签)
还是「改写清单在别处」(提及)。两张卡是同一个判断的两个反方向,合成一个 PR 修:一边放过了不带改写
的破坏性变更,一边拦住了本不该拦的作者。
**为什么改** —— 放过的那一边是真成本:破坏性改动进了消费者手里,既没有改写说明,也没有登记入账,而
门禁报的是一次干净的运行。拦错的那一边是摩擦成本:作者被要求补一份他不欠的改写,而门禁给出的「证据」
就是他自己那句话。两边不能分开修——收紧一边会把另一边推得更远,这份 PR 的实测把这条耦合摆出来了。
**风险与代价(含回滚)** —— 全量实测:429 份 changeset、1322 个 md/mdx 文件,判定零变化,仪器三/三
点亮;门禁自身 32 族全绿。回滚按**倒序**:先回退第二个 commit(#18493)、再回退第一个(#18494)。
1 parent f112a74 commit 46559f6
1 file changed
Lines changed: 342 additions & 21 deletions
0 commit comments