Skip to content

docs(skills): retirement playbook §0 judges each family by mainstream capability - #20364

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20354-retirement-playbook-criterion
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20354-retirement-playbook-criterion

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20354
Clause-②: no

What changed

One rule text in .claude/skills/spec-property-retirement/SKILL.md §0. No new gate, no code, net 0 lines.

  1. The exemption bullet becomes the maintainer's criterion. The old bullet 「它是文档形状的吗?」 ended with 「良性展示元数据(description、tags、icon)永远谈不上「误导」;不要标 authorWarn,也不要退役它。」 It is replaced by one bullet that states the per-family criterion in the maintainer's own words and derives the documentation-shaped case from it:
    • header 「主流平台有没有这个能力?」 and body 「有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读」: both halves of the ruling sentence, quoted verbatim, with the ruling record cited as comment 5727134555;
    • documentation-shaped keys (hook.label, flow.description) record intent for the next reader (ADR-0033), and the text now calls them an instance of the 「有」 branch: render, do not retire. The verdict still goes into the ledger note so the next audit does not re-open it. It is one criterion's outcome, not a second rule;
    • description, tags and icon are no longer exempt by kind. The row-level-policy tags key is named as the 「没有」 example (mainstream platforms have no such attribute, so it retires);
    • whether to mark authorWarn now defers to the liveness README's author-warning rule, which is unchanged. The playbook no longer carries a second copy of it.
  2. One more line in §0 carried the rejected criterion. The bullet 「零编写实例」≠「没有代码读它」 ended 「后者才是退役的证据」, which says "nobody reads it" is the evidence for retirement. That is the in-repo-reader test the ruling rejects, and it would have sat eight lines under the new bullet and contradicted it. It now ends 「后者才证 dead」. An unread key is dead, and dead is what the criterion is asked about. It does not answer the criterion. Same defect class, same section, one line, 8 bytes shorter. It sits inside the claimed file surface (§0) and is named here so review can take or drop it separately.

Before (lines 43–47 at 8af914a3):

- [ ] **它是文档形状的吗?** `hook.label`、`hook.description`、`flow.description`
      没有运行时消费者,但被**有意保留** —— 它们为下一个读者(按 ADR-0033,常是模
      型)记录意图。把豁免写进台账 `note`,下次审计不再重审。良性展示元数据
      (`description`、`tags`、`icon`)永远谈不上「误导」;不要标 `authorWarn`,也
      不要退役它。

After:

- [ ] **主流平台有没有这个能力?** 声明而未执行的键每族问这句,按维护者原话(裁决评论
      `5727134555`):「有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读」。
      文档形状的键(`hook.label`、`flow.description`)为下一个读者记录意图(ADR-0033),是「有」的
      一例:补渲染、不退役,判定写进台账 `note`,下次审计不再重审。`description`、`tags`、`icon`
      不自动豁免:RLS 策略的 `tags` 主流没有 ⇒ 退役。`authorWarn` 另按 README 判。

Why

The maintainer's ruling on #18900 (comment 5727134555, item ④, A′), verbatim: 「18900 同意 每族该问的是:主流平台有没有这个能力 —— 有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读。」 The ruling record's own gloss is "a family is judged by whether mainstream platforms in the domain have the capability, ⛔ not by whether anything in this repo reads the key". Triage applies it to every family card. The playbook said the opposite for description / tags / icon, and a dev on the RLS family already hit the fork: #20353 retires a tags key the old text forbade. Triage direction (5862082626): "the ruling wins, and the text follows it". The documentation-shaped exemption is kept as the case where the mainstream carries the capability, which is how #20299 was graded (render, do not retire).

Inventory: other texts that restate the old exemption (triage note 2)

Searched at 8af914a3 with git grep over .claude/**, skills/**, AGENTS.md, CLAUDE.md, docs/** (ADRs included), content/docs/**, packages/spec/liveness/**, packages/spec/README.md and packages/lint/src/lint-liveness-properties.ts. Patterns: 良性 / 展示元数据 / benign display / display metadata / 永远谈不上 / never retire / 不要退役 / 文档形状 / docs-shaped / 有意保留 / authorWarn / 主流平台 / mainstream / 零消费者.

Surface Restates the exemption? Action
.claude/skills/spec-property-retirement/SKILL.md §0 yes, the two sentences above, plus the line 61 corollary edited here
other .claude/skills/**, .claude/agents/** no, 0 hits (authorWarn has exactly 1 hit in .claude/**, the §0 line) none
skills/**, AGENTS.md, CLAUDE.md no, 0 hits (skills/objectstack-ai/SKILL.md:225 "Display metadata" is a table cell describing label/description, not an exemption) none
docs/adr/** (ADR-0033, ADR-0049 read) no, neither carries a docs-shaped or benign-display exemption none
packages/spec/liveness/README.md "Author warnings", rule 1 (:562–565) only the authorWarn half: benign display metadata is not warned. It never says "never retire" none, and outside this file surface anyway (packages/**). The new §0 points at it rather than restating it
packages/lint/src/lint-liveness-properties.ts header (:21) same authorWarn half only none

The ruling does not address warnings, and the README's warning rule is consistent with it. A key the mainstream carries is a missing consumer, and warning its author would call it misleading. A key the mainstream lacks retires. So the family to fix is this one file.

PM mechanism hypotheses, as measured

  1. Site: confirmed. The branch starts at 8af914a3 (67047171 plus one unrelated docs commit). The bullet spans :43–47 and the two target sentences :45–47.
  2. Inventory: confirmed, only §0 in the governed texts (table above). One addition: the §0 line 61 corollary, fixed in place (item 2 above).
  3. Line ratchet: holds at headroom 0. Details under Line budget.
  4. check:pm-skill-id-lint: it does not scan this file. Its scan set is the pm-dispatch tree, os-dev.md and AGENTS.md (34 files, green), and this playbook carries 20 existing tracker-number citations. The bare comment id does not match its # pattern either. So the ruling record id stays in the rule text, as the triage direction asked, and no tracker number is added to the file.
  5. Documentation-shaped keys as the criterion's outcome: the wording is 「是「有」的一例」, the same criterion's result and not a second rule.

Line budget

before after
SKILL.md lines (ceiling 337) 337 337
widest table row, bytes (pin 326) 326 326
bytes 25,497 25,639 (+142)
tokens, ceil(bytes/4) (the token ratchet's convention) 6,375 6,410 (+35)
whole skill package (.claude/skills/spec-property-retirement/, 1 file) 337 lines / 6,375 tokens 337 lines / 6,410 tokens

Paid by deletion in the same bullet. Removed: 「没有运行时消费者,但被有意保留 —— 它们」, the hook.description example, 「按 … 常是模型」, and the whole 「良性展示元数据 … 也不要退役它」 sentence. No re-wrap of untouched text. New lines are 116 / 115 / 115 / 119 / 102 bytes, and line 61 goes from 120 to 112. Every line break falls between two wide characters or at a real space, so none renders as a stray space. No ceiling was raised and nothing moved to another file.

Verification (HEAD 085a5026)

The derived list comes from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (18 commands, re-derived after git fetch origin main, same list). Every command ran. Exit codes were captured before any pipe.

Command Exit
node scripts/check-ci-filter-parity.mjs 0
node scripts/check-closing-keyword-parity.mjs (and --self-test) 0 / 0
node scripts/check-comment-mask-corpus.mjs 0
node scripts/pm/check-harness-current.mjs --self-test 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions 3 (prerequisite not met: formula and lint not built, so NOT MEASURED); built both under the verify lock, then 0
pnpm check:agent-test-spelling 0
pnpm check:cross-package-test-inputs 0
pnpm check:doc-authoring 0
pnpm check:driver-memory-census 0
pnpm check:gitlink-declared 0
pnpm check:nul-bytes 0
pnpm check:pm-governed-merges 0
pnpm check:pm-skill-ratchet 0, "SKILL.md is 337 lines (ceiling 337; headroom 0)" and "widest table row is 326 bytes (pin 326; headroom 0)"
pnpm check:refd-timer-probe 0
pnpm check:required-contexts 0
pnpm check:skill-frame-sync 0
pnpm check:watch-hint-literal 0

Reconciliation: dispatch-gates --ran with coded exits reports "18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero)".

Also run:

  • Test Core job, scheduled by this path. packages/spec/src/shared/retired-key-migrate-sentence.test.ts reads this playbook. pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/shared/retired-key-migrate-sentence.test.ts passed: 1 file, 14 tests, exit 0. The rest of the shard is left to CI.
  • Roster gates under .claude/skills, which the derivation marks as not placeable. Each exited 0: check:pm-settings-deny-roster, check-skills-token-ratchet.mjs, check:pm-skill-id-lint, check:pm-governed-prose, check:skill-top-level-keys and check-published-list-mirrors.mjs.
  • Type-check lanes: NOT MEASURED. Reason: the diff touches no TypeScript.
  • Ablation: none owed. This PR adds no gate or assertion.

Changeset

skip-changeset. The only path is under .claude/** (fast lane). As a measurement, the root package that holds .claude/ is private: true, no workspace package.json names .claude, and the new text 「主流平台有没有这个能力」 has exactly one hit in the tree, this file. Positive control: the liveness README sentence "Only mark genuinely" is found under packages/spec/liveness/, which @objectstack/spec ships through files[] (liveness).

Acceptance notes

维护者速读(草稿)


Generated by Claude Code

… capability

§0 said benign display metadata (`description`, `tags`, `icon`) is never
retired and that "no code reads it" is the evidence for retirement. Both
contradict the maintainer's per-family criterion: ask whether mainstream
platforms have the capability; yes ⇒ build the consumer, no ⇒ retire,
never judged by whether anything in this repo reads the key.

The docs-shaped keys (`hook.label`, `flow.description`) stay, now as the
"yes" outcome of that one criterion (render, do not retire) rather than as
a second rule. `authorWarn` marking defers to the liveness README.
Net 0 lines, widest table row unchanged.

Claude-Session: https://claude.ai/code/session_01MjvgiFAmjHqsxy1XLiVYfH
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 085a5026
Local-runs: none

Face: .claude/skills/spec-property-retirement/SKILL.md (governed .claude/**, Tier S — check-governed-merges --pr 20364: 1 of 1 path hits the register). Reviewed in-seat at tier by the dispatching seat as the compensating control for a default-tier dev, on the diff (one file, +6 / −6, one commit), the card, the maintainer's ruling comment and the head's check-runs.

① Derived judgments

  • The §0 bullet now asks the maintainer's question as its header (「主流平台有没有这个能力?」) and quotes the maintainer's own words from ruling comment 5727134555 verbatim — 「有 ⇒ 补消费端(一次做对);没有 ⇒ 退役,而不是看仓里有没有人读」 is in that comment character for character (checked by the seat). The ruling wins and the text follows it: matches the triage direction and its execution note 1.
  • The documentation-shaped case is recast as the 「有」 outcome of the same criterion (hook.label / flow.description record intent for the next reader, ADR-0033 ⇒ 补渲染、不退役, verdict written to the ledger note), not a second rule — the wording the dispatch asked for; studio: show the authored label / description on flows, hooks, app areas, RLS policies and the view container (7 keys) #20299's grade (render, do not retire) is the precedent and reads the same way. description / tags / icon lose the by-kind exemption, with the RLS-policy tags key as the 「没有 ⇒ 退役」 example (the spec(security): retire rowLevelSecurity[].tags (1 key); no mainstream platform tags a row-level policy #20321 grade). authorWarn defers to the liveness README instead of a second copy.
  • The §0 corollary at :61 (「后者才是退役的证据」 → 「后者才证 dead」) is the same defect one line down: the old wording made in-repo readership the evidence for retirement, which the ruling forbids; the new wording keeps it as what the ledger's dead verdict rests on. Inside the claim's surface (§0 of the same file), declared separately in the PR body. Taken.
  • Inventory (execution note 2): the dev's count agrees with the seat's grep at 67047171 — no other .claude/**, skills/**, AGENTS.md, CLAUDE.md or docs/adr/** text restates the exemption; the liveness README's 「Author warnings」 rule 1 and the lint-liveness-properties.ts header restate only the authorWarn half, consistent with the ruling and outside the surface — left as they are, recorded. Ledger row notes are data rows; their outcomes agree with the criterion for keys the mainstream carries. No second spelling of the criterion introduced anywhere.
  • Ratchets: 337 → 337 lines (ceiling 337, headroom 0), widest table row 326 → 326 bytes (pin 326), paid inside the same bullet (the hook.description example and the 「常是模型」 clause dropped); no ceiling raised, nothing moved. check:pm-skill-id-lint does not scan this file (its scan set is the pm-dispatch tree, os-dev.md, AGENTS.md), so the bare comment id stays in the text as the triage asked and no tracker number was added — the dispatch's hypothesis 4 partly falsified, correctly. The playbook's only code reader (retired-key-migrate-sentence.test.ts) passed on the head (14 / 14 under the verify lock).
  • Reach: agent-facing rule text only; no schema, runtime or gate change; no public surface.

② Semver level

.claude/** sits in the private root package (no files[]); positive control 「Only mark genuinely」 present in packages/spec/liveness/README.md, which @objectstack/spec ships ⇒ no changeset; skip-changeset correct; consistent with Clause-②: no.

③ Boundary flags

No open_questions. Deviations accepted: the dispatch order's common tail says 「受管面 = Tier H」 while the single-card clause and the register say Tier S — the register decides, and both give the dev the same posture (draft, no ready, no auto-merge); the :61 corollary inside the claimed §0; the prerequisite build for check:doc-formula-expressions under the verify lock (NOT MEASURED then 0). Derived gates 18 / 18 run, a DERIVED zero; roster gates under .claude/skills each 0. No flag remains open.

Implemented-by: claude/issue-20354-retirement-playbook-criterion
Reviewed-by: session_01MjvgiFAmjHqsxy1XLiVYfH

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 03:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit a88a1bb Sep 28, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20354-retirement-playbook-criterion branch September 28, 2026 03:40
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…stem-* migration entries states each lesson in words, not tracker numbers (stage 3) (objectstack-ai#20384)

Part of objectstack-ai#20233

Clause-②: no

**Stage 3 of a staged card.** The card stays open for later stages; this
PR carries no closing keyword. Text only: no entry id, `surface`, `from`
/ `to`, conversion or matching logic moves, and the chain rewrites
exactly what it rewrote before.

## What this does

`os migrate meta` prints every ADR-0087 semantic entry it crosses as one
block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's
`reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's
runtime-string rule applies to all of it: 「Runtime strings — refusal
prose, prescriptions, anything an author is shown — carry no tracker
number (`pnpm check:doc-authoring`): the lesson goes into the text.」
Form **D** of ruling C+D on the parent card sets the shape: the lesson
in words, and no number, dead or alive.

This stage covers the next three families by site count, `driver-`,
`kernel-` and `system-`: **132 sites → 0** in the three prose fields.
None of the 26 entries carries a tracker id in `surface` (ruling A of
the stage-1 ACCEPT, `5858839916`, is checked and has nothing to do
here). Each site now says what the cited ruling, measurement or fix
decided. ADR ids stay. `registry.ts`, `spec-changes.json` and
`docs/protocol-upgrade-guide.md` are regenerated from the entries
(`gen:migration-registry`, `gen:spec-changes`, `gen:upgrade-guide`),
never hand-edited. The stage-1 pin now holds `engine-`, `ui-`,
`plugin-`, `driver-`, `kernel-` and `system-`.

## Census — tracker ids in the author-shown fields

**Instrument.** The stage-2 AST instrument, unchanged: a TypeScript-AST
walk over every `packages/spec/src/migrations/entries/**/*.ts`. For each
`entry` object literal it evaluates the string value of `replacement`,
`reason`, `acceptanceCriteria` and (counted separately) `surface`,
joining string literals with `+`, then counts `#` followed by 4 or 5
digits at a word boundary. **Validated first** by reproducing the
stage-1 readings on the stage-1 tree (`443b2f4fdc`, extracted with `git
archive`): `driver-` 7 entries / 44 sites (0 / 44 / 0, 25 distinct),
`kernel-` 9 / 44 (1 / 41 / 2, 11 distinct), `system-` 10 / 44 (0 / 42 /
2, 6 distinct), `engine-` 5 / 67, whole tree 266 entries / 1,016 sites /
9 `surface` sites — every figure equal to the stage-1 census. **Tree
measured:** `objectstack-ai/objectstack` at `569d4d2dbf` (this branch's
base). Unevaluable fields: 0.

**Controls, same run.**
- **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites
(replacement 1, reason 6), the reading stages 1 and 2 took.
- **Dark (comment lines):** 794 `//` lines in entry files carry a
tracker id, and none is counted. Comment lines belong to the sibling
card, and ⛔ this PR touches none (794 before and after).
- **Dark (field boundary):** the 7 `surface` sites left in the tree
(other families) count 0 in the three-field total and 7 in the `surface`
column.

**Re-measured on the base, matching the stage-1 census:** `driver-` 7
entries, **44** sites (replacement 0 / reason 44 / acceptanceCriteria
0), 25 distinct ids; `kernel-` 9 entries, **44** (1 / 41 / 2), 11
distinct; `system-` 10 entries, **44** (0 / 42 / 2), 6 distinct. 37
distinct ids across the three (the families share `objectstack-ai#14478`, `objectstack-ai#15939`,
`objectstack-ai#17635` and `objectstack-ai#3733`). `surface`: 0 in all three. Whole tree: 300
entries, **843** sites, 7 `surface` sites.

**After this PR:** `driver-` 0, `kernel-` 0, `system-` 0; `engine-`,
`ui-`, `plugin-` still 0; whole tree **843 → 711** sites; `surface` 7
(unchanged, other families).

| entry | sites (replacement / reason / acceptanceCriteria) |
|---|---|
| `17.driver-aggregate-undeclared-key-aliases-removed` | 6 (0 / 6 / 0) |
| `17.driver-capabilities-inert-bits-removed` | 4 (0 / 4 / 0) |
| `18.driver-options-timeout-to-timeout-ms` | 1 (0 / 1 / 0) |
| `17.driver-sql-distinct-bare-filter-typed` | 9 (0 / 9 / 0) |
| `18.driver-sql-unresolvable-where-column-refused` | 14 (0 / 14 / 0) |
| `18.driver-sql-upsert-cross-row-identity-merge-refused` | 9 (0 / 9 /
0) |
| `18.driver-turso-config-local-path-wasm-retired` | 1 (0 / 1 / 0) |
| `18.kernel-compatibility-matrix-estimated-migration-time-unit-in-key`
| 5 (0 / 5 / 0) |
| `18.kernel-context-preview-mode-retired` | 5 (1 / 4 / 0) |
| `18.kernel-event-bus-retention-unit-in-key` | 3 (0 / 3 / 0) |
| `18.kernel-health-check-and-hot-reload-durations-unit-in-key` | 7 (0 /
5 / 2) |
| `18.kernel-package-lifecycle-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.kernel-plugin-health-report-durations-unit-in-key` | 3 (0 / 3 / 0)
|
| `18.kernel-plugin-security-durations-unit-in-key` | 4 (0 / 4 / 0) |
| `18.kernel-runtime-config-timeout-unit-in-key` | 11 (0 / 11 / 0) |
| `18.kernel-startup-orchestrator-durations-unit-in-key` | 3 (0 / 3 / 0)
|
| `18.system-cache-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-collaboration-durations-unit-in-key` | 4 (0 / 4 / 0) |
| `18.system-failover-health-check-interval-unit-in-key` | 3 (0 / 3 / 0)
|
| `18.system-metrics-jsdoc-durations-unit-in-key` | 12 (0 / 12 / 0) |
| `18.system-metrics-window-durations-unit-in-key` | 5 (0 / 3 / 2) |
| `18.system-object-storage-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-registry-config-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-tracing-otel-exporter-durations-unit-in-key` | 5 (0 / 5 /
0) |
| `18.system-tracing-span-duration-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-worker-queue-rate-limit-duration-unit-in-key` | 3 (0 / 3 /
0) |
| **total, 26 entries** | **132 (1 / 127 / 4)** |

## Every citation read, and what the text now says

I read each cited issue or PR myself with single-card REST reads: the
body, and the comments where a ruling or a measurement lives. Ids are in
code spans so this body posts no cross-references. All 36 bare ids were
resolved against this repository, because every sentence that cites one
is about this repository's code; the one cross-repo id is `cloud#1651`.

| cited | what it decided (read) | how the text now carries it |
|---|---|---|
| `objectstack-ai#3733` | The pruned `cached` field key: measured, the parse succeeded
and the removed key was dropped without a word; the orphan schema was
deleted. | "an earlier field-key prune measured exactly that — the parse
succeeded and the removed key was dropped without a word" (health-check,
OTel exporter) |
| `objectstack-ai#3821` | The sharing-rule page: an unsortable query fell through to
an empty page, and the driver fix made an unsortable query lose its
ORDER BY, not its rows. | "the unknown-column recovery ladder (an
unsortable query loses its ORDER BY, not its rows)"; "the ladder's own
premise — rows matter more than their order"; "the ladder's recoveries"
|
| `objectstack-ai#4484` | `IDataDriver.findStream` removed: no production caller, two
of three implementations buffered the whole set, and no tombstone
because nothing parses a driver object. | "Retiring
`IDataDriver.findStream` (it had no production caller, and two of its
three implementations read the whole result set into memory …)";
"(`IDataDriver.findStream`, removed with no tombstone because nothing
parses a driver object)"; by entry id in the `distinct` entry |
| `objectstack-ai#4583` | The datasource ledger's dead keys removed; `capabilities.*`
went as a whole block (11 of 11 unread). | "was retired separately, as a
whole block nothing read" |
| `objectstack-ai#4634` | Audit of all 34 `DriverCapabilities` bits: 3 live, 31 dead
and tombstoned. | the entry already states the audit ("the follow-up
audit checked every bit"); the trailing id is dropped |
| `objectstack-ai#4914` | Maintainer, 2026-08-04: remove `manifest.loading` and
`PluginHotReloadSchema`; keep `HotReloadConfigSchema`, the side with an
implementation (`HotReloadManager`), as the start point. | "kept twice:
as the hot-reload vocabulary that had an implementation when the
manifest-side copy was removed, …" |
| `objectstack-ai#4984` | An org-axis red-line gate read only aliases the schema
rejects while its own fixtures spelt them: tests green, rule dead. |
"the family of the org-axis red-line gate that read only rejected
aliases while its own fixtures spelt them, so its tests stayed green and
the rule stayed dead" |
| `objectstack-ai#5181` | Narrow the query parameter of `IDataDriver`'s methods
(`DriverQuery`, no redundant `object`). | "neither the narrowing of
`IDataDriver`'s query parameters to `DriverQuery` nor the follow-through
…" |
| `objectstack-ai#5499` | Maintainer, 2026-08-05: freeze investment in `driver-memory`
/ `driver-mongodb`; fully lifted 2026-08-11 (comments `5249019855`,
`5252526378`). | "the maintainer's 2026-08-05 investment freeze on
driver-memory, which was lifted on 2026-08-11" |
| `objectstack-ai#5540` | Remove `IStorageService.list(prefix)`: zero consumers, and
the two adapters answered differently and both incompletely. | "(the
zero-consumer `IStorageService.list`, whose two adapters answered
differently and both incompletely)" |
| `objectstack-ai#6011` | Maintainer: close the `ctx.user` `roles` alias now. | "(the
`ctx.user` `roles` alias, closed at once on the maintainer's word rather
than given a window)" |
| `objectstack-ai#6075` | **404** — see Acceptance notes. | "the follow-through that
brought five drivers' implementations in line" |
| `objectstack-ai#6320` | `distinct`'s third argument meant different things on memory
and sql; the sql half was dispatched, the memory half held under the
freeze. | "(the measurement that found the two drivers reading this
argument differently split the fix: the sql half is this entry, and the
memory half was held back by that freeze)" |
| `objectstack-ai#6321` | `query.aggregate` / `agg.func` are undeclared aliases whose
only writers are driver fixtures; order: re-spell the fixtures, delete
the aliases, then narrow the signature. | "The removal ran in a fixed
order — the fixtures re-spelt first, the two alias branches deleted
second, the parameter narrowed to `DriverQuery` last — because the
reverse order yields red nobody can explain." |
| `objectstack-ai#6404` (PR) | Executed that order and narrowed `aggregate`'s query
parameter to `DriverQuery`. | the same sentence |
| `objectstack-ai#7929` | Maintainer, 2026-08-12, ruling B: `driver-sql`'s filter
refusal stops echoing `$field` operands, for every caller; the full
diagnostic goes to the server log. | "the same predicate-text disclosure
shape the driver's field-reference filter refusals had already been made
to stop echoing (the full diagnostic goes to the server log, never the
response)"; "that disclosure shape closed on the last dialect" |
| `objectstack-ai#8371` | Ruled option 2: a dotted filter key whose head is a
relation, a formula or a scalar is refused at both doors; a structured
head stays unjudged. | "the axis owned by the dotted-filter verdict,
which refuses a dotted key whose head is a relation, a formula or a
plain column at the protocol and engine doors" |
| `objectstack-ai#8592` | Measured on live MySQL: knex compiles the named conflict
target away. | stated by the entry ("knex drops the named keys before
the statement leaves the process"); the trailing id is dropped |
| `objectstack-ai#8621` | Option A: a pre-flight refusal when no unique index backs
the caller-named conflict target. | "Two earlier pre-flight refusals
closed the half where no unique index backed a caller-named target …" |
| `objectstack-ai#8622` | `id` becomes insert-only on the merge path: a merge on a
non-primary conflict key was measured rewriting the existing row's
primary key. | "`id` is insert-only on the merge path (made so once a
merge on a non-primary conflict key was measured rewriting the existing
row's primary key)" |
| `objectstack-ai#8755` | Ruling option A: a pre-flight refusal when a second unique
key could absorb a backed, caller-named target. | "… and the half where
a rival unique key could absorb a caller-named one" |
| `objectstack-ai#8790` | Maintainer, 2026-08-15: refuse both halves with
`INVALID_FILTER` / 400, naming the column. | "Ruled by the maintainer on
2026-08-15: refuse BOTH halves …"; "Recover-both was excluded by the
ruling's own argument" |
| `objectstack-ai#8807` | Maintainer, 2026-08-15: an upsert must never modify a row
whose identity the caller did not supply and whose conflict key it did
not name; enforcement delegated, blanket refusal excluded. | "Ruled by
the maintainer on 2026-08-15, as a contract principle …" (the principle
itself was already quoted verbatim) |
| `objectstack-ai#8926` | Maintainer, 2026-08-16, option A: MySQL's spelling joins the
one shared predicate (envelope and recoveries together). | "Addendum
2026-08-16." — the paragraph already states option A |
| `objectstack-ai#9061` (PR) | Implemented that option A. | the same |
| `objectstack-ai#11825` | Maintainer, 2026-08-25: retire the declarative
`AdvancedPluginLifecycleConfig` container; the classes stay a
host-driven library. | "… and as a host-driven library when the
declarative lifecycle config container was retired" |
| `objectstack-ai#11846` | **404** — see Acceptance notes. | "maintainer ruling
2026-08-27 (Option A: remove)"; "(as the removal ruling recorded)" |
| `objectstack-ai#14478` | Ruling B, 2026-09-02: a no-baseline gate plus an ADR-0087
rename of every offender (`DriverOptions.timeout` among the seven
named); ruling B again, 2026-09-05: the population is every authored and
every runtime-emitted duration, minus exemptions declared on the schema.
| "Maintainer ruling B on duration units (2026-09-02, its population
widened on 2026-09-05 to every authored and every runtime-emitted
duration, bar the exemptions a schema declares on the key itself)"; "the
duration-unit rule (…)" |
| `objectstack-ai#14519` | The two tenant timeouts published a describe naming no unit
(the unit sat in the JSDoc only); folded into the rename. | "the
unit-nowhere shape (no unit in the name or in the published describe,
first measured on two tenant timeouts)" |
| `objectstack-ai#15626` (PR) | Landed the gate and the seven founding renames, the
tenant `idleTimeout` → `idleTimeoutSeconds` among them. | "The tenant
half was already renamed, in the same change that landed the duration
gate itself" |
| `objectstack-ai#15678` | `kernel/`: the 14 remaining duration keys carry their unit
in the key name. | "the kernel-directory duration renames" / "the
kernel-directory round"; trailing ids dropped |
| `objectstack-ai#15679` | `system/`: the 15 remaining duration keys carry their unit
in the key name; `size` got an honest name. | "the system-directory
duration round"; trailing ids dropped |
| `objectstack-ai#15939` | The gate did not read JSDoc. Ruled 2026-09-07: refuse the
JSDoc / describe divergence. Ruled A 2026-09-11: remediate the
population per file first, land the widened gate last. | "Director-seat
ruling A of 2026-09-11 on the JSDoc-channel finding … a duration key
whose JSDoc names a unit its describe does not is refused, and the keys
in that shape are remediated per file before that refusal lands" |
| `objectstack-ai#16024` | Maintainer, 2026-09-06, per key: forward `timeout`, remove
`localPath` and `wasm`. | "ruled per key by the maintainer on
2026-09-06, once all three of this package's unread config keys had been
measured" |
| `objectstack-ai#17635` (PR) | The widened gate: refuse a duration key whose JSDoc
names a unit its describe does not; landed last. | "lands that widened
gate last, into a tree already clean" |
| `objectstack-ai#18669` | Ruling A, 2026-09-17: rename `FileValue.duration` and
`estimatedMigrationTime`, each with an ADR-0087 entry; no new closed
type, no narrowing of stored data. | "Maintainer ruling A of 2026-09-17
on the last two duration keys no closed duration type could express …" |
| `cloud#1651` | **Not readable from this session** — see Acceptance
notes. | "cloud — a census closed 2026-08-26: OS_PREVIEW_MODE there is a
routing-only switch …" |

No call-shaped token moves: a `name(` census over `registry.ts` is
identical before and after (297 distinct tokens), so textual
call-spelling ratchets read the same.

## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`,
widened

`COVERED_PREFIXES` is now `engine-`, `ui-`, `plugin-`, `driver-`,
`kernel-`, `system-`. The pin still spawns the real CLI (`os migrate
meta --from 16 --to 18`) once, locates each covered block **verbatim**
in stdout, and asserts the printed block — `surface` included — carries
no `#` plus 4 or 5 digits. Anti-vacuity:
- the `REWRITTEN` floor rises from 29 to **55** ids: the 26 entries of
this stage (7 `driver-`, 9 `kernel-`, 10 `system-`) are added, and every
covered prefix must still select at least one entry;
- presence in stdout is asserted before cleanliness (the
`driver-sql-unresolvable-where-column-refused` reason carries two
blank-line paragraph breaks, and its block is found verbatim);
- the detector is exercised on both sides first (lit on 4 and 5 digits,
dark on 3, 6 and `ADR-0112`).

The file keeps its stage-1 name; the header lists the six covered
families.

## Ablation — the widened pin can fail on a `driver-` block

From committed state, HEAD `1c0dc7ad54`, with
`scripts/ablation-replace.mjs` in wrap mode (it owns the restore trap)
and `scripts/ablation-dist-preflight.mjs` gating each leg. The bundle is
built from the generated `registry.ts`, so that is the file mutated.
- **Mutation.** In `registry.ts`, the reason of
`driver-sql-upsert-cross-row-identity-merge-refused`: anchor `pre-flight
refusals closed the half` → `pre-flight refusals (objectstack-ai#8621) closed the
half`. The tool read anchor 1 → 0 and replacement 0 → 1, blob `b41e1d44`
→ `8f8d226e`.
- **Mutate leg** (one lock turn: build, preflight, pin). Spec build exit
0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed |
2 passed` — `driver-sql-upsert-cross-row-identity-merge-refused: the
printed guidance cites a tracker id: expected 'objectstack-ai#8621' to be undefined`.
- **Restore.** Tool-proven: blob `b41e1d44` == HEAD, `git diff HEAD`
empty.
- **Restore leg.** One lock turn, taken on the second try (the first
waited out its 540 s budget, exit 99, NOT MEASURED, the tree already
restored). Spec build exit 0. The `--absent` preflight found the marker
in none of 222 built files, with the working tree clean against HEAD.
Pin: **green**, `3 passed`.

## Verification

Final head **`1c0dc7ad54`** for every line below; each heavy run went
through `scripts/pm/os-verify-lock.sh` (one turn, `VERDICT command-exit
0`, per-step exits recorded separately).

- **Build:** `pnpm exec turbo run build --concurrency=2
--filter='@objectstack/cli^...'` gives `Tasks: 55 successful, 55 total`.
- **Pin and its neighbour:** `pnpm --filter @objectstack/cli exec vitest
run --project integration --maxWorkers=2
test/migrate-meta-engine-guidance.test.ts
test/migrate-meta-default-range.test.ts` gives `Test Files 2 passed`,
`Tests 10 passed | 1 skipped` (the skip is the default-range file's own
pre-existing `skipIf`).
- **Spec tests that read these entries or the registry:** `pnpm --filter
@objectstack/spec exec vitest run --maxWorkers=2 src/migrations
src/kernel/preview-mode-retirement.test.ts
scripts/build-schemas-check-mode.test.ts` plus the 19 other spec test
files that read `MIGRATIONS_BY_MAJOR`, the registry or an entry file:
`Test Files 24 passed`, `Tests 696 passed`.
- **CLI unit:** `test/vitest-tiers-partition.test.ts` and
`src/utils/spec-release-changes.test.ts`: `Test Files 2 passed`, `Tests
28 passed`.
- **The call-spelling census that reads `registry.ts`:** `pnpm --filter
@objectstack/driver-sql exec vitest run --maxWorkers=2
src/sql-driver-query-signature.test.ts` gives 15 passed.
- **Typecheck:** `pnpm --filter @objectstack/spec typecheck` exits 0
(test layer: 53 files / 255 errors held in its ledger); `pnpm --filter
@objectstack/cli typecheck` exits 0 (test layer: 3 files / 28 errors
held, unchanged).
- **Gate families:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derives **89** families at
`1c0dc7ad54` (after `git fetch origin main`). `--ran` over the recorded
exit codes reads **89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN**, all
exit 0. They include `check:doc-authoring` ("16466 customer-facing
string(s) across 1135 spec sources clean"), `check:issue-citations`,
`check:migration-registry` ("registry.ts is current (300 semantic, 219
retired-key, 199 retired-def)"), `check:spec-changes`,
`check:upgrade-guide`, `check:generated` ("All 15 generated artifacts
are up to date"), `check:duration-unit-keys`, `check:nul-bytes`,
`check:adr-0087-registration` and `check:changeset-no-major`.
- `check:dual-build-cjs-loads` refused first with `PREREQUISITE NOT MET`
(exit 3: twelve packages outside the CLI closure had no `dist/`). Those
`dist/` directories were written later in the same pass (04:48–04:49Z,
inside the `check:type-check-debt` run, whose re-measure builds them);
re-run at the same head it exits 0 (104 entries / 66 packages / 659 CJS
files). The reconciled list takes that latest run.
- **Lint (a proven narrowing, not the repo-wide run, which is CI's):**
`eslint --no-inline-config --format json` over the 28 changed `.ts`
files reports 28 files, 0 errors, 0 warnings.
- The population is read from `eslint.config.mjs`:
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 28
are in it (no file-ignored warning).
- Invariance: the config enables no type-aware linting (no
`parserOptions.project`, no typed rules), so a text edit cannot move the
verdict on a file it does not touch.
- **Mergeability:** see Acceptance notes (driver-free `merge-tree`
against `862b6ce869` exits 0).

## Acceptance notes

- **Two dead ids, rewritten from the code on `main`.** `objectstack-ai#6075` and
`objectstack-ai#11846` answer 404 on both the issues and the pulls endpoint, re-probed
with a 200 control (`objectstack-ai#14478`).
- `objectstack-ai#6075` (`distinct`'s "never reached it" sentence):
`packages/drivers/driver-sql/CHANGELOG.md` (commit `d367f03`) and
`sql-driver-query-signature.test.ts` record what it did — the five
drivers' implementations followed `IDataDriver`'s `DriverQuery`
narrowing. The sentence now says exactly that.
- `objectstack-ai#11846` (preview mode): `packages/spec/CHANGELOG.md` (commit
`0c2334f`), `packages/spec/src/kernel/context.zod.ts` and
`preview-mode-retirement.test.ts` record the 2026-08-27 ruling (Option
A: remove), the three-repo zero-consumer measurement and the
re-declare-fresh condition. Dropped because `main` does not state them:
"decision-inbox batch 2" and "all four decision facets pointed the same
way". "The objectstack-ai#11846 card records the measurement" (objectui leg) now reads
"zero consumers, measured when the removal was ruled", which is what the
changelog and the test header say.
- **One cross-repo id this session cannot read.** `cloud#1651` answers
403 here: `objectstack-ai/cloud` is not attached to this session
(`add_repo` refused: no access). It is neither confirmed nor refuted, so
its sentence was rewritten from what `main` records about it —
`packages/spec/CHANGELOG.md` (`0c2334f`: closed 2026-08-26 with positive
controls, `RuntimeMode` zero hits, `ArtifactKernelFactory` 20+ hits and
never touching `previewMode`) and `context.zod.ts` (`OS_PREVIEW_MODE`
there is routing-only). The cloud-side detail `main` does not state —
`previewMode` "only as a local variable" whose effect is adding
wildcards to "CSRF" trusted origins — is dropped; the parenthesis that
replaces it describes this repository's own `serve.ts` (the one reader
of `OS_PREVIEW_MODE` here only widens better-auth's trusted origins to
preview-domain wildcards), which is measured on `main`.
- **Decision-batch numbers went too (invisible to the regex).** Nineteen
sites cited a decision batch as `#` plus two or three digits (`objectstack-ai#43` ×13,
`objectstack-ai#115` ×4, `objectstack-ai#151` ×1, `objectstack-ai#158` ×1). They are numbers an author is shown
and cannot follow, so each is dropped. One consequence worth naming: 13
entries said `Maintainer ruling B on objectstack-ai#14478 (2026-09-02, decision batch
objectstack-ai#43)`, which fused two rulings on the same card — B of 2026-09-02 (the
gate and the no-baseline rename) and B of 2026-09-05, decided in that
batch (the population: every authored and every runtime-emitted
duration, minus schema-declared exemptions). The sentence now names both
dates. The `objectstack-ai#158` sentence (the agreement shape ruled an offence on
2026-09-18) is corroborated by
`.changeset/18075-agreement-shape-is-an-offence.md` on `main`.
- **"issue NNNN" / "PR NNNN" spellings, checked by hand.** No
bare-number spelling exists in these 26 entries' author-shown text; the
two `PR` citations (`PR objectstack-ai#6404`, `PR objectstack-ai#9061`) were `#`-spelled, so the
instrument saw them and they are gone. The only `#` left in these 26
files is on `//` comment lines (sibling card's surface), including a
`Prime Directive objectstack-ai#13` reference.
- **A citation whose page says something narrower than the text.**
`kernel-health-check-and-hot-reload-durations-unit-in-key` called
`shutdownTimeout`'s shape "the objectstack-ai#14519 unit-nowhere shape". `objectstack-ai#14519`'s
keys carried their unit in the JSDoc; "unit nowhere" is the gate's name
for it (`check-duration-unit-keys.ts` header: "no unit ANYWHERE (the
objectstack-ai#14519 shape)"), because the gate did not read JSDoc. The sentence now
says what the shape is — no unit in the name or in the published
describe — and that it was first measured on two tenant timeouts.
- **A comment that my text edit makes slightly stale.**
`18.system-metrics-window-durations-unit-in-key.ts` carries a `//`
comment saying its acceptanceCriteria sentence "is objectstack-ai#15679's, left word
for word". That sentence now says "that JSDoc-channel gap is filed as a
finding of its own" where it said "is objectstack-ai#15939": same content, no number.
The comment is the sibling card's surface (comment lines), so it is
untouched here.
- **Three "card" references re-anchored.** Removing an id left "the same
card" in the Turso entry pointing at nothing; it now says "the same
measurement". The kernel entries' "renamed by this same card" carry no
number and were not otherwise rewritten, so they are left.
- **Cross-PR check: no open PR adds or edits a `driver-`, `kernel-` or
`system-` semantic entry.** Read at 2026-09-28T04:0xZ: the 18 open PRs'
file lists (`GET /pulls/{n}/files`) carry 0 files matching
`migrations/entries/semantic/NN.(driver|kernel|system)-*`. The Version
Packages PR (`objectstack-ai#17076`) lists more than 1,000 files; the 1,100 rows read
carry no entry file, and it is the bot-generated release PR. Nothing in
flight will be held by the widened pin on arrival.
- **`main` moved 4 commits past the base** (`862b6ce869`: `objectstack-ai#20364`,
`objectstack-ai#20341`, `objectstack-ai#20366`, `objectstack-ai#20352`); none touches
`packages/spec/src/migrations/` or the pin. A driver-free bare-clone
`merge-tree --write-tree` of this head against `862b6ce869` exits 0 with
no conflicted path, so `registry.ts` needs no merge, and `main` was not
merged in.
- **Generated projections** (`spec-changes.json`,
`docs/protocol-upgrade-guide.md`) are regenerated, as in stages 1 and 2;
their `--check` legs are green. Only the three `driver-` entries
registered at protocol 17 appear in them, which is why those diffs are
small.
- **No other test pins these entries' text.** A `git grep` of test files
for the 26 entry ids finds one (`preview-mode-retirement.test.ts`),
which names the entry in a comment and reads no prose; a grep of tests
for the 37 cited numbers finds only comment lines. So no test needed
re-pinning this stage (stage 2's `migrations.test.ts` case has no
counterpart here).

## Line budget

Entry files: **352 changed lines** (+228 / −124) across 26 files,
against the stage-1 ≈400 budget. The whole diff is **776 lines** (+516 /
−260) in 31 files. Of the rest, `registry.ts` is 352, the two
projections are 18 (`spec-changes.json` 12, the upgrade guide 6), the
widened pin is 33 and the changeset is 21.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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 size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants