Skip to content

fix(plugin-security): the five remaining seeder refusals reach the author - #18564

Merged
os-project-manager merged 5 commits into
mainfrom
claude/issue-18091-seeder-refusal-diagnostics
Sep 17, 2026
Merged

os-project-manager merged 5 commits into
mainfrom
claude/issue-18091-seeder-refusal-diagnostics

Conversation

@os-project-manager

@os-project-manager os-project-manager commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18091

The two declared-metadata seeders refuse to write in five more places, and every one of those refusals reported through the doubly-optional logger?.warn?.(…), so a caller that injected no logger got no output at all. Each site now words its own consequence, stamps its own grep token and builds its own record, over ONE shared delivery rule.

Caliber declared: the FULL SPELLING SWEEP — and the two readings reconcile

The dispatch ruled the full sweep (every occurrence of the doubly-optional spelling in the two seeders) over the card's five refusal sites. Re-measured on current origin/main (79a046f8), ⛔ not carried forward:

file raw grep -c prose (in comments) CALL SITES
bootstrap-declared-capabilities.ts 4 1 3
bootstrap-declared-permissions.ts 3 1 2
total 7 2 5

⭐ The triage seat's 7 reproduces exactly. The delta of 2 is not two more call sites — it is prose: bootstrap-declared-capabilities.ts:409 and bootstrap-declared-permissions.ts:313 are comment lines in which #18023 and #17516 each QUOTE the spelling while explaining the defect they repaired. Once comments are excluded, the full-sweep caliber and the refusal-site caliber name the SAME five call sites on this tree. So this PR is the full sweep, and the full sweep is 5.

After the change: 0 call sites in either file, and the 2 prose lines are deliberately kept — they are the historical record of why the shape exists — and are now pinned as comments-only by a class pin that reds the moment a sixth call site appears.

Control that the instrument was reading: bare logger occurrences in the two files, 18 and 16.

The reading this card moves (P2), measured before anything was touched

Each site driven with no logger passed, all five console channels spied, beside the two already-repaired axes as lit controls in the same harness:

                                         counter               author-visible lines
site 1  curated platform capability       skippedPlatform = 1            0
site 2  capability declaration unowned    skippedUnowned  = 1            0
site 3  capability rows unreadable        unreadable      = 1            0
site 4  permission set decl. unowned      (no counter at all)            0
site 4b same, via the ADR-0086 P2 door    (no counter at all)            0
site 5  permission set rows unreadable    unreadable      = 1            0
LIT     capability_name_collision         skippedForeign  = 1            1
LIT     permission_set_name_collision     skippedForeign  = 1            1

Every zero is now a 1, with every counter unchanged.

⚠️ Site 4 has a second door. upsertPackagePermissionSet is also the ADR-0086 P2 publish materializer, which passes no collector — a repair that only lit the boot loop would have left that caller exactly as mute. It is pinned separately.

⛔ No skip changed

Every skip is correct under ADR-0086 D4 (a package never writes into a foreign record) and ADR-0086 D3 (a package-managed row with no package_id makes uninstall undefined). Each test asserts the unchanged outcome beside the new line.

Why five diagnostics and not a mechanical copy

Applied, ⛔ not re-argued: the token, the record, the wording and the CONSEQUENCE are site-specific. The five consequences are genuinely different facts —

  • a curated-platform-name hijack still resolves (the curated pass owns the row), so nothing is denied and only the authored metadata and the ADR-0086 D3 provenance claim are lost; the remedy is rename-only, because unlike a package-to-package collision (ADR-0130 D1) a curated name is not co-ownable;
  • an unowned capability has three outcomes — an existing row, the back-compat derived placeholder, or nowhere at all — decided by what already stands in sys_capability. That three-way wording is preserved verbatim from A refused capability declaration still suppresses the back-compat derivation, so the capability exists nowhere — and an app-declared capability can never get registry provenance #4967 Part 3;
  • an unowned permission set keeps every grant working, because the evaluator resolves declared sets through the metadata registry; only the RECORD is lost (Setup surface, provenance axis, uninstall);
  • an unreadable read compared nothing and wrote nothing, so nothing is lost and nothing arrived either — and what that costs differs again between the two tables.

One generic "declaration skipped" sentence would send the first author hunting for a broken grant that is not broken.

What IS shared, and why that is the point of the card

Exactly one thing: WHERE the line goes. This shape had been repaired instance-by-instance twice, each repair restating the same two lines at its own call site. reportThroughSink() is now the single derivation, so a sixth site cannot re-earn the card. It also improves on both spellings it replaces:

  • logger?.warn?.(…) bought safety against a host sink that lies about its shape with silence — the defect;
  • logger.warn(…) would buy noise with a throw inside a seeding pass, the hazard ProjectionLogger.warn records;
  • the typeof guard buys neither, and keeps the receiver so a class-based host logger does not throw. Pinned on both axes.

Reused, ⛔ not re-derived, per the #18088 precedent: CollisionReportSink is imported from the sibling module rather than re-declared. ⛔ reportPermissionSetNameCollisions is not threaded with an axis parameter — PR #18088 measured that it does not generalise.

⚠️ The INFO channel keeps its outer ?. deliberately. A pass that did its work and refused nothing must stay silent on every console channel with no sink injected — that is the discriminating control #18023 landed, and both axes now carry one. The refusal channel is the one where silence was the defect. Asserted explicitly in the class pins.

New published surface

On the criterion the two existing collision diagnostics state and no wider: a refusal an author can cause has a second door by construction (@objectstack/lint, os build / os validate), and each of these is decidable from the declaration alone with no database.

  • CAPABILITY_PLATFORM_NAME_REFUSED + capabilityPlatformNameRefusedDiagnostic() + reportCapabilityPlatformNameRefused() + the record type
  • CAPABILITY_DECLARATION_UNOWNED + capabilityDeclarationUnownedDiagnostic() + reportCapabilityDeclarationUnowned() + the record type
  • PERMISSION_SET_DECLARATION_UNOWNED + permissionSetDeclarationUnownedDiagnostic() + reportPermissionSetDeclarationUnowned() + the record type

reportThroughSink() is not published either. It was on this list in the first revision and the contract review was right to refuse it: it is the delivery primitive, not a refusal an author can cause, and it has no second-door consumer — the cli door records that it deliberately does not consume a sink printer. This package's own logSeedDurabilityFailure states the rule for its class verbatim — "Deliberately absent from the package's index.ts: this is an intra-package helper, not public API". And the shape is about 390 raw hits across 29 package directories, so a shared delivery rule's home would be a core package, not this plugin's entry. The function did not move: seed-refusal-diagnostics.ts imports it relatively and all five report sites keep working. Verified off the built surface — require(dist/index.js).reportThroughSink is undefined and it appears in no export statement in dist/index.d.ts, while the controls reportCapabilityDeclarationUnowned (a function) and CAPABILITY_DECLARATION_UNOWNED (its token) both read live.

⛔ The two unreadable-rows summaries are not published: an unreadable database is a runtime condition no compile-time door can raise, so they stay package-private for the reason position_name_fold_grant does.

Clause-②: yes — re-derived from the delivered diff (9 value + 3 type exports reachable from the published entry).

Verification

All at cec185ad3.

Tests. pnpm --filter @objectstack/plugin-security test113 files / 2196 tests passed. pnpm --filter @objectstack/plugin-security typecheck — clean (tsc --noEmit, tsconfig.scripts.json, and check:test-typecheck: 0 files / 0 errors / 0 pinned signatures).

ABLATION A1 — delivery. reportThroughSink's body mutated back to the doubly-optional spelling. On-disk proof before reading any result: guard line count 1 → 0, mute line count 0 → 1, git diff --numstat 1/2. Result — 10 failed | 46 passed, and the ten are exactly the no-logger and lying-host pins at all five sites plus the publish door:

× UNOWNED DECLARATION: prints with NO LOGGER INJECTED, and names the RECORD as what is lost
× UNOWNED DECLARATION reaches the author through the ADR-0086 P2 PUBLISH door too
× UNREADABLE ROWS: prints with NO LOGGER INJECTED, with the count and the consequence
× each site keeps its OWN sentence
× a HOST SINK THAT LIES about its shape is reported to the console, never thrown at
× CURATED PLATFORM NAME: prints with NO LOGGER INJECTED, and names what is lost
× UNOWNED DECLARATION: prints with NO LOGGER INJECTED, keeping its three-way consequence
× UNREADABLE ROWS: prints with NO LOGGER INJECTED, with the count and the consequence
× each site keeps its OWN sentence
× a HOST SINK THAT LIES about its shape is reported to the console, never thrown at

ABLATION A2 — wording. Site 1's distinguishing clauses replaced with site 2's, simulating exactly the generic-sentence regression that is forbidden here. On-disk proof: CURATED PLATFORM capability 1 → 0, The name still resolves 1 → 0, the borrowed clause 0 → 1. Result — 2 failed | 35 passed, and the two are the per-site wording pins while every delivery pin stays green:

AssertionError: expected 'warn: [security] [capability_platform…' to contain 'CURATED PLATFORM capability'
AssertionError: expected 'warn: [security] [capability_platform…' to contain 'The name still resolves'

Both legs restored from HEAD under a trap … EXIT INT TERM with absolute paths, verified by blob-hash equality and an empty git status --porcelain — ⛔ not by a step's exit code.

Gates. Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands over the tool's own change set (8 paths vs merge base 79a046f8c), each exit code captured to disk before any pipe, then reconciled with --ran: 63 derived, 60 run green, 3 NOT MEASURED, 0 UNRUN.

The 3 are check:dual-build-cjs-loads, check:i18n and check:type-check-debt, each exiting 3 = PREREQUISITE NOT MET — all three read whole-repo build output, and the closure build (turbo run build --filter='./packages/*' --filter='./packages/*/*') was killed at 520s by this container's foreground ceiling. ⛔ Not a pass and not a finding. CI builds that closure before the same steps.

⚠️ check:cross-package-test-inputs exited 1 locally and is green for this diff — measured, not argued. Its single finding is packages/cli/test/init-created-files-summary.e2e.test.ts descending packages/spec/dist/, neither of which this diff touches. Control: with this full diff in the tree and packages/spec/dist moved aside, the same gate exits 0; moved back, 1. The finding is a function of a locally-built spec dist, and CI's Lint & Repo Gates job runs pnpm install --frozen-lockfile with no build, so that directory does not exist where the gate runs.

Lint. ⛔ Not narrowed — the whole population was measured: eslint . --no-inline-config --format json, 6804 files (the count eslint itself reported, not an estimate), 0 errors / 0 warnings.

Acceptance notes

⛔ Out of scope, filed nowhere by this PR, recorded here:

  • The same doubly-optional spelling is live on a sixth site outside the two seeders. packages/plugins/plugin-security/src/seed-name-lookup.ts:452 reports the batched existence read's own failure through logger?.warn?.(…). Measured mute here: the site-3 and site-5 cases in this PR drive an unreadable database with no logger and see exactly ONE author-visible line — this card's summary — while that one stays silent; with a logger injected the same pass produces both. It is the same class, but it is outside this card's ruled caliber (the two seeders), so it is reported for the triage seat rather than ridden in. A pin in this PR asserts the count by filtering on this card's tokens rather than counting lines, precisely so that line's removal stays visible.
  • The permission-set unowned refusal increments no counter at all. Every other refusal path on both axes moves one (skippedPlatform, skippedUnowned, skippedForeign, unreadable); this branch returns an all-zero outcome, so a caller reading no log still cannot tell it happened. Widening it means editing PermissionSeedOutcome in permission-set-projection.ts, outside this card's declared file surface. Noted, ⛔ not filed.
  • The two already-landed reporters still hand-spell the delivery rule. reportPermissionSetNameCollisions and reportCapabilityNameCollisions each carry their own if (logger) … else console.warn(…), which reportThroughSink now supersedes and which would additionally gain the lying-host guard. Routing them through it is behaviour-preserving, but both files are outside this card's declared file surface. Noted, ⛔ not filed — it is the remaining half of closing the class.
  • Cardinality is preserved, ⛔ not reconsidered. Sites 1, 2 and 4 keep one line per refused declaration, as they had; sites 3 and 5 keep one line per pass with the count, as they had. Making the per-declaration sites summarise once per pass (the shape [finding] A package's whole declared permission set is dropped on a set-name collision with no diagnostic and no build gate — the comment says "refuse loudly", the code warns through a doubly-optional logger #17516 and [finding] A package's whole declared capability is dropped on a name collision with no diagnostic — the same doubly-optional logger as #17516, one axis over #18023 chose for their collisions) would change what an injected-logger host sees and is not what this card measured.

Generated by Claude Code

… the author

The two declared-metadata seeders refuse to write in five more places, and
every one of those refusals was spelled `logger?.warn?.(...)` -- optionally
chained twice, so a caller that injected no logger got no output at all.

Each site now words its own consequence, stamps its own grep token and builds
its own record, over ONE shared delivery rule (`reportThroughSink`). The skips
themselves are unchanged (ADR-0086 D3/D4).

Claude-Session: https://claude.ai/code/session_01WmBwEiWPff9JZPd5BSGNeH
Co-authored-by: Claude <noreply@anthropic.com>
… logger injected

Per-site pins: the token, the site-specific consequence, the unchanged skip,
the injected-logger path, a lying host sink, and a class pin that reds when the
doubly-optional spelling returns to either seeder as a call site.

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

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 33 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/flows.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))
  • content/docs/automation/hooks.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))
  • content/docs/data-modeling/seed-data.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))
  • content/docs/deployment/seed-tenancy-repair.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))
  • content/docs/kernel/events.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))
  • content/docs/kernel/runtime-services/audit-service.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))
  • content/docs/kernel/runtime-services/sharing-service.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))
  • content/docs/permissions/authentication.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))
  • content/docs/permissions/authorization.mdx (via bootstrapDeclaredCapabilities (symbol, a top-level function), bootstrapDeclaredPermissions (symbol, a top-level function), upsertPackagePermissionSet (symbol, a top-level function))
  • content/docs/permissions/capabilities.mdx (via bootstrapDeclaredCapabilities (symbol, a top-level function))
  • content/docs/permissions/system-context.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))
  • content/docs/protocol/kernel/config-resolution.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/index.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))
  • content/docs/releases/v12.mdx (via bootstrapDeclaredPermissions (symbol, a top-level function))
  • content/docs/releases/v16.mdx (via organizationId (symbol, a field of interface PermissionSetDeclarationUnownedDiagnostic))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/index.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: organizationId (7 routes)
  • 2 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.

Coarse fallback — 15 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 25c9a8317bab7e3eea22a1468d928641de06161epackageMentionDocs.

Which tree this was computed on

This run read content/docs from cb570a838f1b18702ad6ececd2fe9b1d8c4e3640 — the merge of head cc9c7f139b4df2b0ba85f6bc1650bb235ba19c9e into base 25c9a8317bab7e3eea22a1468d928641de06161e, 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 cb570a838f1b18702ad6ececd2fe9b1d8c4e3640 && git checkout cb570a838f1b18702ad6ececd2fe9b1d8c4e3640
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 25c9a8317bab7e3eea22a1468d928641de06161e cc9c7f139b4df2b0ba85f6bc1650bb235ba19c9e && git checkout -B drift-repro 25c9a8317bab7e3eea22a1468d928641de06161e && git merge --no-ff cc9c7f139b4df2b0ba85f6bc1650bb235ba19c9e

node scripts/docs-audit/affected-docs.mjs --json 25c9a8317bab7e3eea22a1468d928641de06161e

⚠️ 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 25c9a8317bab7e3eea22a1468d928641de06161e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Green, and deliberately parked — this PR is waiting on a contract-review-tier seat, ⛔ not on CI and ⛔ not on its author

domain:services dispatching seat (objectstack#6021), session_01WmBwEiWPff9JZPd5BSGNeH, written at 2026-09-17T02:07Z. Full review record on the card: #18091 issuecomment-5707196… (the seat's independent re-verification of the caliber reconciliation and the clause-② derivation). This comment exists so a reviewer landing here does not have to re-derive why a green PR is sitting still.

CI — settled green, read on this head, ⛔ not from the webhook

GET /commits/cec185ad398774cb35868044944129cac02846a4/check-runs, taken at 2026-09-17T02:07Z:

34 distinct check names · 29 success · 5 skipped · 0 in_progress · 0 failure
mergeable: true · mergeable_state: clean

The 5 skips are all rostered expected skipsCheck PR Size · Auto Label · Packed-tarball smoke (opt-in) · Build Docs · Console Pin Gate. ⚠️ Method declared: scripts/pm/check-expected-skips.mjs is NOT RUNNABLE in this container (node_modules absent — yaml is declared at ^2.9.0, the container simply never ran pnpm install), so the roster was read from that file's source rather than executed. ⛔ That is a source read, not a run, and it is recorded as such rather than reported as a green tool.

Why it is parked

Clause-②: yes — measured from the DELIVERED diff, ⛔ not from the claim-time prediction: src/index.ts adds 10 value exports (CAPABILITY_DECLARATION_UNOWNED · CAPABILITY_PLATFORM_NAME_REFUSED · PERMISSION_SET_DECLARATION_UNOWNED · three *Diagnostic builders · three report* functions · reportThroughSink) plus 3 exported types, all reachable from the published entry.

reading value
CONTRACT_REVIEW_TIER (scripts/pm/dispatch-gates.mjs:10730) claude-fable-5-1
dispatching session session_context.model claude-opus-5
dispatching session external_metadata.last_served_model claude-opus-5

⇒ the dispatching seat is measurably below tier. Per 「条款②复核只在实测达档的席内做…未达档席 ⛔ 不审、不起该档子代理」 and 「⛔ 免复核不放行」, this seat has ⛔ not flipped ready, ⛔ not attached auto-merge and ⛔ not enqueued — and will not, on any amount of green.

needs:contract-review is hung on both carriers (this PR and card #18091); check-clause2-carriers --pair 18564 reads ✓ both carriers agree.

What the at-tier reviewer still owes, and what is already done

Owed: the clause-② review itself, then — on PASS — strip the label from both carriers citing the record, flip ready, attach auto-merge.

Already verified by this seat (⛔ re-verify anything you don't want to inherit): caliber reconciliation exact — triage's 7 occurrences vs the card's 5 sites differ only by two prose lines (bootstrap-declared-capabilities.ts:409, bootstrap-declared-permissions.ts:313) where #18023/#17516 quote the spelling while explaining the defect they repaired; 5 real call sites, 0 remaining on this head. R1–R5 no deviation. mcp_calls 0; REST writes 2 against a budget of 4.

Docs Drift Check — advisory, no action, and the lane red line is intact

comment 5707169545 is advisory only and no check failed. This PR touches zero content/docs/** files (8 files, all plugin-security plus the changeset), so the three release-owned pages it lists as read-only are untouched — ⛔ no code PR here edits content/docs/releases/**. 10 of its 12 rows anchor on organizationId, a generic field name on an interface this PR newly adds; the check names that weakness itself. The two substantive rows document what gets seeded and skipped, which R2 held unchanged — the diff is additive diagnostics, so no page is falsified.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

⬇️ 契约复核记录 —— 由维护者授权(「18564 你可以派fable复审」)起的隔离契约复审档复核子代理渲染,domain:services 席位 session_01WmBwEiWPff9JZPd5BSGNeH 逐字采纳。⛔ 本席一字未改、未摘录、未润色 —— 细则:父会话只可逐字采纳或整体作废。

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: cec185ad398774cb35868044944129cac02846a4

① Derived judgments

Read from git diff 79a046f8..cec185ad (8 files; numstat identical to the API file list) against packages/plugins/plugin-security/package.json on origin/main (exports holds only "."dist/index.*; the root tsup.config.ts entry is src/index.ts alone), so the published surface is exactly what src/index.ts re-exports.

Accept set — NOT relaxed, verified site by site:

  1. Site 1 (bootstrap-declared-capabilities.ts, curated platform name): out.skippedPlatform += 1; return true unchanged — RIGHT.
  2. Site 2 (unowned capability): out.skippedUnowned += 1; return Boolean(existing?.id) unchanged — RIGHT.
  3. Site 3 (capability rows unreadable): counter and materializedNames unchanged — RIGHT.
  4. Site 4 (upsertPackagePermissionSet, unowned set): return out unchanged, still no counter — RIGHT (the counter gap is [finding] plugin-security: the permission-set unowned refusal increments NO counter — every other refusal path on both axes moves one, so a programmatic caller cannot tell it happened #18571; adding a key to the published PermissionSeedOutcome type would be its own clause-② yes, correctly not ridden in).
  5. Site 5 (permission-set rows unreadable): unchanged — RIGHT.
  6. Delivery behaviour: a no-sink host and a lying-sink host now get console.warn instead of silence; a sink with a callable warn gets the call exactly as before, receiver kept. Log wording and meta keys changed at all five sites (event, fix, declaredBy/grantedBy/hasRow/organization); log lines are not a published contract in this package (the modules' own "event, not code" section) — RIGHT, not a semver surface.
  7. No exported function signature of either seeder moved (sweep of every added/removed export line in the diff: only index.ts and the two new modules).

Published surface — WIDENED by 10 value exports + 3 type exports through src/index.ts, every one reachable from the entry:
8. CAPABILITY_PLATFORM_NAME_REFUSED, CAPABILITY_DECLARATION_UNOWNED, PERMISSION_SET_DECLARATION_UNOWNED — RIGHT: parity with the two precedent blocks (#17516/#18023 export their token so a second door never re-spells the literal; packages/cli/src/utils/permission-set-name-collisions.test.ts consumes the precedent token from the entry).
9. capabilityPlatformNameRefusedDiagnostic(), capabilityDeclarationUnownedDiagnostic(), permissionSetDeclarationUnownedDiagnostic() plus the three record types — RIGHT: builder + record is exactly what the real compile-time door consumes from the precedent (packages/cli/src/utils/permission-set-name-collisions.ts:91,189-203).
10. reportCapabilityPlatformNameRefused(), reportCapabilityDeclarationUnowned(), reportPermissionSetDeclarationUnowned() — TOLERABLE on precedent parity only: both precedent blocks export their report*, so "the criterion the two blocks state" is met; but no second door consumes a sink printer — the cli door records that reportPermissionSetNameCollisions is deliberately NOT consumed ("a SINK-printing pass reporter ... the CLI's text face has no channel for", lines 35-42). Not a FAIL reason; recorded so the next widening on this axis does not cite it as precedent for more.
11. reportThroughSink() — WRONG, and the reason for the verdict. It fails the author's own stated criterion and the in-package precedent: (a) it is not "a refusal an author can cause"; it is the delivery primitive, yet the PR body and the changeset list it under "on the criterion the two existing collision diagnostics state and no wider"; (b) it has no second-door consumer by construction — the cli door has no channel for sink printing (item 10); (c) the index.ts rationale ("so a sixth refusal site never re-spells") is served by a relative import: the only sixth site the PR itself found (seed-name-lookup.ts:452, now #18570) and the two already-landed reporters it names as successors are all inside the package; (d) the package's existing helper of the same class, logSeedDurabilityFailure (per-organization-catalog.ts:150-172), says "Deliberately absent from the package's index.ts: this is an intra-package helper, not public API"; (e) scope: the doubly-optional shape is live at about 390 raw hits across 29 package directories on origin/main (65 in plugin-security alone), so if a shared delivery rule were meant cross-package its home is a core/shared package, not the security plugin's entry — publishing it here makes plugin-security the owner of a generic logger-fallback contract. Neither minimal nor correctly scoped. Remedy: drop export { reportThroughSink } from './seed-refusal-sink.js' and its comment from src/index.ts, and the reportThroughSink() bullet from the changeset; nothing else moves (all five call sites already import it relatively).
12. Deliberately NOT exported (CAPABILITY_ROWS_UNREADABLE, PERMISSION_SET_ROWS_UNREADABLE, reportCapabilityRowsUnreadable, reportPermissionSetRowsUnreadable) — RIGHT and verified package-private: module-level export only, no entry re-export, no subpath in exports, single tsup entry. The stated reason (a runtime condition no compile-time door can raise; the position_name_fold_grant discrimination at permission-set-name-collision.ts:72-77) holds.
13. Advisory, not a FAIL reason: the precedent tuple the door actually consumes includes a format* one-line renderer (permission-set-name-collisions.ts:223-224 imports formatPermissionSetNameCollisionDiagnostic from the entry); this PR publishes none for its three records, and its own report* functions spell the join two ways (${d.message}. Fix: at site 2 vs ${d.message} Fix: at sites 1 and 4). A compile door consuming these records today would re-derive the join — the drift the criterion cites. Additive to fix later; recorded so the surface's incompleteness is on the record.
14. Module-level export type { CollisionReportSink } in both new modules — internal re-exports that do not reach the entry (the entry already exports it from permission-set-name-collision.js); no change.

② Semver level

Derived: minor for @objectstack/plugin-security — additive exports on a released package (Clause-② yes ⇒ at least minor per AGENTS.md "Add a changeset" and the level axis of check-changeset-no-major), nothing removed or renamed, so not breaking and no ADR-0087 marker owed. Declared: "@objectstack/plugin-security": minor in .changeset/18091-seeder-refusal-diagnostics.md — AGREES, and stays minor after the item-11 remedy (9 value + 3 type exports remain). Carriers: needs:contract-review hangs on both PR and card; node scripts/pm/check-clause2-carriers.mjs --pair 18564 (run by this review, read-only) — ✓ exit 0, "readable in the fixed spelling and both carriers agree". Check Changeset is success on this head. Observation only: the PR body carries the declaration as prose ("Clause-② is yes"), not the fixed Clause-②: yes line the precedent PR #18088 carried; the level gate's documented body-or-label disjunction resolved it through the label. Adding the fixed line to the body costs no push.

③ Boundary flags

open_questions: [] in the os-dev-report — none raised. Every other flag on the report and in the PR's acceptance notes, answered:

Implemented-by: claude/issue-18091-seeder-refusal-diagnostics
Reviewed-by: session_01WmBwEiWPff9JZPd5BSGNeH

VERDICT: FAIL


席位的转录核验(⛔ 不是记录的一部分,是采纳依据)

本席对该记录的四条承重断言逐条独立复验,⛔ 不采信自述:

记录的断言 本席自取的读数
(d) 同包先例禁止导出投递助手 per-organization-catalog.ts 逐字:"Deliberately absent from the package's index.ts: this is an intra-package helper, not public API."
被点名的导出行存在 ✅ PR head 上 src/index.ts:116
index.ts 里的东西就是已发布面 package.json exports["."];根 tsup.config.ts entry: ['src/index.ts']
(b) cli 门禁消费 report* :35-36 逐字 "What is NOT consumed is reportPermissionSetNameCollisions … a SINK-printing pass reporter";且 :223-224 确实消费 format*。对照:同文件 permissionSetName 6 命中 ⇒ 仪器在读

FAIL 成立,本席逐字采纳。 记录形制齐备(Served-tier: 常量名、Head-sha: 独占码段、①②③、独立性对、VERDICT 大写)。

⚠️ 独立性对读作 SELF-REVIEW(Reviewed-by 记采纳席位的 session)—— 按细则 mode:subagent 席内审设计如此,⛔ 非缺陷;只有 mode:remote 才能两者不同 session。本席不假装它独立。


Generated by Claude Code

Contract review item 11: the delivery primitive is not "a refusal an author can
cause" and has no second-door consumer -- the cli door has no channel for a
sink printer. The package's own logSeedDurabilityFailure records the same class
as "deliberately absent from the package's index.ts: an intra-package helper,
not public API". The shape is ~390 raw hits across 29 package directories, so a
shared delivery rule's home is a core package, not this plugin's entry.

Not a breaking change: the export never shipped -- it exists only on this
unmerged branch. The function does not move either; seed-refusal-diagnostics.ts
imports it relatively and all five report sites keep working. Changeset stays
minor (9 value + 3 type exports remain).

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

Copy link
Copy Markdown
Collaborator Author

⬇️ 契约复核记录(第二轮,判 merge 后的 head)—— 由维护者授权起的隔离契约复审档复核子代理渲染,domain:services 席位 session_01WmBwEiWPff9JZPd5BSGNeH 逐字采纳。⛔ 本席一字未改。上一轮 FAIL 记录是 issuecomment-5713158778(判 cec185ad3)。

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: cc9c7f139b4df2b0ba85f6bc1650bb235ba19c9e

① Derived judgments

Read from git diff 25c9a8317bab7e3eea22a1468d928641de06161e..cc9c7f139b4df2b0ba85f6bc1650bb235ba19c9e (merge base = origin/main at review time; 8 files, +1018/-34, numstat identical to the API file list). Published surface defined from the head's own package.json (exports holds only "."dist/index.{d.ts,mjs,js}, files = dist/README/CHANGELOG) and the root tsup.config.ts (single entry src/index.ts), and then MEASURED off a build of that entry rather than off the source text: an esbuild 0.28.2 bundle of src/index.ts (ESM + CJS, bare specifiers external) and the TypeScript compiler API's export list of the entry module.

The two head moves:

  1. Patch 3646db6 (the item-11 remedy): exactly 4 deletions — the export { reportThroughSink } line plus its two comment lines in src/index.ts, and the reportThroughSink() bullet in the changeset. Nothing else moved. The function stays in seed-refusal-sink.ts, is imported relatively by seed-refusal-diagnostics.ts:69, and all five report sites (:178, :246, :282, :348, :377) still route through it — REMEDIED, and remedied in the published surface: the built ESM export block (92 names) does not contain reportThroughSink (it survives only as an internal function in the bundle, no __export row in the CJS build); the compiler API lists 129 entry exports (92 value / 37 type / 0 unresolved) with reportThroughSink absent; the per-file tsc --declaration emit of index.d.ts names it nowhere. Removed nothing it should not have: the five call sites, the sink module, the CollisionReportSink type re-exports and the tests are untouched by the remedy. Left behind, none contradicting the PR's description of its surface: (i) changeset line 26 names reportThroughSink() in prose as the single derivation — a true implementation statement, no publication claim; (ii) bootstrap-declared-capabilities.ts:103 comment, same; (iii) seed-refusal-diagnostics.ts:57 carries {@link reportThroughSink} in the module-header docblock, which the declaration emit DOES carry into seed-refusal-diagnostics.d.ts:55 — a dangling doc link in the published types, cosmetic, already recorded by the dev, advisory; (iv) seed-refusal-sink.ts does not state its own package-private status the way the in-package precedent logSeedDurabilityFailure does ("Deliberately absent from the package's index.ts") — advisory, would make the ruling self-documenting.
  2. Merge cc9c7f1 (origin/main into the branch): a clean automatic merge — its tree equals git merge-tree --write-tree 3646db6 25c9a83 byte for byte, so no hand resolution happened — and git diff 79a046f8..3646db6 is byte-identical (cmp) to git diff 25c9a83..cc9c7f1: the merge added nothing to and removed nothing from the PR's delta. What main brought under plugin-security in 79a046f8..25c9a83 (43 commits): declared-capability-context.ts (new), security-plugin.ts, suggested-audience-bindings.ts and two tests — none of the PR's eight paths, index.ts untouched by main, no new logger?.warn?.( call site. The one shared file is bootstrap-declared-permissions.ts: main's new module imports readDeclared from it, a function this PR's diff does not touch. Nothing entered this PR's contract surface. Joint breakage is a CI matter (see ③).

Accept set — NOT relaxed, re-verified site by site against the current merge base:
3. Site 1 (curated platform name): out.skippedPlatform += 1; return true unchanged — RIGHT.
4. Site 2 (unowned capability): out.skippedUnowned += 1; return Boolean(existing?.id) unchanged — RIGHT.
5. Site 3 (capability rows unreadable): counter and materializedNames unchanged — RIGHT.
6. Site 4 (upsertPackagePermissionSet, unowned set): return out unchanged, still no counter (#18571, filed and open) — RIGHT.
7. Site 5 (permission-set rows unreadable): unchanged — RIGHT.
8. Delivery: if (logger && typeof logger.warn === 'function') logger.warn(…) else console.warn(…) — a no-sink host and a lying-sink host get the console, a real sink gets the call with the receiver kept. Log wording and meta keys changed at all five sites; log lines are not a published contract in this package — RIGHT, not a semver surface. The doubly-optional spelling survives in the two seeders only as one prose line each (capabilities.ts:421, permissions.ts:332), 0 call sites.
9. No exported function signature of either seeder moved (no export line added or removed in the two seeders; readDeclared, which main now consumes, is not in the diff).

Published surface — WIDENED by 9 value + 3 type exports, all measured reachable from the built entry (the prior review's 10 + 3 minus the remedied one):
10. CAPABILITY_PLATFORM_NAME_REFUSED, CAPABILITY_DECLARATION_UNOWNED, PERMISSION_SET_DECLARATION_UNOWNED — RIGHT: parity with the #17516 / #18023 token exports; the identifiers and the snake_case string values collide with nothing else in packages/**.
11. The three *Diagnostic() builders and the three record types — RIGHT: builder + record is what the real compile-time door consumes from the precedent (packages/cli/src/utils/permission-set-name-collisions.ts:189-203).
12. The three report* functions — TOLERABLE on precedent parity, re-judged independently and the same conclusion reached: both precedent blocks export their report*; the item-11 grounds against reportThroughSink (a generic delivery primitive, an in-package "intra-package helper" precedent, a shape live across 29 package directories) do not apply to axis-specific reporters. Still no second door consumes a sink printer (the cli door records that it deliberately does not, :35-42), so this is parity, not need — recorded so the next widening on this axis does not cite it as precedent for more.
13. Deliberately NOT exported (CAPABILITY_ROWS_UNREADABLE, PERMISSION_SET_ROWS_UNREADABLE, reportCapabilityRowsUnreadable, reportPermissionSetRowsUnreadable): verified absent from the built ESM export block and from the compiler API's entry export list — RIGHT; the stated reason (a runtime condition no compile-time door can raise) holds.
14. Module-level export type { CollisionReportSink } in both new modules: still present; the entry exports exactly one CollisionReportSink (from permission-set-name-collision.js), 0 unresolved — no change.
15. Departure from the prior review: its item 13 called the two report* joins (${d.message}. Fix: at site 2 vs ${d.message} Fix: at sites 1 and 4) drift. On this head, read from the code, they are NOT drift — see ③. What IS an asymmetry, and is advisory only: (a) the three new records embed [security] [token] inside message, whereas the precedent records keep message prefix-free and let format* prepend [security] [${d.event}]; (b) site 2's message value ends without terminal punctuation while sites 1 and 4 end with a period. A future format* for these records cannot be a copy of the precedent's formatter. The published types declare message: string only, so neither is a contract matter.
16. The PR body's own surface description now agrees with the head: reportThroughSink() stated as not published, the three-block list matches the entry, and the body carries Clause-②: yes at a line start with "9 value + 3 type exports" — the measured count.

② Semver level

Derived: minor for @objectstack/plugin-security (released, 17.4.0) — a purely additive widening of the published entry (9 value + 3 type exports) takes at least minor per .github/workflows/pr-automation.yml:717 ("WHICH LEVEL") and AGENTS.md Post-Task step 3 (yes takes at least minor); nothing removed, renamed or narrowed, so not breaking, no BREAKING banner and no ADR-0087 marker owed. Declared: "@objectstack/plugin-security": minor in .changeset/18091-seeder-refusal-diagnostics.md — AGREES. Check Changeset is success on this head. Carriers: needs:contract-review is hung on both the PR and card #18091 at read time; node scripts/pm/check-clause2-carriers.mjs --pair 18564 (run by this review, read-only, live GitHub) — ✓ exit 0, "the clause-② declaration is readable in the fixed spelling and both carriers agree".

③ Boundary flags

  • Item-13 adjudication (the live disagreement): the dev's reading is right and the prior review's is not upheld. Site 1's message terminates in ${blastRadius}, both arms of which end with a period; site 4's message ends "…nothing to reap (ADR-0086 D5)."; site 2's message ends in ${consequence}, none of whose three arms ends with a period ("…no package provenance", "…uninstall undefined)", "…materialized nowhere"). So ${d.message} Fix: at sites 1 and 4 and ${d.message}. Fix: at site 2 each render exactly one period before "Fix:"; aligning the two spellings would double a period at 1/4 or drop it at 2. Correct punctuation, not drift. The residual point survives as advisory: there is no format* renderer for the three new records, so a compile-time door consuming them would re-derive the join, and the record convention differs from the precedent's (item 15). Successor: the first door that consumes these records adds the format* functions that own the join — and settles the prefix convention one way, ⛔ not by copying the precedent formatter over records that already carry the prefix.
  • Dev patch-report open question 1 (re-hang the carriers after the head moved): answered by state — both carriers carry needs:contract-review at read time and --pair 18564 reads ✓. No action for this review.
  • Open question 2 (merge origin/main before landing): answered — the seat merged it (cc9c7f1, maintainer-account author, no hand resolution). Per AGENTS.md multi-agent §10 the merge commit is validated by CI on the PR and by the queue's rebuilt generation. CI on cc9c7f1 at the review's last read (about 11:28Z): TypeScript Type Check, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Check Changeset, Type Check · workspace/source/consumer/debt, Test Core 2/3/4/6, Dogfood Regression Gate 1/3, Dogfood Verify CLI — all success; Lint & Repo Gates, Test Core 1/6 and 5/6, Dogfood Regression Gate 2/3 and 3/3 — in_progress; 0 failures; expected skips only. ⚠️ Not settled at review time: the seat acts on this PASS only after reading settled green on THIS head (AGENTS.md §7 — in_progress is not a pass).
  • The 3 locally NOT MEASURED gate families (check:dual-build-cjs-loads, check:i18n, check:type-check-debt) and the local check:cross-package-test-inputs exit 1 (a packages/cli test descending packages/spec/dist/, unrelated to this diff; the new pins read only their own package's source via import.meta.url) — not contract questions; Build Core is green on this head and Lint & Repo Gates decides the rest. Not re-run here.
  • Stale-tree finding from the patch round — discharged by the merge.
  • [finding] plugin-security: a SIXTH doubly-optional logger?.warn?.(…) site outside the two seeders — seed-name-lookup.ts:452 mutes the batched existence read's own failure #18570 (sixth doubly-optional site, seed-name-lookup.ts:452) and [finding] plugin-security: the permission-set unowned refusal increments NO counter — every other refusal path on both axes moves one, so a programmatic caller cannot tell it happened #18571 (permission-set unowned refusal moves no counter) — both filed, open, unassigned. Answered.
  • Two already-landed reporters still hand-spell the delivery rule — a behaviour-preserving intra-package successor through a relative import, ⛔ never an entry export. Answered.
  • Dangling {@link reportThroughSink} in the published types — confirmed to survive declaration emit; cosmetic; successor: whichever PR next edits seed-refusal-diagnostics.ts, which may also add the "deliberately absent from index.ts" note to seed-refusal-sink.ts. Not owed here.
  • Cardinality preserved; INFO channel keeps its outer ?. — neither touches accept set or surface; the healthy-pass silence control is the right reading. Answered.
  • PR body footer is now the platform's bare form (a raw REST PATCH consequence the dev measured); the body's verification section still cites cec185ad3 — neither a contract matter; durable attribution is in the commit trailers.
  • Docs Drift Check advisory — no content/docs/** touched, README.md of the package documents none of the diagnostics surface, so no page is falsified. Nothing owed.
  • Escalations: none.

Implemented-by: claude/issue-18091-seeder-refusal-diagnostics
Reviewed-by: session_01WmBwEiWPff9JZPd5BSGNeH

VERDICT: PASS


席位的转录核验(⛔ 不是记录的一部分)

本席复验了本轮最承重的一条 —— 合并没有往本 PR 的 delta 里带进任何东西:

cmp  git diff 79a046f8..3646db6   (pre-merge delta)
     git diff 25c9a83..cc9c7f1    (post-merge delta)
  ⇒ IDENTICAL,各 1181 行
  对照:d1 vs 空 ⇒ differs  ⇒ 比较器有分辨力

⭐ 并记一条本轮复核推翻了上一轮复核的事:上一轮 item 13 把两处 join 拼法判为 drift,本轮读代码后判 dev 对、上一轮不成立 —— 站点 2 的 ${consequence} 三臂皆不以句号结尾,站点 1/4 本就以句号结尾,统一拼法只会造出双句号或缺句号。⇒ 复核可以被复核推翻,这次是。残留的真问题(缺 format* 渲染器、记录前缀约定与先例不同)已降为 advisory 并点名了后继者。

⚠️ 记录自陈 CI 在复核时点未 settle,并明写「the seat acts on this PASS only after reading settled green on THIS head」。本席遵此:PASS 已在案,但清标与落地等 Lint & Repo Gates 收敛


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

清标 provenance —— 条款②闸门 PASS,双载体已剥

domain:services 席位(objectstack#6021),session_01WmBwEiWPff9JZPd5BSGNeH,写于 2026-09-17T11:44Z。

复核记录 issuecomment-5713647182(本 PR)
所判 head cc9c7f139b4df2b0ba85f6bc1650bb235ba19c9e
Served-tier CONTRACT_REVIEW_TIER
VERDICT PASS
前一轮 issuecomment-5713158778,判 cec185ad3,FAIL(item 11);修法已落、已复核
授权 维护者逐字「18564 你可以派fable复审」,记录 issue #18091 issuecomment-5713021862

⇒ 双载体(本 PR + 卡 #18091)同笔剥除。清的是 PASS,⛔ 不是被剥。

落地前检三条

  • 席内条款②复核 PASS 在案 —— 同形记录见上,首行 Served-tier: 为常量名,Head-sha: 独占码段,①②③ 逐项,独立性对齐备。
  • 双载体已清,check-clause2-carriers --pair 18564 机读(下方本席实跑读数)。
  • PR check 全绿 —— 本 head 上 34 项检查:31 success · 3 skipped · 0 in_progress · 0 failure。三个 skip 全在册(Packed-tarball smoke (opt-in) · Build Docs · Console Pin Gate)。⛔ 非 required 子集,是全量。

⚠️ 独立性对读作 SELF-REVIEW(Reviewed-by 记采纳席位 session)—— mode:subagent 席内审设计如此,⛔ 非缺陷。本席不粉饰。


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review September 17, 2026 11:44
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit cf39b83 Sep 17, 2026
39 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-18091-seeder-refusal-diagnostics branch September 17, 2026 12:10
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

2 participants