You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(devx): the clause-② declaration grows a direction arm, and three gates read it (#18346)
Fixes#16421
Clause-②: no
A gate strengthens; no published runtime contract moves. Stated by the
ruling, and re-derived here: the declaration's accept set only grows
(the arm is optional and every existing spelling reads
byte-identically), and the three gates that read it become stricter,
never looser.
## The defect
A **narrowing** — "these spellings stop being accepted" — could ship to
customers with the release notes saying nothing, and every gate green.
`check-adr-0087-registration.mjs` decided breaking-ness from an author's
hand-written `**BREAKING**` prose banner; an author who did not type it
was classified non-breaking and was asked for no ADR-0087 disposition.
Measured, not hypothesised: **#16296** narrowed `sys_job.timezone` /
`sys_report_schedule.timezone` to the IANA value domain and shipped with
no banner, no `major` and no `!`. **#14238** did the same one surface
earlier.
## The ruling being executed
Director summon #17, decision batch #2 item 1, **option B**, maintainer
verbatim 「同意」 (comment `5572145955`, 2026-09-07T14:32:32Z). Options A
(keep the prose banner) and C (a static accept-set differ) are refused
there and are not reopened here.
## What landed
| # | deliverable | where |
| --- | --- | --- |
| 1 | the closed two-arm enumeration in the declaration format |
`AGENTS.md` item 3 |
| 2 | `readClause2Line` reads the arm — **once**, no second parser |
`scripts/pm/check-clause2-carriers.mjs` |
| 3 | signal (4): a declared `narrowing` is breaking |
`scripts/check-adr-0087-registration.mjs` |
| 4 | the level axis reads the arm |
`scripts/check-changeset-no-major.mjs` |
| 5 | **ONE** ADR-0087 ledger row, recording #16296 |
`packages/spec/src/migrations/entries/semantic/` |
> ⚠️ **Row 5 is deliberately one row, not the two the ruling names —
corrected by the `domain:devx` seat (#6023) on the dev's measurement.**
> The ruling lists #14238 and #16296 as the two already-shipped
narrowings. **#14238 is not a narrowing**: its diff is 535 insertions
and 0 deletions across 11 files (two nullable columns ADDED, carrying
`valueDomain: 'iana_time_zone'` from their first release), and its own
shipped changeset says so in writing — *"A NON-BREAKING ADDITION,
registered here in writing because ADR-0087's registries have no
additive entry kind"*.
> A row claiming it narrowed something would be false data in the one
ledger this mechanism keeps true, which ADR-0087's #13080 addendum
refuses by name. ⇒ One row is written, the second is **not fabricated**,
and the falsified premise is carried to the maintainer rather than
silently dropped.
> ⚠️ **The citation above was wrong in the dispatch order and is
corrected here.** The seat's dispatch word cited comment `5573372199`,
which answers **HTTP 404**; the real ruling is `5572145955` (HTTP 200,
verified by both the dev and the seat). The quoted ruling text itself is
unchanged — only the provenance id moved.
| 6 | pins for both arms, in both directions | the three gates'
self-tests |
### The shape
| spelling | reading |
| --- | --- |
| `Clause-②: yes` | a widening, declared through the value — unchanged |
| `Clause-②: yes (widening)` | the same, said out loud |
| `Clause-②: yes (narrowing)` | widens one surface, narrows another;
both facts are read |
| `Clause-②: no (narrowing)` | NOT a widening, but BREAKING — the point
of the arm |
| `Clause-②: no (widening)` | MALFORMED: the value and the arm
contradict each other |
(The rows are table cells on purpose: a line-initial second declaration
in this body would be a second answer to the same question.)
The arm is **optional**, and that is a measurement rather than a
kindness: all five declarations on the open board the day this landed
carry no parenthetical arm, and all five read byte-identically
afterwards (proof below).
Two shapes fail **closed** rather than reading as an absent arm, because
reading them as absent is the direction a declared narrowing silently
disappears in: a near-arm spelling (`(narrowed)`, `(Narrowing)`,
`(widen)`) and the unfilled template `(widening|narrowing)`. Both land
in `malformed`, the state this reader already owns for "the slot holds
something nobody can grade".
## Measurements
### Back-compat — the five in-flight declarations
Read from the live GitHub payload, before and after the change,
comparing the whole reading object:
| PR | reading | arm |
| --- | --- | --- |
| #18344 | `declared no` — IDENTICAL | none |
| #18268 | `declared no` (em-dash reasoning, parenthesised identifiers)
— IDENTICAL | none |
| #18231 | `near-miss inline-key` — IDENTICAL | n/a |
| #18227 | `declared no` — IDENTICAL | none |
| #18096 | `declared no` — IDENTICAL | none |
**5/5 byte-identical** apart from the additive `arm: null` field.
Nothing in flight loses its reading.
### `AGENTS.md` — both ratchet axes
Its line ratchet has **zero headroom** (1075 lines, ceiling 1075), so
the format is carried at **net zero lines**, paid for by deleting
content rather than by re-wrapping:
- deleted: the widening-rationale clause (`the widening it declares is
what makes it more than a patch…`), 93 bytes;
- deleted: the CI step name the gate already prints in its own failure,
78 bytes.
| axis | before | after |
| --- | --- | --- |
| lines | 1075 / ceiling 1075 | **1075** — headroom 0, unchanged |
| widest line | ≤ 120 bytes | edited lines measure 119 / 118 / 120 / 119
/ 81 bytes |
| widest table row | 768 / pin 768 | **768**, untouched |
`check:pm-skill-ratchet` self-test and run both exit 0.
### Both arms, both directions
Every arm pin is a **pair**, because one direction alone cannot tell a
reading from a constant — a gate that classified both arms as breaking
would satisfy a `narrowing` test while telling a consumer nothing:
- `check-adr-0087-registration`: `narrowing` is in
`MUST_MATCH_BREAKING`, `widening` is in `MUST_NOT_MATCH_BREAKING`, both
on a banner-free `minor` changeset — the exact shape #16296 shipped in.
- `check-changeset-no-major`: same tree, same levels, only the
declaration moves — `no (narrowing)` reaches `enforce`, bare `no`
reaches `not-declared`.
- `check-clause2-carriers`: both arms read; the contradiction, the
near-arm family and the unfilled menu are all `malformed`; and three
CONTROL rows pin the arm-less spellings unmoved.
## Two judgement calls, declared rather than made quietly
1. **The `**BREAKING**` banner signal is KEPT.** The dispatch word asked
for breaking-ness to stop being read from the prose banner. Removing
signal (2) would un-declare the **52 changesets in stock** that carry it
and nothing else — the gate's own docblock has refused that direction
("narrowing to any one of them would drop real declarations") since it
was written, and the ruling's own text asks only that the gate *read the
arm*. Signal (4) is added beside it, so breaking-ness no longer
**depends** on prose. If the maintainer wants (2) retired, that is a
second, deliberate card with its own stock count.
2. **`check-adr-0087-registration` reads the arm from the CHANGESET
body, not the PR body.** It has no pull-request payload by construction
— `cut-rc.yml` runs it on a `workflow_dispatch` over a whole snapshot
range with no PR at all. All three of its existing signals are
changeset-local, and the fourth is too.
## Acceptance notes
- noted, not filed: `readClause2Correction` does not carry the arm. A
correction comment can change a declaration's *value* but not its
*direction*. No half-state results today — the arm's three readers are
the changeset body (ADR-0087), the PR body (no-major) and this file's
own rows, and none of them reads corrections — so this is recorded
rather than filed. Carrier: `scripts/pm/check-clause2-carriers.mjs`, the
next card that touches `readClause2Correction`.
- noted, not filed: the I1/I2 fixture in
`check-adr-0087-registration.mjs` staged its import siblings from a
hand-written two-name list. Signal (4) added an edge into a nine-module
closure including `pm/dispatch-gates.mjs`, so the list is now
**derived** by walking real import statements. That is a guard extension
inside this card's own defect class, not a drive-by: without it, the
next import added over in `pm/` would kill this gate's fixture with an
error about neither file.
## 维护者速读(草稿)
**改了什么** —「本卡是否放宽契约」这条申报,现在可以再带一个方向:`(widening)` 或
`(narrowing)`,只有这两种拼法。声明 `(narrowing)` 的改动会被三个门禁当作破坏性变更处理:必须写 ADR-0087
处置、必须给到 `minor` 档。`AGENTS.md` 记下格式,行数一行没多。
**为什么改** — 一次「以后不再接受某些写法」的收紧,过去可以在发布说明里一个字都不提就发给客户,所有门禁照样亮绿:门禁是从作者手写的
`**BREAKING**` 横幅判断破坏性的,忘了写就当没有。#16296 和 #14238
就是这么发出去的。改完之后,判据是一个封闭的词,不是一段散文。
**风险与代价(含回滚)** — 风险面是「在飞的申报会不会一夜失效」。方向词是可选的,板上 5 个在飞 PR
的申报实测全部逐字不变,已列在上面的表里。代价是作者多写一个词,并且要把同一行抄进 changeset
正文。两条已申报的判断:`**BREAKING**` 横幅这条旧信号保留(库存里 52 个 changeset 只靠它),以及 ADR-0087
门禁从 changeset 正文读方向(它跑 RC 切版时根本没有 PR)。回滚是单笔
revert:三个门禁各自的自测都是新增用例,没有既有用例被改写。
**席位意见** — (留空,复核席定稿)
**你要做的** — 确认两件事:① 旧的 `**BREAKING**`
横幅信号保留,是否合你意(裁定原文只说「读分肢」,派发词的转述是「不再从横幅判断」);② 两条 ADR-0087
账本行记的是平台对象列的取值域收紧 —— 该账本服务的是元数据升级者,请确认这两行落在那里是你要的记录位置。
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: claude[bot] <noreply@anthropic.com>
0 commit comments