Skip to content

docs(skills): the retirement playbook prescribes a minor changeset, not the major a live gate refuses - #19446

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-19360-retirement-changeset-level-minor
Sep 20, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-19360-retirement-changeset-level-minor

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #19360

Clause-②: no

The contradiction

.claude/skills/spec-property-retirement/SKILL.md :291 — the Changeset item of the
retirement checklist — told the executing agent to grade @objectstack/spec a major bump.
That checklist is executed verbatim by every retirement dev, and the per-PR Check Changeset
job (.github/workflows/pr-automation.yml :244, running
node scripts/check-changeset-no-major.mjs --base "$MERGE_BASE" at :1075) refuses exactly that
bump while the launch window stands. So the line reddened the author's own PR for doing what the
instructions said — and then left them to decide, mid-round on an already delicate public-surface
deletion, whether the playbook or the gate was authoritative.

Reproduction, both legs measured in this worktree

A throwaway .changeset/repro-19360.md was committed on this branch, the gate run against the
branch point 23f1de078, the exit captured before any pipe, and the tree restored
(git diff HEAD empty, git status --porcelain empty) after each leg.

Leg 1 — the checklist line copied verbatim ('@objectstack/spec': major) :: exit 1

⛔ This PR introduces changeset(s) that declare a `major` bump.

   .changeset/repro-19360.md
     - @objectstack/spec: major

Every publishable package is in the Changesets `fixed` (lockstep) group, so a single
`major` promotes the ENTIRE monorepo to a new major version. During the launch window
ship breaking changes as `minor` instead (they do not burn a major version number).

Leg 2 — the prescription this PR lands ('@objectstack/spec': minor) :: exit 0

✓ This diff introduces no `major` bump.

The same file's own header states the rule the fix restores (:47-:51): "During the launch window
we ship breaking changes as minor (pre-1.0 semantics …). This guard makes that convention
enforceable instead of tribal". .changeset/pre.json is absent on main, so the RC exemption is
not standing the guard down — the enforce branch is live.

Before / after

Before (:291-:296, 6 lines):

- [ ] **Changeset** —— `@objectstack/spec` 用 `major`。AGENTS.md:breaking
      changeset 必须带 FROM → TO 映射与一行修复;它作为 npm 包里的 `CHANGELOG.md`
      发出,是升级中的 agent 撞上墓碑报错后 grep 的东西。
      样板是任一条活着的 `.changeset/*-retired.md` —— 抄它的 "The retirement
      kit:" 段。⚠ 并带上 ADR-0087 处置标记(AGENTS.md 的 changeset 一节;
      `pnpm check:adr-0087-registration` 把门)—— 退役正是它点名的那一类。

After (:291-:296, 6 lines):

- [ ] **Changeset** —— `@objectstack/spec` 用 `minor`,⛔ 不用 `major`:逐 PR 的 `Check Changeset` 跑
      `scripts/check-changeset-no-major.mjs`,发射窗口期内拒收 `major`,照抄即红。破坏性语义由 BREAKING
      banner 承载 —— AGENTS.md:breaking changeset 必须带 FROM → TO 映射与一行修复;它作为 npm 包里
      的 `CHANGELOG.md` 发出,是升级中的 agent 撞上墓碑报错后 grep 的东西。样板抄任一条活着的
      `.changeset/*-retired.md` 的 "The retirement kit:" 段。⚠ 并带上 ADR-0087 处置标记(AGENTS.md 的
      changeset 一节;`pnpm check:adr-0087-registration` 把门)—— 退役正是它点名的那一类。

Everything the item already carried survives — the AGENTS.md:breaking pointer, the FROM → TO
mapping requirement, the CHANGELOG.md rationale, the .changeset/*-retired.md template pointer
and the ADR-0087 disposition marker. What is added is the level (minor), the prohibition
(⛔ 不用 major), the gate and job that enforce it, and the sentence that the breaking semantics
ride the BREAKING banner rather than the bump level.

Why the level is stated in the playbook at all, on the four axes

It would have been cheaper to delete the level and defer it to each card's ruling. That is the
option this PR rejects, and the four axes say why.

  • 实际业务需求 — measured, not assumed: the one retirement that ran against this
    contradiction is disposition for PluginCapabilityManifestSchema / plugin-registry.zod.ts — the #13285 census closed all-zero across every reachable repo, so enforce-or-remove is now the open question #18623, whose ruling (comment 5725503716, maintainer 「同意」, 2026-09-18)
    states the level minor explicitly. That card therefore needed no stop. A retirement whose
    ruling is silent on the level has nothing to break the tie, and the checklist is the only
    artifact it reads. A blank slot in an executed checklist is not neutral — it is a prompt to
    guess.
  • 项目长远合理性 — the split the fix writes down is already the repo's declared architecture,
    not a new convention: pr-automation.yml's "WHICH LEVEL" prose says verbatim "During the launch
    window major stays refused by check-changeset-no-major and breaking-ness is carried by the
    BREAKING banner plus the ADR-0087 disposition, not by the level" (maintainer ruling 2026-09-04,
    decision batch [WIP] Add query enhancements and advanced validation features #35). Naming the gate in the playbook makes the playbook derive from that rule
    instead of racing it, so the line goes stale only when the rule does — and the gate's own header
    carries the GA end condition that will retire both together. No workaround: the consumer-side
    alternative (teach the dev to add allow-major) would route around a guard whose error message
    says the label means "a whole-stack major release is genuinely intended", which is false for a
    property retirement.
  • 防 AI 写代码犯错 — this is the axis that decides it. The playbook is a checklist an agent
    executes verbatim; the failure mode is 「示例照抄即失败」. A declared step that the runtime
    refuses is exactly the shape the house rules forbid — 声明即强制, never let an agent declare
    what the runtime will not honour. Deferring the level would replace a wrong instruction with an
    absent one, which is the softer version of the same defect: the agent still has to guess, and
    the guess is now invisible in review. Stating the level AND naming the refusing gate makes the
    instruction self-checking — the next author can run the named script and see the verdict for
    themselves rather than trusting the line.
  • 创业阶段不扩散需求 — net 0 lines, one checklist item, one file. No new gate, no new label,
    no allowlist, no transition window and no second spelling kept alive: the major prescription
    is replaced outright rather than deprecated beside its replacement. scripts/check-changeset-no-major.mjs
    is untouched — the launch-window guard is the thing being obeyed, not the thing being changed.

Budget

reading before after
.claude/skills/spec-property-retirement/SKILL.md lines 337 337
ceiling in scripts/pm/check-skill-line-ratchet.mjs 337 337
widest line in the touched item (bytes, cap 120) 98 120

Net 0. The item is repacked in place at 6 lines; every rewrapped line was produced by the
ratchet's own wrapLine/breakLegal atoms, so no break lands at a Han + ASCII-punctuation
junction. node scripts/pm/check-skill-line-ratchet.mjs :: exit 0.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran at head ee26ec6:
18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN — a derived zero, every family recorded an exit
code and none of them is 3. Route-named additions beyond the derived union, all exit 0:
check-skill-line-ratchet.mjs, check-skill-id-lint.mjs, check-governed-prose.mjs,
check-skills-token-ratchet.mjs, check:pm-settings-deny-roster, and the declared cross-package
consumer packages/spec/src/shared/retired-key-migrate-sentence.test.ts (14 tests passed), which
reads this playbook as a scanned corpus.

pnpm lint narrowed to a measurement rather than skipped: eslint's covered population is
**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} read from eslint.config.mjs :971 — a .md path is not in
it; --format json over the one changed file returns 1 entry, errorCount 0, with the message
"File ignored because no matching configuration was supplied"; and the config declares no
parserOptions.project (type-aware linting deliberately off, :328), so this diff cannot move the
verdict on any untouched file. The intersection of this diff with eslint's population is empty.

Acceptance notes

  • Swept for the same defect elsewhere and found none: grep for a major changeset prescription
    across AGENTS.md, all of .claude/** and the published skills/** catalog returns zero hits
    outside the line this PR fixes. The playbook was the only site.
  • scripts/check-changeset-no-major.mjs is deliberately untouched: the launch-window guard is
    correct and carries its own GA end condition. This PR moves the instruction to the gate, not the
    gate to the instruction.

维护者速读(草稿)

改了什么 —— 退役 playbook 的 Changeset 那一条,原来写「@objectstack/spec 用 major」,现在
写「用 minor,不用 major」,并点名是哪个门禁在拒收(Check Changeset job /
scripts/check-changeset-no-major.mjs)。一个文件、一条清单项、净增 0 行。

为什么改 —— 这条清单是 agent 照着一条条执行的,不是读着理解的。照抄这一行写出来的 changeset
会被逐 PR 的门禁当场拒收,PR 变红。更贵的是红之后:agent 得在一张本来就很敏感的「删除公开键」
卡上,临时判断到底是 playbook 算数还是门禁算数。上一次撞上它的是 #18623,那张卡的裁决里恰好写了
minor,所以没停;下一张裁决里没写级别的退役卡就没有东西能打破平局。

风险与代价(含回滚) —— 风险面就是这一条清单项的措辞,没有代码、没有门禁、没有 schema 改动。
本地实测两条腿:写 major 的 changeset 门禁退出码 1,写 minor 退出码 0。回滚 = revert 这一个
commit,退役流程回到今天的状态(即:回到会红的那一版)。发射窗口在 GA 结束时,那条门禁自己的
文件头写着终止条件;到那天 major 重新合法,这一行要跟着门禁一起改——所以这里点名了门禁,
让下一位读者顺着指针找得到。

席位意见 ——

你要做的 —— 确认一句话:发射窗口期内,退役的 changeset 用 minor + BREAKING banner 承载破坏性
语义,这与 pr-automation.yml 里 2026-09-04 的裁决一致。如果同意,这张卡按 Tier S 走席位 contract
review 落地,不需要你再做别的。


Generated by Claude Code

… not the `major` a live gate refuses

The `spec-property-retirement` Changeset checklist item told the agent to grade
`@objectstack/spec` a `major` bump. The per-PR `Check Changeset` job runs
`scripts/check-changeset-no-major.mjs`, which refuses exactly that while the
launch window stands — so an agent executing the checklist verbatim reddened its
own PR and then had to decide, mid-retirement, whether the playbook or the gate
was authoritative.

Measured in this worktree, both legs on a throwaway changeset:

  "@objectstack/spec": major  -> exit 1, "This PR introduces changeset(s) that
                                 declare a `major` bump."
  "@objectstack/spec": minor  -> exit 0, "This diff introduces no `major` bump."

The item now says `minor`, names the gate and the job so the next reader sees
why, and states that the breaking semantics ride the BREAKING banner instead of
the bump level — the same split `.github/workflows/pr-automation.yml` already
states under "WHICH LEVEL". The `AGENTS.md:breaking` pointer, the FROM -> TO
mapping requirement, the `CHANGELOG.md` rationale, the template pointer and the
ADR-0087 disposition marker all survive; the item is repacked in place at 6
lines so the file stays at its ratchet ceiling of 337, every line under 120
bytes.

Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi
Co-authored-by: Claude <noreply@anthropic.com>
@os-steve os-steve added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 20, 2026 — with Claude
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ee26ec6aa30e77cadaa7ddffe5d83a250f172366

① Derived judgments

  • Published accept set / public surface: none moves — one internal playbook (.claude/skills/spec-property-retirement/SKILL.md, +6 / −6, net 0 against the net-0 budget), the Changeset checklist item :291–:296 repacked in place; no package, no gate, no workflow. check-governed-merges --pr 19446: GOVERNED — Tier S (.claude/**), this record is the landing carrier; queue landing by the seat. Judged correct.
  • The item, read against the gate: before, 「@objectstack/spec 用 major」 — an instruction the per-PR Check Changeset job (scripts/check-changeset-no-major.mjs, the LAUNCH-WINDOW GUARD, live: .changeset/pre.json absent on main) refuses outright. After, 「用 minor,⛔ 不用 major:逐 PR 的 Check Changeset 跑 scripts/check-changeset-no-major.mjs,发射窗口期内拒收 major,照抄即红。破坏性语义由 BREAKING banner 承载」 — the level the binding disposition for PluginCapabilityManifestSchema / plugin-registry.zod.ts — the #13285 census closed all-zero across every reachable repo, so enforce-or-remove is now the open question #18623 ruling (5725503716) specified, the gate and job named so the next reader sees why, the FROM → TO mapping, the CHANGELOG.md rationale, the *-retired.md template pointer and the ADR-0087 disposition marker all kept. Nothing beyond the line's own subject was added. Judged correct.
  • Evidence, seat-run on this head in a detached worktree: check-skill-line-ratchet.mjs exit 0 (337 / 337; the six touched lines 98–120 B, the cap 120), check-skill-id-lint.mjs exit 0, check-governed-prose.mjs exit 0; merge-tree vs origin/main 57ceb9d: 0 conflicts; CI on ee26ec6: 34 check runs, 2 still running, 0 red. The dev's reproduction is the measurement the card asked for: a throwaway changeset with '@objectstack/spec': major through the gate → exit 1 「This PR introduces changeset(s) that declare a major bump.」; the same with minor → exit 0; both legs restored to a clean tree and proven so. The playbook's declared consumer test (retired-key-migrate-sentence.test.ts, 14 / 14) run under the shared lock; the derived union 18 / 18 exit 0 with --ran reconciled (a DERIVED zero), check:pm-dispatch-gates 1883 cases exit 0 captured.

② Semver level

None — nothing published (.claude/** is in no released package's files[]); skip-changeset on the PR is the correct declaration. Judged correct.

③ Boundary flags

open_questions empty. All four mechanism assumptions confirmed by measurement (the gate's live enforce arm and its exact refusal text; the ratchet's one ceiling row at 337, the 326 being the table-row width pin; 0 of 20 open PRs on the file; GOVERNED Tier S). Deviations read and accepted: the prerequisite-not-met first run of check:doc-formula-expressions rebuilt and re-run green; a mistyped pnpm alias recorded as the dev's own slip, not a reading; the slow gate detached and waited on in-round. Two noted, not filed items are negative results (no sibling site prescribes major; three of the six lines now sit at exactly 120 B — the next edit sheds before it adds), no carrier owed.

Implemented-by: claude/issue-19360-retirement-changeset-level-minor
Reviewed-by: session_017ETYWqMQD4qMtZzAGovWNi

VERDICT: PASS


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 20, 2026 22:33
@os-steve
os-steve added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 2951c0f Sep 20, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-19360-retirement-changeset-level-minor branch September 20, 2026 22:57
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…nsumers after the `PluginSecurityScanner` retirement (objectstack-ai#15932) (objectstack-ai#19610)

Fixes objectstack-ai#15932

Clause-②: yes

ADR-0049 enforce-or-remove. Executes the ruling on objectstack-ai#15932 (director
seat, decision batch objectstack-ai#65, 2026-09-07, maintainer verbatim 「同意」). This is
the second half of the `PluginSecurityScanner` retirement, whose live
record is **PR objectstack-ai#15930**. ⚠️ **Citation corrected:** the number that
landing was filed under — 14919, written bare on purpose — **no longer
resolves on the board** (404 at 2026-09-21T18:3xZ; lit control: the
neighbouring 14920 returns 200), so the digits are kept greppable while
the sigil is dropped, which is the same treatment this repo gave the
dead `[objectstack-ai#14423]` **docblock citation** that PR objectstack-ai#19609 repaired in
`packages/metadata/src/metadata-manager.ts`. ⚠️ **Carrier corrected:**
an earlier draft of this sentence called that precedent a `Blocked-by:
objectstack-ai#14423` line. There is no such line — `Blocked-by:.*14423` returns **0**
across `origin/main` (lit control: real `Blocked-by:` lines do exist, in
five files and more), and objectstack-ai#19609's own diff removes `* [objectstack-ai#14423] The
keyed plural read …` from a docblock. The precedent itself is real and
reads verbatim *"the card it was filed under — issue 14423, written here
without a leading hash because it no longer resolves"*; only its carrier
was misnamed. ⛔ No replacement number is guessed: objectstack-ai#15930 is the PR whose
patch did the work, ⛔ not a re-issued card. That change retired
`PluginSecurityScanner`, and its type-only import was the scan-result
family's only importer of any kind, so the schemas it fed went from one
type-only importer to **zero consumers of any kind** while staying fully
published.

## What is retired

| member | route |
| --- | --- |
| `KernelSecurityScanResult` (def + 3 exports) | whole-def removal,
`RETIRED_DEFS_BY_MAJOR[18]` |
| `KernelSecurityVulnerability` (def + 3 exports) | whole-def removal,
`RETIRED_DEFS_BY_MAJOR[18]` |
| `PluginSecurityManifest.scanResults` | `retiredKey()` tombstone,
`RETIRED_KEYS_BY_MAJOR[18]` |
| `PluginSecurityManifest.vulnerabilities` | `retiredKey()` tombstone,
`RETIRED_KEYS_BY_MAJOR[18]` — see **Scope** below |
| `PluginQualityMetrics.securityScan` | `retiredKey()` tombstone,
`RETIRED_KEYS_BY_MAJOR[18]` |

Two routes because the two questions have different answers. Nothing
parses the two **defs**, so there is no author a prescription could
reach and a tombstone would be noise. The three **keys** sit on shapes
that are not `.strict()`, where a bare deletion is a silent strip
(ADR-0104) — so each becomes a `retiredKey()` tombstone, audible in both
channels: `tsc` (input type `never`) and the parse, which raises the
prescription itself.

**No D2 conversion.** A plugin security manifest and a plugin registry
entry are package artifacts a publisher ships — never stack collection
members, never stored `sys_metadata` rows — so the conversion chain has
no seam that would see one. That is the disposition the sibling
`kernel-plugin-security-durations-unit-in-key` entry already records for
this same manifest. The D3 semantic entry
`plugin-security-scan-result-surface-retired` carries the judgement.

## Premise, re-measured first-hand on `origin/main` @ `236cec19a5`

| reading | value |
| --- | --- |
| `KernelSecurityScanResult` / `KernelSecurityVulnerability` in
`packages/**/*.ts` outside the declaring module | **0** |
| LIT CONTROL — `PluginSecurityManifest` inside
`plugin-security-advanced.zod.ts` | **5** ⇒ the file is greppable, so
the zero is a reading |
| `packages/core/src/security/security-scanner.ts` | **absent** ⇒ the
`PluginSecurityScanner` retirement (PR objectstack-ai#15930) landed |
| `PluginQualityMetrics.securityScan` in `packages/**/*.ts` |
`plugin-registry.test.ts` only — the spec's own self-test |
| objectui at the pinned sha `87af769e` — does it import any of this? |
**0** hits; LIT CONTROL: `@objectstack/spec` is imported there ⇒ no
sibling fix and no pin bump are owed |

**The authorable-row count is 27, not the 22 the card carried.**
Measured with the playbook's instrument on
`authorable-surface/kernel.json`: 8 rows for `KernelSecurityScanResult`,
17 for `KernelSecurityVulnerability`, plus
`PluginSecurityManifest:scanResults` and
`PluginQualityMetrics:securityScan` — 28 counting the forced-consequence
`PluginSecurityManifest:vulnerabilities`. The disagreement is reported,
not reconciled: the 22 is superseded, and the FOLLOW-UPS row now says
so.

## Scope — one key outside the four names, reported rather than absorbed

`PluginSecurityManifest.vulnerabilities` is **not** one of the four
names the ruling listed. It is a forced consequence: it was an array of
`KernelSecurityVulnerability` and the last authorable referent of a def
the ruling retires by name, so it cannot outlive that def, and keeping
the def alive only to carry it would be keeping the retired family alive
under a second name. It is not a neighbour retired by proximity — the
fence's stated concern — and it is named here, in the registry entry, in
the changeset and in the hand-back.

⛔ **The outstanding carve-out is ONE enum member wide, not three — and
it is NOT recorded as checked.**

⚠️ **Coordinates corrected.** The ruling made three carve-outs
conditional on a producer grep of `objectstack-ai/cloud`. Two of the
three no longer exist in this tree, so only one is still outstanding:

| carve-out the ruling named | state at head `3e0a06d0b5` |
|:--|:--|
| marketplace `'scanning'` status, `marketplace.zod.ts` | **LIVE**, one
hit in any `.zod.ts`, at `marketplace.zod.ts:348` — the sole outstanding
carve-out |
| `marketplace-admin.zod.ts` | **file absent from the tree**; that
family's disposition was decided on objectstack-ai#16526 |
| incident `'malware'` type, `incident-response.zod.ts` | **file absent
from the tree** — the incident family was retired whole by objectstack-ai#15513, ruled
**2026-09-05**, two days BEFORE the ruling that made `'malware'`
conditional; `malware` returns **0** in any `.zod.ts` |

Instrument controls, so the two zeros are readings rather than a dead
grep: `marketplace*.zod.ts` on the same `find` returns
`marketplace.zod.ts`, and `malware` on the same grep returns 7
non-`.zod.ts` files (ADRs, design docs, records). An earlier draft of
this body named all three files in the present tense; that was wrong and
is retracted here.

`objectstack-ai/cloud` is not reachable from this session, so the
producer question for `'scanning'` is genuinely **NOT MEASURED**. ⛔ The
absence of these names from this diff is not evidence about them.

## Breaking, for a population that is not measured

`@objectstack/spec` is published, so removing six exports and three
authorable keys is breaking for consumers no download, dependent or
source telemetry was consulted for — exactly as that retirement's own
changeset (PR objectstack-ai#15930) says of its own three exports. That was an input
to the ruling, not a reason to soften the removal. No deprecation window
(maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进」). The release note is
written centrally; `content/docs/releases/` is untouched.

⚠️ **Runtime behaviour is deliberately unchanged.** Nothing ever read
any of these keys, so deleting one removes no check that was running. A
consumer that gated on `securityScan.passed === true` was gating on
nothing.

## ⚠️ Changeset level — the ruling says `major`, a live gate refuses it

The ruling and the dispatch both say **`major`**.
`scripts/check-changeset-no-major.mjs` hard-refuses a `major` bump for
the duration of the launch window (every publishable package is in one
Changesets `fixed` group, so one `major` promotes ~70 packages), and the
retirement playbook was corrected to say so in objectstack-ai#19446, which is on
`main`. A `major` changeset here is a guaranteed-red PR that cannot
land.

This PR therefore ships **`minor` + a BREAKING banner carrying the FROM
→ TO mapping and the one-line fix** — the carrier the window designates
for breaking-ness — plus the ADR-0087 disposition marker.
`check-changeset-no-major.mjs` and `check-adr-0087-registration.mjs` are
both green on it. **This is flagged, not silently chosen:** if the seat
wants the literal `major`, that is a decision about the launch-window
guard, not about this diff.

## Verification

| check | result |
| --- | --- |
| `pnpm --filter @objectstack/spec build` | **pass** (after the two
deletion gates fired and were answered, below) |
| `pnpm --filter @objectstack/spec test` | **pass** — 510 files, 14897
passed, 1 todo |
| `pnpm --filter @objectstack/spec typecheck` | see the hand-back |
| `pnpm --filter @objectstack/spec check:generated` | **pass** — 15
artefacts; 5 were stale and were regenerated by `--fix`, never
hand-edited |
| `check-adr-0087-registration.mjs` | **pass** — 1 declared-breaking
changeset, disposition `registered
plugin-security-scan-result-surface-retired` |
| `check-changeset-no-major.mjs` | **pass** — no `major` bump introduced
|

**Two gates fired on the way, and both were answered rather than routed
around.** The json-schema manifest deletion gate refused the two
vanished defs until their keys left `json-schema.manifest/kernel.json`
*and* each was declared in `RETIRED_DEFS_BY_MAJOR`; the
authorable-surface deletion gate then refused the 25 orphaned key rows
until they left `authorable-surface/kernel.json` in the same commit.
That sequence is the removal's own evidence and is why the ratchets
moved. ⛔ `authorable-surface.base.json` was not touched.

**Reverse verification — the refusal pin can fail.** The `scanResults`
tombstone was ablated to `z.array(z.unknown()).optional()` with
`scripts/ablation-replace.mjs`, which proved the mutation on disk
(anchor 1 → 0, blob `0f3af37f5068` → `969efcdaa46a`) before running
anything. Result: exactly one test failed — the `scanResults` refusal
pin — and the other four passed. The restore leg verified blob == HEAD
and `git diff HEAD` empty.

## Acceptance notes

Noted, not filed — observed while executing, outside this card's scope,
and no in-flight PR or person is known to be heading for these files:

- `packages/spec/src/kernel/plugin-security.zod.ts` declares a
**parallel, unprefixed** scan-result family —
`SecurityVulnerabilitySchema` and `SecurityScanResultSchema`,
near-duplicates of the pair retired here, with their own self-test in
`plugin-security.test.ts`. It is outside the four names and is
deliberately untouched; the pin test asserts both are still exported, so
the fence is machine-checked rather than described. Whether it is live
is a separate census this card did not take.
- `plugin-security-advanced.test.ts`, the declaring module's own
self-test, contained **zero** references to the scan-result family. The
premise called `plugin-security.test.ts` the family's self-test; it is
in fact the *other* family's. The retired family had no self-test at all
— a reading slightly stronger than the card's.

PR body maintained by the `domain:spec` execution seat, session
`session_01UDXER3sdqfeVYpEWZs5mZx`; the diff is the dev's.

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

---------

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

Development

Successfully merging this pull request may close these issues.

1 participant