Skip to content

feat(spec)!: retire the plugin-security scan-result surface — zero consumers after the PluginSecurityScanner retirement (#15932) - #19610

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-15932-retire-scan-result-surface
Sep 22, 2026
Merged

os-warren merged 3 commits into
mainfrom
claude/issue-15932-retire-scan-result-surface

Conversation

@os-warren

@os-warren os-warren commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #15932

Clause-②: yes

ADR-0049 enforce-or-remove. Executes the ruling on #15932 (director seat, decision batch #65, 2026-09-07, maintainer verbatim 「同意」). This is the second half of the PluginSecurityScanner retirement, whose live record is PR #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 [#14423] docblock citation that PR #19609 repaired in packages/metadata/src/metadata-manager.ts. ⚠️ Carrier corrected: an earlier draft of this sentence called that precedent a Blocked-by: #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 #19609's own diff removes * [#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: #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 #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 #16526
incident 'malware' type, incident-response.zod.ts file absent from the tree — the incident family was retired whole by #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 #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 #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

ADR-0049 enforce-or-remove; maintainer ruling 2026-09-07 (director seat,
decision batch #65), adopted verbatim. The second half of #14919: that change
retired `PluginSecurityScanner`, whose type-only import was the family's only
importer of any kind, leaving `KernelSecurityScanResult`,
`KernelSecurityVulnerability`, `PluginSecurityManifest.scanResults` and
`PluginQualityMetrics.securityScan` fully published with zero consumers and no
`.parse`/`.safeParse` site anywhere.

The two defs leave the build whole (`RETIRED_DEFS_BY_MAJOR[18]`) because nothing
parses them. The authorable keys are `retiredKey()` tombstones registered in
`RETIRED_KEYS_BY_MAJOR[18]` — neither carrying shape is `.strict()`, so a bare
deletion would strip an authored key in silence (ADR-0104).
`PluginSecurityManifest.vulnerabilities` is a forced consequence: it was the last
authorable referent of `KernelSecurityVulnerability`.

No D2 conversion — a plugin security manifest and a plugin registry entry are
package artifacts a publisher ships, never stack collection members and never
stored `sys_metadata` rows. The D3 semantic entry
`plugin-security-scan-result-surface-retired` carries the judgement.

The marketplace `'scanning'` status and the incident `'malware'` type are
deliberately untouched: the ruling made them conditional on a producer grep of
`objectstack-ai/cloud`, which is not reachable from this session.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 14 documentable anchor(s). ⚠️ 11 changed file(s) yielded no anchor (packages/spec/api-surface/kernel.json, packages/spec/authorable-defaults/kernel.json, packages/spec/authorable-surface/kernel.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/cluster.mdx (via RETIRED_DEFS_BY_MAJOR (symbol, a top-level const object))
What this run could not see
  • 11 changed file(s) yielded no anchor (packages/spec/api-surface/kernel.json, packages/spec/authorable-defaults/kernel.json, packages/spec/authorable-surface/kernel.json, …) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

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

Which tree this was computed on

This run read content/docs from 7efd0522b900787f168562d2ccf4c4d81194f9e5 — the merge of head 3e0a06d0b521e0df9d54dd2356b860f84d9a45ab into base 5c5b67fc4140f76ca3158acea9e0845d9eebfad8, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7efd0522b900787f168562d2ccf4c4d81194f9e5 && git checkout 7efd0522b900787f168562d2ccf4c4d81194f9e5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 3e0a06d0b521e0df9d54dd2356b860f84d9a45ab && git checkout -B drift-repro 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 && git merge --no-ff 3e0a06d0b521e0df9d54dd2356b860f84d9a45ab

node scripts/docs-audit/affected-docs.mjs --json 5c5b67fc4140f76ca3158acea9e0845d9eebfad8

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ger serves

`check:issue-citations` went red at 0a1bac8 with eight dangling sites across
five files: issue 14919 at six of them and issue 8715 at two. Measured cause,
from the gate's own `--probe-cause`: deleted, all eight — not transferred, not
mistyped. Probed with a lit control beside each, since both dead numbers sit
next to live ones: 8714 404 / 8715 404 / 8716 200, and 14918 404 / 14919 404 /
14920 200. Scattered pairs, not a contiguous band, which is what
deletion-by-author looks like. Both were live references when the prose was
written.

⛔ No number is guessed and none is swapped for a plausible neighbour. Each site
keeps its number in prose and now says it no longer resolves, then names a
record that DOES — verified by probe, not inferred:

  - issue 14919 -> PR #15930, `feat(core)!: retire PluginSecurityScanner`,
    merged 2026-09-05, whose body opens with a closing line naming that very
    issue number. Probe: 200.
  - issue 8715  -> #11825, the half of the pair this tree cites together that
    still resolves, and the same whole-def disposition shape. Probe: 200.

The `#` sigil is what the gate judges (`CITATION_RE`); a bare number in prose is
not a citation, so the number survives verbatim and the reference stops
dangling. `migrations/registry.ts` is GENERATED and was NOT hand-edited: its
three copies come from the two entry files, re-emitted by
`gen:migration-registry`. The module docblock feeds a reference page, so
`check:generated --fix` regenerated
`content/docs/references/kernel/plugin-security-advanced.mdx` through `gen:docs`.

⛔ Nothing else moves: no schema, no key, no registry entry, no changeset level,
no authorable row. Comments and the prose they generate, only.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
…tes were stale

From the at-tier review, item ③.3. The POSTURE on the carve-out was right and is
kept verbatim: the marketplace 'scanning' status stays untouched and ⛔ NOT
recorded as checked. What was wrong is a COORDINATE — two of the three files
those records name no longer exist, so the present-tense clause "stay exactly as
they are, unremoved" was an assertion nobody had measured, and false for one half.

Verified here by shape rather than taken on report, since stale coordinates are
the defect being repaired. Tree entries on this branch AND on origin/main:

  marketplace.zod.ts        1   ('scanning' live)
  marketplace-admin.zod.ts  0
  system/incident-response.zod.ts  0
  'malware' in any *.zod.ts 0   (lit control: 'scanning' returns a live
                                 declaration, so the zero is a reading)

The incident 'malware' type was a member of system/IncidentCategory, and the
whole incident-response family was retired by #15513 — maintainer ruling
2026-09-05, two days BEFORE the 2026-09-07 ruling that made it conditional.
marketplace-admin.zod was deleted outright with the cloud subpath (#16526).

⇒ The conditional question is ONE enum member wide, not three, and the records
now say so — so whoever finally runs the objectstack-ai/cloud producer grep knows
its real size. ⛔ The 'scanning' half stays genuinely unmeasured; it does not
become "checked and clean".

Also repaired in the same prose, same defect class as the previous commit: ten
citations of the deleted issue 14919 that this card's own records were handing
on. None was visible to check:issue-citations — FOLLOW-UPS.md and .changeset are
not judged surfaces, and the semantic entry's are inside string literals, which
the comment-prose projection blanks. The changeset is what an upgrading agent
greps after a tombstone error, so a dead number there is the defect the gate
exists to prevent, one surface out of its reach. Same repair form: the number is
kept in prose, said to no longer resolve, and the live record named.

⛔ Nothing about the retirement changes: no schema, key, def, registry entry,
changeset level, authorable row or generated artefact. check:generated reports
all 15 up to date.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
@os-warren os-warren changed the title feat(spec)!: retire the plugin-security scan-result surface — zero consumers after #14919 (#15932) feat(spec)!: retire the plugin-security scan-result surface — zero consumers after the PluginSecurityScanner retirement (#15932) Sep 21, 2026
@os-warren
os-warren marked this pull request as ready for review September 21, 2026 18:40

Copy link
Copy Markdown
Collaborator Author

Ready, green, reviewed — and the last step is blocked with no channel

All three landing preconditions are met and recorded on the card:

  • ① at-tier contract review: PASS, with the served tier measured from the reviewer's transcript per assistant row, and the tier constant re-read from origin/main at 2026-09-21T18:06Z before the round was dispatched.
  • ② check-clause2-carriers --pair: EXIT=0, exit code captured before any pipe.
  • ③ CI by job conclusion, latest run per check NAME: 35 distinct names, 0 failure, 0 cancelled; every skip is on the EXPECTED_SKIPS roster. ⛔ No aggregate roll-up was read as the verdict.
  • Governance: ungoverned, 0 of 6 — predicate executed (GOVERNED_SURFACES + governedPathsIn imported from origin/main) rather than recalled, with a lit control (6/6) and a near-miss control (0/8). ⛔ .github/CODEOWNERS was not consulted; it is not a governed surface.

This PR has been flipped draft → ready, confirmed by GET /pulls/{n} returning draft: false — ⛔ not by the POST's status code.

⛔ auto_merge could NOT be enabled. The seat's session permission classifier refused the call, and there is no second channel: the MCP enable_pr_auto_merge tool is on this session's deny roster, and ⛔ working around a classifier refusal is not a channel. Recorded rather than retried, per the standing rule.

⛔ The seat will not merge this PR, enqueue it by hand, or submit an approving review on it. The only remaining act is enabling auto-merge with merge_method: SQUASH so the merge queue lands it — ⛔ never a direct merge, never a queue bypass.

Action needed from the maintainer or a seat with the channel: enable auto-merge (SQUASH). Everything else here is finished.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

✅ Correction — this PR is NOT blocked any more. It is in the merge queue.

domain:spec execution seat 2, session session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T00:3xZ.

The maintainer re-granted the permission and the call was retried. ⛔ The earlier standing-down note on this PR — "auto_merge could not be enabled … the only remaining act is enabling auto-merge" — is now false and is retracted here.

⚠️ And the seat's first read-back of that retry was WRONG. Recording it, because it is the more useful half.

PUT .../ccr/auto_merge returned 200 {"enabled":true,"merge_method":"squash"}. The seat then read GET /pulls/{n} and saw auto_merge: null on all four, and was one step from reporting "returned 200 but stored nothing" — the known 「状态码不作数」 failure shape.

That reading was the wrong instrument. On a repository with a merge queue, the action does not populate the auto_merge attribute at all — it enqueues the PR. The repo's own channel table says so in as many words: 「问本仓 auto-merge 是否经队列,答案来自尝试动作,不来自属性字段」, and its criterion ② is the added_to_merge_queue timeline event. The seat read the field the table warns has no discriminating power, ⛔ not the event the table names.

The evidence, on two independent instruments:

  1. Timeline — added_to_merge_queue on all four, at 00:35:03 / 00:35:05 / 00:35:06 / 00:35:08Z, the exact moment of the four PUTs.
  2. git, zero quota — the queue branches exist on origin and are chained, each built on the previous one's result:
gh-readonly-queue/main/pr-19602-1c16889a…  -> dc9e29bb
gh-readonly-queue/main/pr-19609-dc9e29bb…  -> 71f94e29
gh-readonly-queue/main/pr-19610-71f94e29…  -> 157c62f9
gh-readonly-queue/main/pr-19493-157c62f9…  -> 85265e6f

⇒ queue order #19602 → #19609 → #19610 → #19493, each tested against the cumulative result of the ones ahead of it. That is the merge queue doing its job, and it is ⛔ not a bypass: the seat did not merge, did not enqueue by hand, and submitted no approving review.

What happens next

Each PR merges as its queue branch goes green. ⚠️ A queue branch can still fail — it tests a combination that never existed before — and if it does, the PR is ejected and that is this seat's to diagnose, ⛔ not a re-enqueue on reflex.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Carrier stripped on a PASS that is on record — and the seat's own enqueue error, stated plainly

domain:spec execution seat 2, session session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T00:5xZ.

⛔ What I got wrong

I enqueued this PR at 00:35Z while needs:contract-review was still hung on both carriers. The merge queue's Governed Surface Queue Guard refused the queue build and ejected it, exit code 6, verbatim:

#19610 — ⛔ CARRIES \needs:contract-review` — this pull request may not be in the queue.`

The guard was right and the rule it cites is one I had already read: 「双肢命中任一 ⇒ 无席内条款②复核 PASS 在案 ⛔ 禁止入队」. I ran the three landing preconditions (at-tier PASS, --pair EXIT=0, CI green by job conclusion) and then did ready + enqueue — and skipped 剥标, which 「PASS ⇒ 同席剥标并引记录、ready、auto-merge」 puts before the enqueue, not after.

⚠️ Why --pair did not catch it, so nobody re-derives this: --pair EXIT=0 reported 「the needs:contract-review LABEL is in the same state on both LABEL carriers」. That is a statement about the two carriers agreeing with each other — ⛔ not a statement that the carrier should be gone. I read a consistency row as a release. Two distinct questions, one of which no gate was asking.

✅ Why stripping now is the sanctioned act and ⛔ not a way past the check

The guard's own text draws the line: "⛔ Stripping the label to get past this check, with no verdict on record, is the defect this leg was built from — not a way through it."

There is a verdict on record, and it is cited here rather than asserted:

At-tier contract review, VERDICT: PASS — comment 5765611333 on card #15932, head 3e0a06d0b521e0df9d54dd2356b860f84d9a45ab (this PR's current head, unmoved), served tier measured from the reviewer's transcript at 115/115 rows at CONTRACT_REVIEW_TIER. It was a scoped re-review discharging the single fail basis of the prior record 5765428945. Seat disposition adopting it verbatim: comment 5765643574.

⇒ the condition 「PASS ⇒ 同席剥标并引记录」 is satisfied. The carrier is stripped from both carriers — this PR and its card — by the same seat that adopted the verdict, in the four-step label write, with read-back.

What happens next

Carrier stripped on both sides → --pair re-run → re-enqueued. ⛔ The seat does not merge by hand, does not bypass the queue, and submits no approving review. If the queue ejects it again, that is a different failure and gets its own diagnosis — ⛔ no reflex re-enqueue.


Generated by Claude Code

@os-warren
os-warren added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit 744a0a3 Sep 22, 2026
61 checks passed
@os-warren
os-warren deleted the claude/issue-15932-retire-scan-result-surface branch September 22, 2026 01:28
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…face (objectstack-ai#19054) (objectstack-ai#19618)

Fixes objectstack-ai#19054

Clause-②: no

Executes the maintainer ruling recorded verbatim on the card:
「organizationField 撤出可授权面 同意你的建议」. `object.tenancy.organizationField`
leaves the authorable surface at protocol 18 (ADR-0049
enforce-or-remove). The divergence the key existed for is **not**
retired — only its authorability.

## What the key was, and why it could never be more than one table's
fact

It answered "which column says who this platform row is ABOUT", where
`tenancy.tenantField` answers "what is this object WALLED by". The
spec's own docblock stated the consequence: *"For ordinary objects the
two coincide and `organizationField` is never needed."* Re-measured at
head before this branch: the entire repository declared it **once**, on
`packages/platform-objects/src/identity/sys-api-key.object.ts` — the
better-auth credential table — and zero business objects declared it.
Its readers were three platform-row writers, scope-pinned by name, so an
application declaration was inert by construction while still being
authorable on every object.

## The shape of the change

`TenancyConfigSchema` is a `strictObject`, so this is the
strict-deletion route:

- the key is deleted from the shape, and `TENANCY_RETIRED_KEY_GUIDANCE`
gains its prescription beside the two v15.0 precedents
(`tenancy.strategy`, `tenancy.crossTenantAccess`). Authoring it is now
**refused with the prescription**, not stripped
- D2 conversion `object-tenancy-organization-field-removed` (`toMajor:
18`, `retiredFromLoadPath: true`) strips it from authored sources and
stored `sys_metadata` rows; D3 wires it into the protocol-18 chain step;
`RETIRED_KEYS_BY_MAJOR[18]` declares
`data/TenancyConfig:organizationField`
- the `authorable-surface/data.json` row is deleted in this same commit
— the strict route's tripwire, with the build computing the
guidance-route proof for itself
- the liveness ledger row is **deleted** (not tombstoned): the key
leaves the walked shape entirely, so a surviving row would read as an
ORPHAN. `liveness/README.md`'s `object` row records why, and
`state-counts.md` moves `object` 51 → 50 live

Limb 0 of the shared resolver now reads a platform-internal table
instead of a declaration:

```
PLATFORM_STAMP_ORGANIZATION_COLUMNS = { sys_api_key: 'active_organization_id' }
```

keyed by the object's registered NAME, read by the STAMP face alone.
`resolveRecordOrganizationField` and `createRecordOrganizationResolver`
keep their signatures — `check:api-surface` is byte-identical — and the
engine-bound face passes the name it was asked about rather than reading
`objectDef.name`, because several engine doubles in this monorepo return
a bare `{ tenancy, fields }` map with no `name`.

## The two facts the card said must survive

1. `sys_api_key` is `managedBy: 'better-auth'`, so
`resolveInjectedSystemColumns` bails before tenancy is consulted and no
`organization_id` is injected. Pinned, and the pin is now stated as the
better-auth bail rather than as key-blindness
(`packages/spec/src/data/injected-system-columns.test.ts`).
2. ⛔ The column is **not** renamed to `organization_id`. In this
platform "has an `organization_id` column" IS the wall, so the rename
would wall the credential table on an equality that excludes NULL.
`plugin-security`'s Layer-0 suite pins both halves against the real
shipped object.

The stamp/wall divergence pin is green:
`resolveRecordWallOrganizationField` never read the key and is
untouched.

## Base merge after objectstack-ai#19600 landed, and the tombstone version it exposed
(2026-09-23)

The collision partner this section used to name, objectstack-ai#19610, has landed, and
so has objectstack-ai#19600 (card objectstack-ai#15178, merged at 03:04:25Z as `d0f1845657`). objectstack-ai#19600
is one of the three PRs in the serial on
`packages/spec/src/migrations/registry.ts` described in notice
`5780847968`. After it landed, this PR read `dirty`.

⚠️ **The sentence that stood here before was wrong in part.**
`registry.ts` is only partly generated. Its `<os-generated …>` regions
are regenerated. But `registry.ts:18-38` says outright that each step's
`rationale` and `conversionIds` are **hand-written and merge as text**.
No gate turns red when a paragraph is dropped from them.

The merge was done on the branch with no rebase and no force-push. It is
three commits:

1. **`bde765bf05` merges `origin/main` at `67add1301a`.** It is a merge
commit with parents `7cc0ca1b3d` and `67add1301a`, and it resolved two
textual conflicts by hand.
- `step18.rationale` keeps objectstack-ai#19600's paragraph verbatim. Its last line is
re-terminated with a trailing space, and this PR's paragraph is appended
after it.
- `step18.conversionIds` keeps both
`'translation-per-app-settings-removed'` and
`'object-tenancy-organization-field-removed'`. That gives 33 ids, 33 of
them distinct.
- `packages/spec/src/conversions/registry.ts` keeps both D2 conversions
in `CONVERSIONS_BY_MAJOR[18]`, in landing order. The file's own rule is
「ordering within a major is application order」.
   - The generated regions were regenerated and never hand-merged.
2. **`9d5fb0ba5f` is regeneration only.** It regenerates the two
reference pages that `os-regen-merge.sh` had deferred.
3. **`3fb1a4994c` is a CONTENT change, not merge resolution.** The merge
brought in `check:future-spec-major` (objectstack-ai#19655), which landed after this
PR's old base, and CI went red on two sites. Under ADR-0087 (amended
2026-09-13), a tombstone names the npm release it ships in, never the
protocol major. This retirement ships `minor`, so it lands in 17.x. The
commit therefore changes the prescription at
`packages/spec/src/data/object.zod.ts:540` and its refusal pin at
`packages/spec/src/data/object.test.ts:1947` from `@objectstack/spec 18`
to `@objectstack/spec 17`. The protocol-major references (`toMajor: 18`,
`RETIRED_KEYS_BY_MAJOR[18]`, `os migrate meta --from 17`) are unchanged,
because the gate permits them.

**Measured by the dispatching seat against the committed trees, not
taken from the dev's narration:**

- The merge commit against the main parent `67add1301a`: 2 files,
+128/−1. Every hunk is this PR's.
- The merge commit against the branch parent `7cc0ca1b3d`: 2 files,
+656/−28. Every hunk is main's.
- On the merged `registry.ts`, each of the following appears exactly
once:
  - the shared closing line `dataset. '`
  - objectstack-ai#19600's paragraph
  - objectstack-ai#19600's last line, continuing with a trailing space
  - each of the two `conversionIds`
  - the retired-key row
- A dark control phrase appears 0 times.
- This PR's paragraph follows objectstack-ai#19600's.

⚠️ **The earlier contract review (`5765681233`) names head
`7cc0ca1b3d`.** Commit `3fb1a4994c` changes a string that review's AC2
pinned, so this head move is **not** regeneration-only, and the earlier
record does not govern the new head. `check-clause2-carriers.mjs --pair
19618` confirms it: exit 4, C6. A fresh contract review of the current
head is owed before landing.

## Verification

**Re-measured at the current head `3fb1a4994c`, after the base merge.**

- **CI**, measured by the seat from the head's check-runs: 35 checks,
latest run per name. 33 success, 2 skipped (`Console Pin Gate`,
`Packed-tarball smoke (opt-in)`), 0 failed. All five type-check lanes
pass. The legacy commit status is `success`.
- **Suites and gates**, from the os-dev report `5788876254`, which the
seat did not re-run:

  | package or gate | result |
  | --- | --- |
  | `@objectstack/spec` | 516 files, 15065 passed, 1 todo |
  | `@objectstack/metadata-core` | 16 files, 285 passed (unchanged) |
  | `@objectstack/plugin-audit` | 25 files, 363 passed (unchanged) |
  | typecheck, spec and metadata-core | exit 0 |
  | `check:generated` | 15 of 15 current |
  | `check:future-spec-major` | exit 0 |
| `dispatch-gates --ran` | 115 accounted: 112 run with exit 0, 3 NOT
MEASURED |

- The spec suite grew from the pre-merge 509 files / 14898 tests. The +7
files are exactly the seven spec test files `main` added in the merged
range.
- `check:future-spec-major` was checked against a lit control:
re-planting `18` makes it exit 1 with exactly one problem.
- The 3 NOT MEASURED gates were refused for build prerequisites (exit
3). They run in CI jobs that build first.

The tables below are the **pre-merge** readings, kept as history:

Every number in the tables below was taken at `7cc0ca1b3d`, the
pre-merge head.

**Reverse verification (both legs committed first, both restored
byte-identically, both via `scripts/ablation-replace.mjs`):**

| ablation | anchor → replacement landed | result |
| --- | --- | --- |
| the platform stamp row renamed (`sys_api_key` → `sys_api_key_ABLATED`)
| anchor 1 → 0, blob `0be02fdcc6b7` → `21856a4a20d0` | **4 of 19**
metadata-core tests RED; restore `blob == HEAD`, `git diff HEAD` empty |
| the prescription's first clause replaced with placeholder text |
anchor 1 → 0, blob `2e9e19825ef6` → `eea8b4c7f061` | refusal pin RED
with `expected 'Unrecognized key(s) on 'tenancy': 'or…' to contain
''tenancy.organizationField' was remov…'` — the pin measures the
PRESCRIPTION, not merely that parse throws; restore verified the same
way |

**Suites** (`pnpm test` per package, through the shared verify lock):

| package | result |
| --- | --- |
| `@objectstack/spec` | 509 files, 14898 passed, 1 todo |
| `@objectstack/metadata-core` | 16 files, 285 passed |
| `@objectstack/platform-objects` | 53 files, 848 passed |
| `@objectstack/plugin-security` | 117 files, 2249 passed |
| `@objectstack/plugin-audit` | 25 files, 363 passed |

**Typecheck:** `@objectstack/spec`, `@objectstack/metadata-core`,
`@objectstack/platform-objects`, `@objectstack/plugin-audit`,
`@objectstack/plugin-security` — all green, test layers included.

**Gates:** `node scripts/pm/dispatch-gates.mjs --ran` reconciles **114
derived / 114 run / 0 NOT-MEASURED / 0 UNRUN** against this diff. `pnpm
--filter @objectstack/spec check:generated` reports 15 of 15 artifacts
current. `pnpm lint` (`eslint . --no-inline-config`, the whole repo, no
narrowing) exits 0.

**The three sanctioned platform-row writers' pins stayed green
UNTOUCHED**, as the card required — `plugin-approvals` (`approval-node`,
`backfill-platform-row-organizations`), `service-automation`
(`suspended-run-store`), `service-storage`
(`backfill-sys-file-organizations`): 33 + 52 + 15 tests, zero edits. The
`driver-sql` and `trigger-schedule` read-neutrality suites are green
untouched too (36 + 61).

## Acceptance notes

**Declared widening of the dispatched file surface — three files, each
because this diff makes a statement in it FALSE.** None was edited for
tidiness; each is named with the measurement that forced it.

1. `packages/spec/src/shared/alias-integrity.test.ts` — RED. It pins the
exact key set of the folded `tenancy` guidance table: `expected [
'crossTenantAccess', …(2) ] to deeply equal [ 'crossTenantAccess',
'strategy' ]`. The retirement adds the third row, which is the only
channel the refusal travels on.
2. `packages/plugins/plugin-security/src/tenant-layer.test.ts` — RED. It
asserted the declaration off the shipped object: `expected undefined to
be 'active_organization_id'`. Rewritten to pin what this suite actually
owns: the stamp column exists as a field, `organization_id` does not,
and `tenancy` is exactly `{ enabled: false }`.
3. `packages/plugins/plugin-audit/src/audit-writers.test.ts` — RED, two
cases, and one of them is a **finding the card asked for**. See the next
section.

A fourth file,
`packages/spec/src/automation/schedule-organization.zod.ts`, carried a
docblock asserting "`tenancy.organizationField` wins there" — a
statement this diff falsifies, and one that **publishes**, into
`content/docs/references/automation/schedule-organization.mdx`.
Corrected in prose; the generated page follows.

**⭐ Finding — one sanctioned writer's pin DID have to be edited, and the
reason is not cosmetic.** Two `plugin-audit` cases went red:

- *"`organizationField` outranks `tenantField`"* pinned the precedence
on `crm_lead`, an object declaring BOTH keys, with the comment *"No
shipped object declares both; this pins the precedence so the day one
does is not a coin flip."* After the retirement no application can
declare a stamp column at all, so the question is **closed rather than
answered**. The case is rewritten to pin the closed set — an application
object carrying a lookalike column stamps from its own wall.
- *"control: without the declaration the credential table still stamps
the actor's org"* fed a `sys_api_key` schema with **no** `tenancy` block
and pinned the actor's org, proving the stamp came from the declaration
rather than from a column-name heuristic. Keying limb 0 by object name
makes that shape stamp `active_organization_id` instead. This is a
**real, deliberate behaviour change on a shape that is not reachable for
the shipped table** — `sys_api_key` is `managedBy: 'better-auth'` and
`protection: { lock: 'full' }`, so its block cannot be dropped. Recorded
in the rewritten case rather than smoothed over, and the `objectstack-ai#5315` guard
that did not move (column absent ⇒ fall through to the actor's org) is
pinned beside it.

**⭐ Finding — two issue citations this repo carries in these files do
not resolve.** `check-issue-citations --base origin/main` judged 12
citations this change adds and refused all 12: `objectstack-ai#8778` and `objectstack-ai#8707` are
`allocated-but-absent` (minted, ≤ frontier 19616, not on the board;
deleted vs transferred NOT MEASURED). Both are pre-existing text — the
diff only re-adds them by rewriting the docblocks around them. Following
the gate's own prescription, the added lines now name the rulings in
prose and cite the cloud record that does resolve. ⛔ No number was
guessed. The standing occurrences on unchanged lines elsewhere in the
tree are untouched and are not this PR's to repair.

**Stale-but-green fixture residue, deliberately NOT touched** (green
today, outside the dispatched surface, and not a defect — the fixtures
feed drivers and engine doubles, never `TenancyConfigSchema`):
`packages/drivers/driver-sql/src/sql-driver-tenant-scope.test.ts`,
`packages/triggers/trigger-schedule/src/time-relative-trigger.test.ts`,
`packages/plugins/plugin-approvals/src/{approval-node,backfill-platform-row-organizations}.test.ts`,
`packages/services/service-automation/src/suspended-run-store.test.ts`,
`packages/services/service-storage/src/backfill-sys-file-organizations.test.ts`
still author `tenancy: { …, organizationField: … }` in raw
object-definition fixtures. Their assertions remain true; what has gone
vacuous is the *claim* that the driver / wall face is neutral about a
key nobody can write. `packages/lint/src/validate-object-field-refs.ts`
carries the key in a list of scalars it deliberately does not judge.

**No tree-scoped absence pin is added**, and that is a decision rather
than an omission: the playbook's tree-scoped form would have to declare
its radius in `scripts/cross-package-test-inputs.mjs` and `turbo.json`,
both far outside this card's surface, and it would go red against
exactly the six inert fixtures above. The absence is instead enforced
where it is cheap and exact — `authorable-surface/data.json` has no row,
and `check:authorable-surface` is the gate over that baseline.

## Clause-② re-judged from the diff

`no`, and the diff agrees. No hunk puts a new key on a published
payload: the guidance row is a prescription string, the
`RETIRED_KEYS_BY_MAJOR` / `CONVERSIONS_BY_MAJOR` entries are registry
rows, `json-schema/**` **loses** a key, and `api-surface/` is
byte-identical — `resolveRecordOrganizationField`'s signature is
unchanged. This is a pure retirement, which narrows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2

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

---------

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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: the plugin-security-advanced scan-result surface has ZERO consumers after #14919 — 22 published authorable rows with no author and no parser

2 participants