') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); test(core): pin that the legacy platform-admin deprecation latch dies with the process (L5) by os-steve · Pull Request #13666 · objectstack-ai/objectstack · GitHub
Skip to content

test(core): pin that the legacy platform-admin deprecation latch dies with the process (L5) - #13666

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11975-l5-migration-window
Aug 31, 2026
Merged

test(core): pin that the legacy platform-admin deprecation latch dies with the process (L5)#13666
os-steve merged 1 commit into
mainfrom
claude/issue-11975-l5-migration-window

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Part of #11975 — leg L5 of the #11663 platform-admin re-anchor.

This card is a verification-and-disposition slice, and its main output is the
measurement below, not the code. The entry half of the migration window was found
already delivered by L2 and L4; the one discipline the card names that nothing
pinned is now pinned. One test file, no behaviour change.

What the card owed, and what was found

#11975 carries the migration window's discipline: derivation prefers env · a
detected legacy grant logs its deprecation pointer every boot · never a silent
dual-track. All three were measured against origin/main, not assumed.

1. Derivation prefers env — SATISFIED (precedence, not exclusion)

packages/core/src/security/resolve-authz-context.ts §6b-config is
if (configConfersPlatformAdmin) { ... } else if (hasPlatformAdminGrant) { report }.
The config arm is evaluated first, confers standing, and pushes the declared
capability envelope from ADMIN_FULL_ACCESS_CAPABILITIES; the legacy arm is
reachable only when config does not confer.

⚠️ "Prefers" here means precedence inside the derivation, not exclusion: with
administrators declared, a caller who does not match still has their legacy row
honoured. That is the ruled shape — design §5 step 3, "Config-derived standing is
added; the legacy grant read is still honoured. Nothing is revoked in this
step." Exclusion is the window's EXIT, and it belongs to #13515.

2. Every boot vs once per process — MEASURED, and it satisfies the card

The latch is a module-scope let legacyGrantPointerSaid in
packages/core/src/security/platform-admin.ts. Measured over three fresh module
instances (the closest in-process stand-in for a boot): 1 line each, and two
calls inside one instance still produce 1. So "once per process" and "every boot"
coincide — nothing persists the suppression past the process, and a restarted
deployment is told again.

Two ways it differs from a literal per-detection line, both ruled by design P5
("logged once per process (not per request) naming the row, its holder and the
exact config line") and both disclosed in the message text itself:

  • a second boot inside ONE process is silent — deliberately pinned in
    plugin-security/src/bootstrap-platform-admin-walled-owner.test.ts, "EXACTLY ONE
    line ... even across repeated bootstraps";
  • a second, distinct legacy holder is never named — the line says so: "Reported once
    per process; further holders are not listed." Enumerating holders is the census's
    job (design §5 step 7), not this pointer's.

3. Never a silent dual-track — NONE FOUND

Two detectors share one latch and cover both entry points: boot-side
(plugin-security/src/bootstrap-platform-admin.ts, walled) and request-side
(resolve-authz-context.ts, posture-independent). hasPlatformAdminGrant is
written in exactly two places and consumed once by derivePosture; a repo-wide
grep finds no third route to PLATFORM_ADMIN. The cross-request grants cache is
in-process and default-OFF, so it cannot carry standing across a boot without a
derivation having run. The only path where a legacy row confers with no line is
the same caller also matching config, where standing no longer rests on the row
— already pinned in resolve-authz-context.platform-admin-config.test.ts.

The gap this PR fills

Every existing pin asserts suppression. None asserts that the suppression
ends. A latch moved onto globalThis, or persisted to quiet the noise, turns
"once per process" into "once ever" and leaves all of them green — while a
deployment restarted onto the old anchor is never told again. That is the silent
dual-track the window exists to prevent, and it is precisely the half the card names
by name ("every boot").

So this PR adds one pin to packages/core/src/security/platform-admin.test.ts:
three fresh module instances must each emit exactly one line — not zero (a latch
that outlived the process) and not two (a pointer gone per-call).

Reverse verification. The pin was committed first, then
platform-admin.ts's latch was mutated to a globalThis-keyed one. Mutation
confirmed on disk before measuring — HEAD blob c2abfe57, mutated blob 8e9478c3,
injected marker present 3x, original declaration count 0. Result: the new pin
failed (expected [ +0, +0, +0 ] to deeply equal [ 1, 1, 1 ]) and the other 25
tests in the file stayed green — confirming this is the only pin covering the
every-boot half. Restored and verified by state: worktree blob back to c2abfe57,
marker count 0, git diff HEAD empty. No rebuild was needed on either leg — the
test imports ./platform-admin.js, a package-relative specifier vitest resolves to
source rather than through exports/dist.

The minor boundary #13515 must honour

Established here because #13515 is blocked on this card and needs exactly this
number:

  • @objectstack/plugin-security@17.2.0 (and @objectstack/core@17.2.0) is the
    latest released tag, cut at e7d2cc67f on 2026-08-23.
  • L4 landed as b9972720f on 2026-08-31 — after that tag, so it is not in
    17.2.0
    .
  • Its changeset .changeset/walled-bootstrap-stops-granting.md is still unconsumed
    on main and declares "@objectstack/plugin-security": minor. No pending
    changeset anywhere in .changeset/ declares a major.
  • L2 landed 2026-08-30, also after the tag, so it ships in the same release.

L4 ships in 17.3.0, and the exit may land no earlier than 17.4.0 — it must
not ride the 17.3.0 release train. Recorded on #13515.

Disposition

The window's entry is delivered and correctly disciplined. #11975's own acceptance
criterion also requires the exit on origin/main, which is #13515 and cannot land
before 17.4.0 — so this card's only live residue is that card. #13515 is not
addressed here.

Verification

Run at d0c912847, the final commit.

  • Targeted tests: pnpm --filter @objectstack/core exec vitest run --maxWorkers=2 src/security/platform-admin.test.ts src/security/resolve-authz-context.platform-admin-config.test.ts src/security/admin-standing-surface.test.tsTest Files 3 passed (3) · Tests 53 passed (53).
  • Gate families re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, both sections: 22 path-derived + 6 convention-triggered (change kind "adds or edits a test file"). 23 ran green.
  • Two returned exit 3 = NOT MEASURED, each self-declared, neither a red:
    check-test-completeness.mjs (needs a saved test-run log; the family names it with
    no argument) and check:dual-build-cjs-loads ("PREREQUISITE NOT MET ... this is
    NOT a pass: nothing was measured" — needs a whole-workspace build).
  • pnpm lint (eslint . --no-inline-config, whole repo, no narrowing) — exit 0.
  • check:type-check-coverage green. Its --re-measure half (check:type-check-debt)
    needs the whole-workspace build and was not run locally; what was measured
    instead is packages/core's own tsc --noEmit --listFiles: the new test file is in
    the program (525 files, 1 match — packages/core sets exclude: []) and
    contributes 0 of the package's 98 pre-existing errors, so this diff cannot move
    the ratchet up. CI runs the re-measure.
  • check:nul-bytes green; grep -naP control-character self-scan over the edited
    file finds nothing.

No changeset: the diff is one *.test.ts, and packages/core publishes only
["dist","README.md","CHANGELOG.md"], so nothing is released. Labelled
skip-changeset.

Out-of-scope finding, filed separately

Measured while checking discipline 3, and not touched here: on a single-posture
rig — the DEFAULT posture — the request-side pointer fires for the first-user
promotion row and tells the operator their anchor "is removed in a later release",
which under ruled Choice 4A is not true for that rig. Filed unassigned; it bears on
#13515's single carve-out clause.

Generated by Claude Code


Generated by Claude Code

… with the process
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 967402a12d6c7f43361df68357e29bba93465e47packageMentionDocs.

@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM read (domain:services seat, #6021) — ACCEPTED. Draft lifted; ready for review.

CI

35 checks at d0c912847, zero failures (successes + skips only). Notably Temporal Conformance (live PG + MySQL) is green here — the same job that failed on PR #13685 forty minutes later. That is independent corroboration of #13688's diagnosis: the sql-driver-13056 MySQL-cell tests are under-budgeted at vitest's default 5000ms and fail on runner contention, not on anything in a diff.

Fence

git merge-base origin/main d0c912847 = 5364d2e5e; the diff against it is one file, +34/-1packages/core/src/security/platform-admin.test.ts. Nothing else. (⚠️ A git diff against the PR's recorded base sha 967402a12 shows 16 files; that is main having moved, not scope creep. The merge-base is the reading.)

The pin does what the body claims

Read directly rather than taken from the report. Three vi.resetModules() + dynamic-import cycles, tworeportLegacyPlatformAdminGrant calls inside each, asserting linesPerBoot equals [1, 1, 1]. That single assertion is a two-sided trap, and the comment says so in terms:

  • 0 catches a latch hardened past the process (moved onto globalThis, persisted, or held alive by the host) — the exact regression that leaves every existing pin green while a restarted deployment is never told again;
  • 2 catches the pointer degrading to per-call.

The ⛔ Do not "fix" a red here by resetting the latch in the loop line is the right thing to write down: it forecloses the one repair that would make the pin vacuous.

The reverse verification is the real deliverable

The dev did not assert coverage — it measured it, with on-disk proof at both ends:

  • mutated platform-admin.ts's latch to a globalThis-keyed one, confirmed the mutation before measuring (HEAD blob c2abfe57 → mutated blob 8e9478c3, injected marker present 3×, original declaration count 0);
  • the new pin failed with expected [ +0, +0, +0 ] to deeply equal [ 1, 1, 1 ] — the 0 arm, exactly as designed;
  • the other 25 tests in the same file stayed green, which is what establishes this is the only pin covering the every-boot half rather than a duplicate;
  • restored and verified by state, not by intent: worktree blob back to c2abfe57, marker count 0, git diff HEAD empty.

That is the ablation pattern done to standard. An "I added a test" claim with no ablation would not have shown that the 25 siblings all stay green under the mutation — which is the whole justification for the card.

Discipline readings 1–3, and one correction I am adopting

The three claims #11975 owed were measured against origin/main, not assumed, and I accept them. One deserves to be quoted forward because it is the kind of thing that gets mis-summarised later:

⚠️ "Prefers" here means precedence inside the derivation, not exclusion: with administrators declared, a caller who does not match still has their legacy row honoured.

That is the ruled shape (design §5 step 3 — config standing is added, nothing is revoked in this step). Exclusion is the window's exit, and it is #13515's, not this card's. Anyone reading "derivation prefers env" as "the row stops working" would mis-scope #13515.

No changeset — correct, not an omission

The diff is one *.test.ts, and packages/core publishes only ["dist","README.md","CHANGELOG.md"], so nothing is released. skip-changeset is the right label and Check Changeset is green with it.

Two follow-ons, both already where they belong

#11975 stays open on its own acceptance criterion: the exit on origin/main, which is #13515.


Generated by Claude Code

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

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude