Skip to content

docs(comments): retire the present-tense platform-admin elevation gate at the sites #13903 scoped - #14049

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-13903-elevation-gate-present-tense
Sep 1, 2026
Merged

os-steve merged 2 commits into
mainfrom
claude/issue-13903-elevation-gate-present-tense

Conversation

@claude

@claude claude Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Part of #13903 — deliberately not a closing keyword. Six of the card's seven rows are complete; the seventh is complete except for one listed line that turns out to be an it() title rather than a comment, which this PR's tier cannot take. See "The one row left half-taken" below. The PM re-grades.

Comment-only. Zero behaviour change. The #11663 platform-admin re-anchor (legs L2 and L4, both landed) retired the walled platform-admin elevation gate: under a walled posture bootstrapPlatformAdmin writes no grant row and elevates nobody — it reports. Standing is derived per request at resolve-authz-context.ts §6b-config, from a config-anchored verified email or the legacy unscoped grant row.

Premise re-verified against today's origin/main, not the card's transcription

The card measured at 46b53a25b; this branch is cut from 9b18c0475. Every quoted line was re-read in the tree before editing and every one is still there, at line numbers that moved by at most a few. Two premise details did not survive, and both are recorded rather than papered over:

  • The card is right that platform-admin.ts:287 is stale only in its NAME, but the supporting detail is stale too. bootstrap-platform-admin.ts no longer emits walled_owner_not_registered / walled_owner_not_verified. Its walled arm returns reason: 'walled_owner_email_undeclared' or 'walled_config_derived' (grep -n "reason: '" on that file lists exactly seven reasons and neither of those two is among them; walled-bootstrap-stops-granting.md records the replacement, and bootstrap-platform-admin-walled-owner.test.ts:161 says "formerly walled_owner_not_registered"). The reasoning survives and is kept, as the card requires: the two halves are still separate answers, now as the registered and verified fields of PlatformAdminStandingEntry, which is what lets the boot log print "not registered yet" apart from "registered, NOT verified". The corrected comment names that, and names the retired pair as its ancestor.
  • env.ts:169 is now :165-175 and reads "the elevation refusal in plugin-security's bootstrapPlatformAdmin". What that site actually holds today is the fail-closed backstop for an undeclared or refused config (:424) plus the config-derived standing log (:457) — neither is an elevation refusal.

The seven rows, one at a time

The card filed itself to prevent a global search-and-replace, so each row got its own reading.

  1. packages/types/src/env.ts (docblock over the exported PLATFORM_OWNER_EMAIL_ENV, ships in @objectstack/types' .d.ts) — wrong about the mechanism. The parenthetical now names what those two message sites really are, and a ⚠️ paragraph states the retirement and where standing comes from instead.
  2. packages/core/src/security/platform-admin.ts:117 (declaredSpellings) — the referent moved, not the fact. The two-spelling sys_user lookup is real and lives in resolvePlatformAdminStanding (plugin-security/platform-admin-service.ts, verified by reading it: it queries new Set([email, declaredSpelling]) and then matches by normalizePlatformAdminEmail). The comment now names that function and says it inherited the discipline from the retired gate.
  3. packages/core/src/security/platform-admin.ts:287 — ⚠️ the row the card singled out. The name is replaced, the paragraph is kept: it still says the two halves must stay SEPARATE and now says why in today's terms (registered / verified as independent per-entry fields, and the operator log that depends on them), with the retired reason-code pair named as history.
  4. packages/plugins/plugin-security/src/platform-owner-wall-bypass.ts:37-39 — "the gate must elevate" described an action nothing performs. The third site is now the derivation (resolve-authz-context.ts §6b-config, through matchesConfiguredPlatformAdmin), and the MUST-agree requirement is restated unchanged. This also removes a self-contradiction: :10-14 of the same docblock already said the elevation twin is retired.
  5. packages/plugins/plugin-security/src/security-plugin.ts:5822 — "the elevation gate's twin" was the twin of a retired thing. Now: the shared predicate in platform-owner-wall-bypass.ts, extracted as that twin, whose opposite number since the re-anchor is the per-request derivation.
  6. packages/plugins/plugin-auth/src/last-admin-guard.test.ts:1613 — "un-makes every platform admin in one write" is true for the GRANT anchor only. Corrected the way docs(comments): correct four docblock sites describing the retired platform-admin elevation gate #13907 corrected the same sentence in the source file, and it now points at last-admin-guard.re-pricing.test.ts, which pins both directions. The added sentence also says why this block's refusals are still live: the file declares no owner emails (git grep OS_PLATFORM_OWNER_EMAIL in it returns only the new comment line), so the grant anchor is the whole population here.
  7. packages/plugins/plugin-auth/src/walled-owner-operator-stamp.test.ts — seven of the eight listed lines. The three-layer docblock now names the derivation rather than the gate, and carries an explicit retirement note quoting the implementation file's own [#11973] parenthetical — that file already said this and only its test had not caught up. The :91 note now states today's consequence (born unverified, so the per-request derivation resolves it non-admin and the boot log reports "registered, NOT verified"), and the two inline comments at :395 / :443 follow.

The reverse control the card named

packages/plugins/plugin-security/src/platform-admin-service.ts:21 ("the retired elevation gate") and :99 ("the elevation gate used") are already correct and are the card's control: a correct edit does not touch them.

git diff --name-only | grep -c "platform-admin-service.ts"   ->   0

The file is not in the diff at all. Same for security-plugin.ts:6351-6367, the fenced face of #13722: the only hunk in that file is @@ -5822,2 +5822,5 @@.

The one row left half-taken, and why

Row 7 lists eight lines. Line 257 (now :270) is not a comment:

it('the email comparison MIRRORS the elevation gate: trimmed, case-insensitive, both sides', () => {

A test title is a string literal. It survives removeComments, so editing it breaks this PR's own acceptance criterion (non-comment diff empty) and the Clause-②: no grade that rests on "the whole diff is comments". The corroboration is in this PR's own measurement run: the identical comment filter applied to an unrelated real code commit (c54d4d3d7) left 279 non-comment lines, and the first of them is an it(...) title rename.

So the line is left untouched, the docblock above it says so in terms, and the title family is filed as #14046 together with two comment sites the card's table did not scope.

Zero-behaviour-change measurements

All taken at head 312a60d22. Every exit code captured before any pipe.

Non-comment diff is empty, measured rather than asserted: each of the six files, before and after, run through ts.transpileModule with removeComments: true. diff -u is 0 lines for all six, and the sha256 of the stripped output is unchanged in every case:

0aded016a914cce3a875de0c009e7b81e77273903d025bf1ced8436e315ae541  env.js                              (before AND after)
7aa362c9402b81aa625630f63abc5bf902683641ce914a96d9aca5158bd6aceb  platform-admin.js                   (before AND after)
544eff7b7f44e7e24bbc01fa48a56b1cb8f3a546fef88824202de3656e02e833  platform-owner-wall-bypass.js       (before AND after)
ee8e85d25ba0ae6b2d6a5aa9af8167dd7d0b1f58af03c63a5d5c5350ff352ef6  security-plugin.js                  (before AND after)
0ddeb985d3a7a6c783f05afa4e62849d89dbd20d02e57ae202993d467cf52a93  last-admin-guard.test.js            (before AND after)
9ef783413a9f7cc86097c948a19669d6b663a78fade7a7616df733a99bdd0653  walled-owner-operator-stamp.test.js (before AND after)

Every zero above is a reading, with a same-shaped control that comes back non-zero:

  • Does the harness see changes at all? The identical diff -u over the source pairs of the same six files: 22, 42, 20, 16, 26 and 77 lines. So a zero on the stripped pair is a measurement, not a blind spot.
  • Does the stripper actually strip? env.ts carries 3 throw in source and 1 in the stripped output; security-plugin.ts 47 and 35. The stripped text is demonstrably not the source.

A cruder second reading agrees. Over git diff -U0 of the six paths: 113 changed lines, and filtering out every line whose content begins with a JSDoc or // marker leaves 0. Control for that filter: the same pipeline over an unrelated real code commit (git show -U0 c54d4d3d7 -- '*.ts') reports 507 changed lines and 279 non-comment residue.

throw census, both directions — counted with \bthrow\b over source and over the comment-stripped output:

File Source before to after Stripped before to after
packages/types/src/env.ts 3 to 3 1 to 1
packages/core/src/security/platform-admin.ts 0 to 0 0 to 0
packages/plugins/plugin-security/src/platform-owner-wall-bypass.ts 0 to 0 0 to 0
packages/plugins/plugin-security/src/security-plugin.ts 47 to 47 35 to 35
packages/plugins/plugin-auth/src/last-admin-guard.test.ts 8 to 8 8 to 8
packages/plugins/plugin-auth/src/walled-owner-operator-stamp.test.ts 0 to 0 0 to 0

Census of the dead premise, git grep -n -i 'elevation gate\|elevation refusal' -- packages, merge base 9b18c0475 to head 312a60d22: 20 hits to 14. All fourteen survivors were read: ten are correct (retired / past tense / the corrections this PR and #13907 wrote) and four are #14046's subject.

Control-byte self-scan over the six files: grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' matches nothing (exit 1). Control: the same grep over a file holding one control byte matches (exit 0), so the clean result is a reading.

Gates

Re-derived from the actual diff, not from the dispatch list: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 312a60d22 — 6 paths, 24 path-matched families plus 7 more from three convention triggers (edits a test file; edits a file in a package owning an i18n-extract.config.ts; carries an ADR-0112 code). Both output sections were read whole; the 31 distinct commands were harvested with --commands so neither invocation spelling could be dropped. A first derivation printed a STALE TREE warning (scripts/import-prerequisite.mjs had moved on origin/main); origin/main was merged in and the derivation re-run, which is the reading above.

27 green. check:cross-package-test-inputs, check:dispatcher-error-vocabulary, check:doc-authoring, check:engine-double-contract, check:i18n-stale-fill, check:kernel-hook-pairs, check:logger-receiver-detach, check:objectql-double-limit, check:page-declaration-shape, check:published-files, check:query-options-erasure, check:slot-lookup, check:test-source-alias, check:type-check-coverage, check:type-source-resolution, check:where-matcher, check-ci-filter-parity, check-comment-mask-adoption, check-cross-package-test-inputs, check-keyed-text-bounds, check-plugin-teardown-shape, check-shard-attestation, check-system-context-census, check-tenant-audit-census, check-undeclared-dep-imports, check-affected-docs, check-drift-comment.

4 NOT MEASURED — neither a pass nor a red. Each quoted from the gate's own verdict line, not from a bare exit code:

  • check-test-completeness (exit 3) — "Nothing was measured: this gate exited before parsing a single summary line". It grades a saved turbo run test log and none was named.
  • check:dual-build-cjs-loads (exit 3) — "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/". Needs a whole-workspace build.
  • check:type-check-debt (exit 3) — "--re-measure cannot run: 56 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk". Same prerequisite. Its structural half, check:type-check-coverage, is green.
  • check:i18n (exit 1, and the exit code is the trap here) — its own text is "PREREQUISITE NOT MET — the workspace CLI is not built ... Nothing was checked: no bundle was compared and no config was parsed". Read as NOT MEASURED on the verdict line, never as a red on the code. Its self-test passed. check:i18n-stale-fill, which needs no build, is green.

All four want the same thing: a full workspace build. One was attempted under scripts/pm/os-verify-lock.sh and completed 31 of 32 turbo tasks in 7m34s before the container's ~10-minute foreground cap killed it (exit 143); three further lock acquisitions returned VERDICT queue-timeout (exit 99) against a queue that held five other agents' builds and test runs throughout. Declared narrowing: these four are left to CI, which builds the workspace as a matter of course. Nothing in this diff can move them — the stripped output is byte-identical, so no dist/, no emitted bundle and no ledgered type count can differ.

Beyond the derived family:

  • pnpm lint (eslint . --no-inline-config, the whole repo, the unconditional CI step) — green, 2m50s. Not narrowed.
  • Per-package typecheck and the two touched test files are the other half of that same narrowing: they need @objectstack/plugin-auth and @objectstack/plugin-security built, which is the lock turn that did not come. Recorded as NOT MEASURED rather than claimed. What a comment edit could plausibly break — an unterminated block comment taking the rest of a file with it — is measured twice over regardless: all six files were parsed by ts.transpileModule (that is where the byte-identical stripped output comes from) and by ESLint in the green whole-repo run.

Changeset: skip-changeset, deliberately

The label is applied. skip-changeset is this repo's exemption for a PR that publishes nothing, and a comments-only diff is on its closed list — the same judgment #13907 made for the same card family. The nuance is real and worth naming: packages/types docblocks do ship, in .d.ts. But what ships there is comment text, not type shape — no type, no exported name, no runtime, no contract moves, and the byte-identical stripped output above is exactly that statement. A changeset here would force a patch release of four packages whose CHANGELOG row could only say "an internal docblock now describes the current mechanism".

Out of scope, filed

#14046 — four more sites naming the retired gate: two comment sites the card's table did not scope (platform-admin.ts:19-21, the module header of a file this PR does edit elsewhere, and walled-platform-bucket-diagnostic.test.ts:105), and the two test TITLES described above, one of which is #13903's own unfinished line. Filed unassigned and unlabelled for triage.


Generated by Claude Code

…sites #13903 scoped

Comment-only. The #11663 platform-admin re-anchor (leg L4) retired the walled
platform-admin elevation gate: `bootstrapPlatformAdmin` writes no grant row
under a walled posture and elevates nobody, it reports. Standing is derived per
request at `resolve-authz-context.ts` §6b-config.

Each site is corrected on its own reading, not by a sweep: two named a mechanism
that no longer runs, one was stale only in its NAME (the reasoning it carries is
still correct and survives verbatim in substance), and the two already-past-tense
sites in `platform-admin-service.ts` are deliberately untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@github-actions github-actions Bot added the size/m label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/permissions/field-level-security.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/permissions/index.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/ui/forms.mdx (via SecurityPlugin (symbol, a top-level class))

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

  • content/docs/releases/implementation-status.mdx (via SecurityPlugin (symbol, a top-level class))

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
  • 2 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/platform-owner-wall-bypass.ts, packages/types/src/env.ts) — pages documenting those are invisible to this run
  • 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 — 32 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 836a29c27aa9dbac27cc643106e5b2bf68cc96c1 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 836a29c27aa9dbac27cc643106e5b2bf68cc96c1

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

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants