Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions docs/adr/0107-withdrawn-hook-body-write-set-static-gap.md
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 31 additions & 15 deletions scripts/check-adr-anchors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
//
Expand Down Expand Up @@ -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.
];

/**
Expand Down Expand Up @@ -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;
Expand Down
Loading