From 7ccd261981760da457fb14d2179dad881ca8ef54 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 06:27:22 +0000 Subject: [PATCH] =?UTF-8?q?docs(adr),chore(devx):=20tombstone=20ADR-0107?= =?UTF-8?q?=20=E2=80=94=20withdrawn,=20number=20retired=20(#6676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/adr/ ran 0106-* straight to 0108-*. Archaeology on full history (9,829 commits, clone verified unshallowed) settles which of the three cases it is: the number was DRAFTED AND WITHDRAWN, not lost and not never-assigned. 53d37f1ae (#3716) 2026-07-28 00:25 +0800 adds 0107-hook-body-write-set- accepted-static-gap.md, Proposed 3bb382b67 (#3735) 2026-07-28 09:24 +0800 owner decision deletes it; #3700 closed as not planned Nothing else ever claimed the number: `git log --all --diff-filter=AD -- 'docs/adr/0107*'` returns exactly those two commits, `-S'ADR-0107'` five (both of the above, the two later gate commits, #6634), `-S'0107' -- docs/` the same two. No restoration is wanted — and one would be actively wrong: the withdrawn record's D2 declared "no heuristic source analysis" a PERMANENT posture, and c1d44f7dc (#4271, 2026-07-31) reversed exactly that, shipping the parser-based `hook-body-write-unknown-field` lint. Its subject says so: 从 accepted gap 变 为作者时 lint 告警. So: a tombstone, recording the withdrawal, the later reversal, and the archaeology, so the next reader does not redo it. The gate edit is forced by the tombstone, not scope creep. #6634 had grandfathered the bare number onto `UNRESOLVED_ADR_CITATIONS`, which is audited in BOTH directions — a number that gains a record fails the entry as stale. Removing it is the (a) remedy the gate's own dangling-citation message recommends over the allowlist, and it is strictly stronger: the entry would have expired on its own once the withdrawal changeset is consumed by a release (direction B: "nothing cites it any more"), quietly re-freeing the number, whereas a record makes re-use collide in the number-uniqueness audit. Reverse-verified in both directions, predicted before running: - tombstone removed, entry stays removed -> RED, "ADR-0107 is cited by 3 file(s) but names no record", fix (a) named in the message - tombstone kept, entry restored -> RED, "entry for 0107 is stale — docs/adr/ now HAS a record" Gates: check:adr-anchors OK (119 decision numbers, 20916 citations resolve), check:adr-links OK (532 destinations), check:nul-bytes OK, check:doc-authoring OK (375 files), check:empty-changeset OK, eslint clean. No changeset: docs/adr/** and scripts/** ship in no published package, so this releases nothing — the `skip-changeset` label is the documented route and an empty-frontmatter changeset is rejected by check:empty-changeset (#5471). Fixes #6676 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn --- ...ithdrawn-hook-body-write-set-static-gap.md | 96 +++++++++++++++++++ scripts/check-adr-anchors.mjs | 46 ++++++--- 2 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 docs/adr/0107-withdrawn-hook-body-write-set-static-gap.md diff --git a/docs/adr/0107-withdrawn-hook-body-write-set-static-gap.md b/docs/adr/0107-withdrawn-hook-body-write-set-static-gap.md new file mode 100644 index 0000000000..6948c37bf5 --- /dev/null +++ b/docs/adr/0107-withdrawn-hook-body-write-set-static-gap.md @@ -0,0 +1,96 @@ +# ADR-0107: Withdrawn — this number is retired and must not be reused + +**Status**: **Withdrawn (2026-07-28)**. This file is a *tombstone*, not a decision: nothing in it is in force, and the number must never be reassigned. +**Deciders**: ObjectStack Protocol Architects (the withdrawal was an owner decision) +**The record that held this number**: *"The Hook-Body Write Set Is an Accepted Static-Analysis Gap"* — `docs/adr/0107-hook-body-write-set-accepted-static-gap.md`, Status *Proposed (2026-07-27)* +**Landed by**: [#3716](https://github.com/objectstack-ai/objectstack/pull/3716) — `53d37f1ae`, 2026-07-28 00:25 +0800 +**Withdrawn by**: [#3735](https://github.com/objectstack-ai/objectstack/pull/3735) — `3bb382b67`, 2026-07-28 09:24 +0800, nine hours later +**Tracking**: [#3700](https://github.com/objectstack-ai/objectstack/issues/3700) (the proposal this record routed — closed as **not planned**), [#6676](https://github.com/objectstack-ai/objectstack/issues/6676) (this tombstone) +**Consumers**: none. No code is governed by this number, and none may be — see [Do not anchor to this number](#do-not-anchor-to-this-number). + +--- + +## TL;DR + +A real record occupied ADR-0107 on `main` for nine hours on 2026-07-28 and was then +deleted by an owner decision. It was **withdrawn, not lost**: the deletion was +deliberate, its reasoning is in the withdrawing commit, and no restoration is wanted. + +This file exists so the number resolves to that explanation instead of to nothing, and +so it is never handed to an unrelated decision. Reassigning it would retroactively +re-point every historical "ADR-0107" — in commits, issues, the audit record and the +withdrawal changeset — at a document its author never meant. That is the squat failure +mode [ADR-0079](./0079-record-display-name.md)'s reconstruction was written for +([#6634](https://github.com/objectstack-ai/objectstack/issues/6634)), where one number +had silently accumulated 77 citations it did not resolve. + +**A new record takes the next free number. Not this one.** + +## What happened + +| When (UTC+8) | What | Evidence | +|---|---|---| +| 2026-07-27 | Decision taken on [#3700](https://github.com/objectstack-ai/objectstack/issues/3700): accept the hook-body write-set gap and mark it (option (a) of three) | audit §5 D4 | +| 2026-07-28 00:25 | ADR-0107 written and merged, Status *Proposed* | `53d37f1ae` ([#3716](https://github.com/objectstack-ai/objectstack/pull/3716)) | +| 2026-07-28 09:24 | Owner reverses course: the record and its changeset are deleted, #3700 closed as *not planned* | `3bb382b67` ([#3735](https://github.com/objectstack-ai/objectstack/pull/3735)) | +| 2026-07-31 14:25 | The withdrawn record's central posture is **reversed** — see below | `c1d44f7dc` ([#4271](https://github.com/objectstack-ai/objectstack/issues/4271)) | +| 2026-08-08 | The bare number is grandfathered onto `check-adr-anchors`'s citation allowlist, pending this tombstone | [#6634](https://github.com/objectstack-ai/objectstack/issues/6634) | + +The withdrawing commit states the reasoning in full. In short: the record's whole +content was *that the gap is documented*, and the documentation — `hook-bodies.mdx`, +`hooks.mdx`, the `ScriptBodySchema` TSDoc — is itself the entire disposition. A +decision record whose only claim is "the docs say so" earns nothing and adds an +apparatus to keep in sync, so it went and the docs stayed. The structured `writes` +declaration it had routed as a deferred proposal was dropped outright at the same +time: it would duplicate the write set in a second place the author must keep +synchronized — worst of all for AI authors — while exercising hooks against a SQL +driver already surfaces the failure mode schemaless `memory://` green-lights. + +## Do not resurrect it + +Beyond being withdrawn, the record's substance is now **contradicted by shipped code**, +so restoring the text would plant a false statement in the decision log. + +Its D2 declared "no heuristic source analysis" a *permanent posture, not a deferral* — +that the write set would never be regex- or AST-guessed out of a Turing-complete body. +Three days after the withdrawal, [#4271](https://github.com/objectstack-ai/objectstack/issues/4271) +(`c1d44f7dc`, 2026-07-31) did exactly that: `validateHookBodyWrites` in +`@objectstack/lint` parses `body.source` and resolves its literal writes against the +target object's fields, warning `hook-body-write-unknown-field` with a did-you-mean. +That commit's own subject records the reversal — "从 accepted gap 变为作者时 lint 告警". + +The live account of that surface is `content/docs/automation/hook-bodies.mdx` +("Write-set checking"), which states the current bounds directly, including that a +missing warning is not a clean bill of health. Read that, never this file, for what +is true today. + +## Do not anchor to this number + +`scripts/check-adr-anchors.mjs` requires every `ADR-NNNN` cited in a tracked file to +name a record under `docs/adr/`. This tombstone satisfies that check — deliberately, +because the historical citations below are legitimate references to the withdrawal. +It is **not** a licence to cite ADR-0107 as governing anything: an anchor entry must +state the invariant its ADR decided, and this number decided nothing. + +The citations that exist today all discuss the withdrawal itself: + +- `.changeset/withdraw-adr-0107-drop-writes-proposal.md` — the changeset that withdrew it; +- `docs/audits/2026-07-app-metadata-reference-integrity-assessment.md` §5 D4 — the audit whose open decision it recorded, which carries the revision; +- `scripts/check-adr-anchors.mjs` — the gate, describing this case. + +## Archaeology (2026-08-10, #6676) + +Recorded so the next reader does not repeat it. Run against full history +(9,829 commits, clone unshallowed — a shallow clone silently answers "never existed"): + +```bash +git log --all --diff-filter=AD -- 'docs/adr/0107*' # -> exactly 2 commits, both above +git log --all --oneline -S'ADR-0107' # -> 5 commits, all accounted for +git log --all --oneline -S'0107' -- docs/ # -> the same 2 commits +``` + +**Nothing else ever claimed this number**, on any branch, at any time. There is no +second era of "ADR-0107" and no lost content: the withdrawn text is recoverable in +full at `git show 53d37f1ae:docs/adr/0107-hook-body-write-set-accepted-static-gap.md`, +and is kept in history rather than here on purpose — a withdrawn record reprinted +inside its own tombstone reads as a record. diff --git a/scripts/check-adr-anchors.mjs b/scripts/check-adr-anchors.mjs index be9e62bb07..63fc865a0d 100644 --- a/scripts/check-adr-anchors.mjs +++ b/scripts/check-adr-anchors.mjs @@ -96,11 +96,18 @@ // the `.vN` version rule above and unlike an allowlist — an author who cites // a sibling repo has a spelling that is both correct to a reader and clean // to the gate. Bare `ADR-0001`, meaning ours, still fails. -// - **A record that was withdrawn or deleted, cited as history.** ADR-0107 was -// withdrawn before it landed (#3735) and is cited by the changeset that -// withdrew it. Those numbers sit on `UNRESOLVED_ADR_CITATIONS` below — an -// explicit, shrink-only allowlist, audited in both directions like the -// collision list. +// - **A record that was withdrawn or deleted, cited as history.** ADR-0001's +// record was deleted in the 2026-02-11 permission-protocol rewrite and is +// cited as history by ADR-0002. Those numbers sit on +// `UNRESOLVED_ADR_CITATIONS` below — an explicit, shrink-only allowlist, +// audited in both directions like the collision list. +// +// It is the WEAKER of the two remedies, and the list says so: preferred is +// to give the number a record, even when the record is a tombstone. ADR-0107 +// left this list that way (#6676) — withdrawn nine hours after it landed +// (#3735), it now has `docs/adr/0107-withdrawn-*.md` saying so, which both +// resolves the historical citations and makes re-use collide loudly under +// the number-uniqueness audit instead of merely going stale here. // // ## Where the registry lives (#6957) // @@ -222,14 +229,15 @@ const UNRESOLVED_ADR_CITATIONS = [ // 404. This entry never blessed that link and does not bless any future one. why: 'record deleted 2026-02-11 (9da8e3e72); cited as history by ADR-0002', }, - { - number: '0107', - // Withdrawn before it landed: #3700 was closed as not planned and 3bb382b67 - // (#3735) deleted the record. Cited by the changeset that withdrew it, by - // the audit whose D4 it recorded, and by this file's own comment above — - // all three discussing the withdrawal itself. - why: 'record withdrawn 2026-07-28 (#3735, 3bb382b67); cited by the withdrawal changeset and audit', - }, + // 0107 was the second entry until #6676. It is gone from this list because the + // number gained a record — `docs/adr/0107-withdrawn-hook-body-write-set-static- + // gap.md`, a tombstone. That is the (a) remedy the dangling-citation message + // recommends over this list, and it is strictly stronger here: an allowlist + // entry survives only as long as something still cites the number (the + // direction-B staleness rule below), and 0107's citations are a changeset + // awaiting release plus an audit, so the grandfather clause would have expired + // on its own and quietly re-freed the number. A record does not expire, and a + // re-use now collides in `auditAdrDirectory` — loud, and about the right fact. ]; /** @@ -588,8 +596,16 @@ for (const entry of anchors) { errors.push(`${shard}: "${adr}" is not an ADR id (expected e.g. ADR-0090).`); continue; } - // Anchoring a withdrawn or never-written record sends the next author to a - // dead end (cf. ADR-0107, withdrawn before it landed). + // Anchoring a never-written or deleted record sends the next author to a + // dead end (cf. ADR-0001, whose record was deleted in the 2026-02-11 + // permission-protocol rewrite and never restored). + // + // ⚠️ This resolves against the FILENAME only, so a tombstone — a record whose + // whole content is "this number is withdrawn, do not reuse" — reads here as a + // decision that exists (#6676 added the first, ADR-0107). Anchoring code to + // one is not caught mechanically; what catches it is the `invariant` field, + // which has to state what the ADR decided and cannot be written truthfully + // for a number that decided nothing. if (!records.has(m[1])) { errors.push(`${shard}: ${adr} has no record under ${ADR_DIR}/ — anchor a decision that exists.`); continue;