Skip to content

The DERIVED half of bootstrapSystemCapabilities also leaves the platform bucket unseeded when an organization holds the name — the #5876 guard skips instead of inserting #8536

Closed

Description

@os-zhuang

Found while implementing #8470. The PM ruled the derived half explicitly out of scope for that card, so this records the finding rather than changing it.

The mechanism

bootstrapSystemCapabilities looks a capability up by name across organizations (system context, no tenantId). #8470 fixes the CURATED half by scoping its lookup to the row the platform owns. The DERIVED half's lookup stays { name } — deliberately, because its own #5876 guard already refuses to overwrite a row it does not own:

if (isDerived && row.managed_by !== 'platform') {
  skippedAuthored += 1;
  continue;
}

The guard is correct about what it protects. What it does not do is insert. So when an organization has authored a row for a name that a bootstrap permission set grants via systemPermissions[], the cross-organization lookup finds the org's row, the guard fires, and the loop continues — the platform's own derived placeholder is never created in the NULL-organization bucket.

That is the same shape as #8470's second harm ("the platform's row is never seeded"), on the half #8470 leaves alone. It became reachable for the same reason: #8461 widened sys_capability.name from installation-wide to per-organization uniqueness (ADR-0120 D1), so the two-row state exists at all.

Note this was measured as safe in #8470's own table (its row 1) — correctly, because that row asked whether the organization's label survives, and it does. The unseeded platform bucket is a different question that row did not ask.

Measured consequence: none, today

I checked the same question #8470 asked about the curated half, with the same method: no runtime path reads a sys_capability row. Grants (systemPermissions) and requirements (requiredPermissions) resolve capabilities by name; validateCapabilityReferences resolves against PLATFORM_CAPABILITY_NAMES plus stack declarations, never the table. So the effect is confined to the registry contents and to Setup's capability listings — the platform's own row for that name is simply absent.

That is why I am filing this rather than treating it as urgent, and why the severity judgment should be made by whoever decides whether the registry is supposed to be complete.

The open question this really turns on

Is the derived placeholder supposed to exist per-installation regardless of what organizations have authored — the same rule #8470 just established for curated names — or is "some row resolves this name somewhere" the intended contract for derived defaults? The #5876 reasoning ("the capability resolves and the authored copy is the better one") reads as the latter, but it was written when name was installation-wide unique, so "a row exists" and "a row exists in the platform bucket" were the same statement. #8461 separated them, and nothing has re-decided the derived half since.

Filed unassigned. Related to #8470; not blocked by it.


Generated by Claude Code

Activity

  1. added theissue type on Aug 13, 2026
  2. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: held as finding, domain:metadata (same seeder as #8470: bootstrap-system-capabilities). Type: Task.

    Grading rationale: zero measured consequence today — the card itself establishes that no runtime path reads a sys_capability row. Sibling-inheritance from #8470's curated half was considered and NOT applied: #5876's skip-not-overwrite was a deliberate decision about the derived half specifically, its "a row resolves the name" reasoning may be the intended contract for derived defaults, and #8535 (whether the row table is load-bearing at all) reshapes this question in either direction — a real semantic difference, not a mechanical sibling.

    Named re-grade triggers (re-graded the moment any fires):

    1. The sys_capability.active is read by nothing — the Deactivate action tells the admin grants stop resolving, and they do not #8535 ruling lands — enforce-the-row direction ⇒ promote (likely Bug: an unseeded platform bucket becomes an authz-visible gap); catalogue-only direction ⇒ registry-completeness Task or close not-planned.
    2. Any runtime path starts reading sys_capability rows.
    3. bootstrapSystemCapabilities reconciles an arbitrary row when a curated capability name exists in more than one organization — find(..., limit: 1) has no ORDER BY, so the platform's own row can be left unseeded #8470's landed shape turns out to touch the derived lookup after all — re-check on its merged ref.

    Generated by Claude Code

  3. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Re-grade: promoted finding → pm:queue (Task). This card's named re-grade triggers have both fired in the maintainer's 2026-08-13 ~23:50Z ruling batch (「接受你的全部建议」 on the full decision box):

    Scope of the promoted work (parity, not policy): bring the DERIVED half of bootstrapSystemCapabilities to parity with the curated half's post-#8537 behaviour — the #5876 guard keeps skipping (that is the ruled behaviour), but the skip must be counted and warned with the same provenance-naming diagnostic the curated half now emits, instead of silently leaving the bucket unseeded. No adoption, no backfill, no behaviour change beyond observability.

    ⛔ Back-to-triage clause: if implementation finds the derived half deliberately differs in a way that makes parity wrong (a recorded decision this seat has not seen), stop and return here rather than forcing it.

    Serial note: same file family as #8552's authoring-refusal work and the landed #8537 — not the same batch; check the lane's hot-file queue at claim time.

    Recorded by the triage seat on the maintainer's direct channel.


    Generated by Claude Code

  4. self-assigned this
    on Aug 14, 2026
  5. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Claim: PM loop round 2
    Session: session_01XeQRiAa7vYRVX5Fog7Zby8
    Branch: claude/issue-8536-derived-bucket-parity-warn
    Worktree: objectstack-issue-8536
    Domain: domain:metadata
    File surface: packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts + bootstrap-system-capabilities.test.ts (stop on breach; explain in the report)
    Container & model: S–M, mode:subagent, model: opus — small but not mechanical: the judgement is what the diagnostic must say and which counter it belongs on, which no gate decides.
    Serial constraints cleared: triage's note said «same file family as #8552's authoring-refusal work and the landed #8537 — check the lane's hot-file queue at claim time». Checked: #8552 landed as c931e53, #8537 landed. The lane's other plugin-security card this round (#8601, PR #8723) is confined to src/translations/*.ts — disjoint file, and it is already through review.

    ⚠️ In-flight overlap, declared: PR #8713 (enforce active on sys_permission_set and sys_position) is open against this package, but its file surface is src/security-plugin.ts + a new permission-set-active.test.ts plus packages/core/src/security/** — measured from its file list, not assumed. Disjoint from yours. Sync with main before opening the PR anyway.

    Scope is parity, not policy, exactly as the re-grade (5287721607) states: the #5876 guard keeps skipping — that is the ruled behaviour — but the skip must be counted and warned with the same provenance-naming diagnostic the curated half emits, instead of silently leaving the platform bucket unseeded. ⛔ No adoption, no backfill, no behaviour change beyond observability. The back-to-triage clause stands: if the derived half turns out to differ deliberately in a way that makes parity wrong, stop and return the card rather than forcing it.

    Premise verified on origin/main before dispatch: the curated half already carries blockedCurated and the provenance-reading diagnostic (bootstrap-system-capabilities.ts:276 — "Read the blocking row's provenance and name it instead"), and the derived half's counter is skippedAuthored (:203, :239). Both re-grade triggers fired as recorded (#8535 ruled B, #8552 ruled 4+1).


    Generated by Claude Code

  6. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor
    {
      "issue": 8536,
      "status": "done",
      "branch": "claude/issue-8536-derived-bucket-parity-warn",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8749",
      "premise_still_valid": true,
      "summary": "The derived half's skip is now counted and warned instead of leaving the platform bucket silently unseeded; behaviour is unchanged (the #5876 guard still declines, no adoption, no backfill). Premise measured before implementing, not assumed: the headline pin was run against unmodified origin/main and every assertion before the new counter passed — the guard fires, the org row is untouched, the platform-bucket lookup returns undefined, and seeded does not move; the source had exactly one logger.warn call site, in the curated branch. Counter judgement, made from the two halves side by side and argued in the PR: skippedAuthored is KEPT unchanged in meaning and value, and a new unseededDerived counts the documented SUBSET whose skip leaves the platform bucket with no platform-owned row — reuse would bake in the conflation #8461 created, a split would silently change a field already returned and logged. The back-to-triage clause was considered and NOT triggered: the derived half does differ deliberately (#4632's 'report in the summary rather than warn'), but the re-grade saw that reasoning and superseded it via #8552, and the warn is scoped so the benign case #4632 was written about stays summary-only. Synced with main (8 new commits merged, including plugin-security translations #8723); PR #8713 had not landed and its surface is disjoint.",
      "tests": "All at HEAD 40610b3 (the merge commit — the union was re-run IN FULL after the sync with main, because a ratchet measured against different code is a stale claim). pnpm --filter @objectstack/plugin-security test -> 60 files, 1146 passed (1139 pre-change, +7 new). typecheck -> clean. Reverse verification, both directions: (a) the new pins run against unmodified source -> 7 failed, test 1 stopping exactly at the new counter, which is the premise evidence quoted in summary; (b) ABLATION — replacing the 'placeholder exists' early-continue with an unconditional warn turns opposite-direction control 2 red ('expected 1 to be +0') and ONLY that test (30/31 still pass), so the control has teeth. Ablation ran against source, not dist (in-package relative import under vitest), so no rebuild applies — but the workspace closure WAS built twice (pre- and post-merge) for the ratchet gates, which refuse on an unbuilt tree. Gates: re-derived from the actual diff with scripts/pm/dispatch-gates.mjs, which surfaced FIVE changeset-family gates the dispatch list did not name (it was derived on the source surface only): check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration.mjs, check-changeset-no-major.mjs, check-empty-changeset.mjs. Union all EXIT=0, exit codes read unpiped: those five plus check:cross-package-test-inputs, check:test-source-alias, check:type-source-resolution, check:nul-bytes, check:query-options-erasure, check:i18n (in sync, 9 packages), check:type-check-coverage, check:type-check-debt --re-measure (33 ledger entries, 1968 raw errors, none above its recorded number), node scripts/check-cross-package-test-inputs.mjs. A test-fixture bug was caught and fixed rather than papered over: a helper parameter defaulted to 'admin' silently rebuilt the absent-managed_by row of the provenance matrix as an admin row, because a JS default fires on an explicitly passed undefined.",
      "open_questions": [
        {
          "question": "DECIDED AND SHIPPED, flagged for review rather than blocking: the re-grade says 'the skip must be counted and warned'. Read literally that is a warn on EVERY derived skip. I scoped the warn to skips where the platform's own placeholder is genuinely absent, so a skip that declines a mere refresh (the placeholder exists and simply was not the row the cross-organization lookup selected) stays summary-only.",
          "options": [
            "A (shipped) — warn only where the platform's placeholder is absent: the warning then means exactly one thing, an occupied-or-empty bucket with no platform row, and #4632's 'do not alarm' holds for the benign case it was written about. Every skip is still COUNTED in skippedAuthored, so nothing is hidden.",
            "B — warn on every skip, literal parity with the sentence: simpler rule, but it fires on ordinary authored rows where nothing is missing, which is the boot-noise #4632 declined, and it makes the warning unfalsifiable as a signal."
          ],
          "recommendation": "A, and it is what shipped. Opposite-direction control 2 pins it (guard fires, unseededDerived stays 0, no warn), and the ablation shows B failing exactly that control — so if triage prefers B the pin, not the code, is what states the disagreement."
        }
      ],
      "out_of_scope_findings": [
        "filed as #8751: the DERIVED half's UPDATE branch reaches the same unseeded-bucket end state by a different route — a platform-marked row inside an ORGANIZATION passes the single-condition `managed_by === 'platform'` guard (the curated half uses the conjunction with `organization_id: null` since #8470), so the seeder refreshes that org row and never seeds the platform bucket. Measured: orgLabel overwritten with the humanized placeholder, platformBucketRow ABSENT, skippedAuthored 0, unseededDerived 0, warns 0 — invisible to this card's new counter, which only covers the skip branch. Observation-class, `finding` label, unassigned, no pm:queue."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  7. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    ACCEPT — PR #8749. The scoped warn (option A) is confirmed, and it is the more faithful reading of the ruling, not a narrowing of it.

    PM session session_01XeQRiAa7vYRVX5Fog7Zby8. Diff read on GitHub. Landing waits on gates.

    The judgement call you flagged

    You read the re-grade's "the skip must be counted and warned" literally, saw that it would warn on every derived skip, and scoped the warn to skips where the platform's placeholder is genuinely absent — flagging it rather than quietly choosing.

    Option A is right, and the ruling's own sentence is why. It does not stop at "counted and warned"; it says "…instead of silently leaving the bucket unseeded." The unseeded bucket is the stated harm. A skip that declines a mere refresh does not leave the bucket unseeded — the placeholder is sitting in it — so warning there would fire on a state the ruling was not written about, and one that #4632 had already decided should stay summary-only. Option B would have overridden a prior decision by reading past the clause that says what the new one is for.

    The "counted" half stays literal: every skip still moves skippedAuthored, unchanged in meaning and in value. Only the "warned" half is scoped, and it is scoped to precisely the condition the ruling names. Nothing is hidden.

    And your framing of the disagreement is the part I want to keep: "if triage prefers B the pin, not the code, is what states the disagreement." Opposite-direction control 2 encodes the choice as an assertion, so a reversal is one test edit and is visible in the diff — rather than a silent behavioural drift someone reconstructs from a log six months out.

    The controls are what make this reviewable

    Control 2 is the sharp one and it is doing real work: the guard fires, skippedAuthored moves to 1, unseededDerived stays 0, no warn — because the platform placeholder exists and the cross-organization lookup simply selected the org's row first. "Warn on every skip" fails exactly there, which is what gives the warning a single meaning. Your ablation confirms it has teeth: an unconditional warn turns that one control red and only that one (30/31 still pass).

    Control 1 covers the other direction (ordinary derivation → no warn, no counter), so an unconditional warn cannot pass either.

    Two things I want on the record

    The remediation line is withheld where it would be wrong. Where an organization's row is what stands in the way, the message says there is nothing to remove, because ADR-0066 D1 explicitly supports admins extending the registry — printing "rename or delete it" there would advise removing a legitimate row. That distinction is not decoration; a diagnostic that tells an operator to delete supported data is worse than no diagnostic.

    The fixture bug you caught rather than papered over. A helper parameter defaulting to 'admin' silently rebuilt the absent-managed_by row of the provenance matrix as an admin row, because a JS default fires on an explicitly passed undefined — collapsing that case into its neighbour while staying green. A three-row matrix where one row is secretly a duplicate of another is a test that reports coverage it does not have. The fixture now requires the parameter, and the comment says why.

    Scope and premise, verified

    Out-of-scope finding

    #8751 — the derived half's UPDATE branch reaches the same unseeded-bucket end state by a different route: a platform-marked row inside an organization passes the single-condition managed_by === 'platform' guard, where the curated half uses the conjunction with organization_id: null since #8470. Measured, and explicitly noted as invisible to this card's new counter. That asymmetry between the two halves' guards is the same species this card is about, one branch over — a good filing, and correctly not widened into.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions