Skip to content

feat(devx): the clause-② declaration grows a direction arm, and three gates read it - #18346

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-16421-clause2-arms
Sep 16, 2026
Merged

os-zhuang merged 4 commits into
mainfrom
claude/issue-16421-clause2-arms

Conversation

@os-try-charles

@os-try-charles os-try-charles commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

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 feat(platform-objects): sys_job.timezone and sys_report_schedule.timezone are validated against the IANA domain #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

Generated by Claude Code


Generated by Claude Code


Generated by Claude Code

… gates read it

A narrowing — "these spellings stop being accepted" — could ship to consumers
with every gate green. `check-adr-0087-registration.mjs` decided breaking-ness
from a hand-written `**BREAKING**` prose banner, so an author who did not type
it was classified non-breaking and owed no ADR-0087 disposition. Measured on
#16296 (and #14238 one surface earlier), both of which shipped that way.

The declaration now carries a closed direction arm — `(widening)` or
`(narrowing)` — read once, in `readClause2Line`, and imported by every reader:

* `scripts/pm/check-clause2-carriers.mjs` — `CLAUSE2_ARMS`, the arm reader, and
  the two shapes that must fail closed: a near-arm spelling (`(narrowed)`, the
  unfilled `(widening|narrowing)` menu) and the self-contradiction
  `no (widening)`. Both read `malformed`, never as an absent arm.
* `scripts/check-adr-0087-registration.mjs` — signal (4). `narrowing` declares
  breaking; `widening` declares nothing. The `**BREAKING**` banner is KEPT: 52
  changesets in stock carry it and nothing else.
* `scripts/check-changeset-no-major.mjs` — a declared narrowing owes the same
  grade a declared widening owes, because inside the launch window a breaking
  change ships `minor`.

The arm is OPTIONAL, and that is a measurement: all five declarations on the
open board read `Clause-②: no` with no parenthetical, and all five read
byte-identically after this change.

AGENTS.md carries the format at net zero lines — its ratchet has no headroom —
paid for by deleting the widening-rationale clause and the CI step name the
gate prints for itself.

Maintainer ruling: director summon #17, decision batch #2 item 1, option B,
verbatim 「同意」.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Sep 16, 2026
…7 ledger

`#16296` gave `sys_job.timezone` and `sys_report_schedule.timezone` the
`valueDomain: 'iana_time_zone'` declaration — a write-time narrowing that
shipped with no breaking-change marker, so the repo's detector classified it
non-breaking and asked for no ADR-0087 disposition.

The ledger now carries a `semantic` entry for it, protocol 18, stating what
narrowed and the one-line fix per offending row. Nothing is re-released and
nothing is ratified in silence.

⚠️ The ruling named TWO narrowings. The second, `#14238`, is NOT one: its diff
is 535 insertions and 0 deletions across 11 files — two nullable columns ADDED,
carrying the value domain from their first release — and its own changeset says
so in writing ("A NON-BREAKING ADDITION ... 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 refuses by name. The
finding is reported to the seat rather than written into the ledger.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1411cf2c6fd2c9e695682416c9e655869945f531packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0f472bf23dfd1ee7789687ada28d6b6d6705661b — the merge of head d4d5fe9aa58c925ba95633476c146fbbf5fc1356 into base 1411cf2c6fd2c9e695682416c9e655869945f531, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0f472bf23dfd1ee7789687ada28d6b6d6705661b && git checkout 0f472bf23dfd1ee7789687ada28d6b6d6705661b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1411cf2c6fd2c9e695682416c9e655869945f531 d4d5fe9aa58c925ba95633476c146fbbf5fc1356 && git checkout -B drift-repro 1411cf2c6fd2c9e695682416c9e655869945f531 && git merge --no-ff d4d5fe9aa58c925ba95633476c146fbbf5fc1356

node scripts/docs-audit/affected-docs.mjs --json 1411cf2c6fd2c9e695682416c9e655869945f531

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…h, not typed

`check:objectui-changeset` went red on this branch, and the error named neither
cause: `ERR_MODULE_NOT_FOUND` for `fw-gate/scripts/pm/check-clause2-carriers.mjs`.
`objectui-changeset-digest.mjs`'s self-test copies `check-adr-0087-registration.mjs`
into a sandbox and runs it for real; that gate gained one import in this branch,
its OWN staging fixture was updated in the same edit, and this second one — a
hand-written two-name manifest in another file — was not.

⛔ The import is not removed: the ruling makes one parser a condition
("the arm is added there once, and every reader imports it; no second parser").
What is repaired is the copy surface.

`scripts/first-party-closure.mjs` derives a module's transitive first-party
closure from the same statement-shaped edges Node resolves, and every staging
site now calls it:

* `check-adr-0087-registration.mjs`'s own I1/I2 fixture;
* `objectui-changeset-digest.mjs`'s `fw-gate` sandbox;
* the SEVEN identical three-name manifests in that file's bump-driver sandboxes,
  now one `stageBumpDriver()`;
* `bump-objectui.selftest.sh`, a THIRD site the sweep found — its failure mode
  was worse than a missing module: the digest died as a subprocess and the
  driver reported the objectui RANGE as unwalkable, a true sentence about the
  wrong thing.

`bump-objectui.sh` stays named in the shell site: no import statement reaches a
shell script, so no module walk can find it. What is derived is what a walk can
answer.

Ruling citation corrected to #16421 comment `5572145955` (read back HTTP 200,
hotlong, 2026-09-07T14:32:32Z) in the ledger entry, the declaration reader's
docblock and the changeset. The id the dispatch carried answered 404.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
`check:pm-dispatch-gates` went red on the previous commit, and both failures are
registration gaps rather than logic: a new cross-module edge has three faces —
the copy surface, the declared path population, and the read key — and the last
commit repaired only the first.

⭐ Run it the way the tree already prescribes. `platform-readings.md` records
`check:pm-dispatch-gates` as exceeding the container's foreground cap and names
the remedy — detach, then wait with `tail --pid`. A timeout is not a reading,
and this gate has a recorded way to be read. Measured that way: 3 of 1730 cases
failed before, 1730/1730 pass after, 759s.

FACE 2 — the declared path population. `bump-objectui.selftest.sh` carries a
`no-path-population` marker whose own text promises that no quoted literal in it
names a tracked path. `DIGEST_ENTRY='scripts/objectui-changeset-digest.mjs'`
broke that promise: `dispatch-gates` reads a quoted literal carrying a separator
as a declared watched path, so the gate began naming paths while still declaring
it had none. The basename is spelled alone and the directory interpolated onto
it — the same idiom this file already uses for `CHANGESET_NAME`, and for the
same machine reason. ⛔ The declaration is not withdrawn and no case is relaxed.

FACE 3 — the read key. `dispatch-gates` derives "which gate does this family run
a copy of?" from an anchored `readFileSync` whose target resolves to a tracked
path; a loop variable, or a read performed inside another module, is deliberately
not followed. Folding the entry's staging into the derived walk therefore staged
it correctly and made it invisible. The entry is read by name again, and the walk
now stages only the DEPENDENCIES, so neither line is redundant with the other.

Also corrects a sentence this branch wrote about `bump-objectui.sh`: that script
swallows no stderr and its `WALK_RC` branch already separates a probe verdict
from a no-answer, refusing to offer `--unshallow` for a crash. The misleading
presentation lives in the self-test, and is carded as #18354.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk
@os-zhuang
os-zhuang marked this pull request as ready for review September 16, 2026 06:10
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 2fc092b Sep 16, 2026
37 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-16421-clause2-arms branch September 16, 2026 06:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation domain:devx size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] An accept-set narrowing owes a **BREAKING** banner in core but not in platform-objects — and the ADR-0087 classifier reads the banner

3 participants