Skip to content

feat(plugin-security): walled bootstrap stops minting the platform-admin grant row; platformAdmin audit service; legacy-grant deprecation pointer (L4) - #13514

Merged
zhuangjianguo merged 2 commits into
mainfrom
claude/issue-11974-l4-plugin-security-walled-bootstrap
Aug 31, 2026
Merged

zhuangjianguo merged 2 commits into
mainfrom
claude/issue-11974-l4-plugin-security-walled-bootstrap

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #11974

Part of #11663 (leg L4; design comment 5394453215 §6 row L4, maintainer acceptance 5404675670, bundle 1A/2B/3A/4A/5A/6A/7A — verbatim 「接受你的建议,继续」). #11973 (L3) remains open and is not addressed here; #11670 remains open.

What lands

Union of gates/tests re-run at the final commit d5eeda5 — all green (15 derived gate families incl. check:nul-bytes, plugin-security 90 test files / 1664 tests, package typecheck; downstream: verify harness posture suites 18/18, walled dogfood fixtures 18 passed / 3 env-skipped).

  1. Walled postures stop minting the grant row. bootstrapPlatformAdmin under group/isolated writes no sys_user_permission_set row, whatever accounts exist. Standing is config-derived at the one derivation site (resolve-authz-context.ts §6b-config, landed with L2/PR feat(core): anchor PLATFORM_ADMIN on a verified OS_PLATFORM_OWNER_EMAIL match, inside the one derivation site #13146 and fix(core,plugin-auth,plugin-security): every OS_PLATFORM_OWNER_EMAIL reader asks the ONE list-aware parser #13319) — each declared, verified account resolves PLATFORM_ADMIN at request time. single keeps first-user promotion and its grant row byte-for-byte (Choice 4A; over-denial pins kept).
  2. Read-only platformAdmin service (new platform-admin-service.ts, registered by SecurityPlugin): configuredEmails() (declared/refused/normalized list) + standing() (per entry: registered, verified, standing-holding user id). Frozen; no writable member (Choice 3A: no runtime mutation path). The walled bootstrap's per-boot standing log consumes the same implementation, so log and service cannot disagree.
  3. Deprecation log fires at this leg (pin [WIP] Fix error in step four of the action run #5). A detected legacy org-less human grant is pointed at the config path through core's reportLegacyPlatformAdminGrant — the SAME once-per-process latch the derivation-site reporter uses, so boot-time + request-time detection can never total two lines. Pinned: a seeded legacy grant produces exactly one line naming OS_PLATFORM_OWNER_EMAIL, the holder id and their address, across repeated bootstraps. Nothing is revoked (P5's honoured window).
  4. Explain agrees with enforcement (P8): new pin — a declared+verified admin with ZERO grant rows explains as PLATFORM_ADMIN through buildContextForUser (which delegates to the one resolver; core's own §6b-config pins cover the derivation side).
  5. Replay narrowed. shouldReplayBootstrapFor now fires only for sys_user insert/create under non-walled postures. The [security] 围墙提权只按未验证邮箱匹配 —— #11184 落地后仍存在一条更窄的同形路径:抢在 owner 之前用其邮箱注册即可被提权 #11343 update arm (email_verified/email) existed solely to re-attempt the walled elevation after the verifying write; that elevation is retired, and under walled postures no sys_user write can change the bootstrap's answer. Middleware-seam pin re-derived accordingly.
  6. The one-line local isEmailVerified alias is deleted (the predicate's canonical home is @objectstack/types, consumed via the shared standing implementation — the card's "promoted into core" premise was stale; nothing in packages/types or packages/core is touched).

Guard refusals: none deleted, none added

L3's last-admin-guard re-pricing is its own reviewed step and is untouched (as is all of packages/plugins/plugin-auth/**). The walled walled_owner_email_undeclared backstop refusal is kept with its exact reason string; walled_owner_not_registered / walled_owner_not_verified are not refusals removed but elevation outcomes whose decision moved to the derivation site — their distinctions survive as the standing report's per-entry registered/verified fields, and the fail-closed direction (unverified confers nothing) is enforced and pinned in core.

Behavior notes for the contract reviewer

  • Bootstrap return contract: under walled postures with a usable config the reason is now walled_config_derived (was walled_owner_not_registered/walled_owner_not_verified, or a grant write). already_have_admin is now a single-only outcome; under walled a pre-existing human grant row gets the deprecation pointer instead of a silent early exit. The only in-repo consumer reading the report (cli meta resync) reads seed/resync counters only — unaffected.
  • Seed-ownership handoff (claimSeedOwnership) no longer runs under walled postures — it was keyed on the promoted admin, and there is none. Walled/multi-org seed ownership is the enterprise organizations package's per-org bootstrap job (per auth-plugin.ts's posture split). single handoff unchanged.
  • Reader census for the stopped write (PM assumption 3, measured at 3800e42): plugin-auth's ensureDefaultOrganization is gated !postureEnforcesWall(...) and never runs under walled postures in this repo; last-admin-guard sees the same zero-grant-row population a fresh walled rig already has today before its owner registers (no new state); explain's provenance reads report nothing on empty tables; auto-org-admin-grant reads the permission-set rows, which this leg keeps minting (the org-less-row reap is L6, behind the census and auto-org-admin-grant resolves the organization_admin set id by name alone (limit 1, unscoped, process-cached), so walled org-admin grants can point at the organization-less row #11670's resolver scoping). Nothing revoked on existing rigs — the legacy read is honoured.
  • Known interim window (design H4, owned by L3 platform-admin re-anchor L3 (plugin-auth): re-point ensure-default-organization; re-price last-admin-guard as its own reviewed step #11973 — not addressed here): the enterprise organizations package (out of this repo) reuses ensureDefaultOrganization, which still finds "the platform admin" via grant rows and triggers on grant inserts. A FRESH walled rig running that package will not auto-create its default organization between this leg landing and L3's re-point. The design's §6 dependency table imposes no L3-before-L4 order (both depend on L2 only), and existing rigs keep their rows; flagged here so the review weighs it consciously.
  • The harness fixture item from the card was measured dead: walled harness rigs declare the dev admin as owner and the dev-admin seed already stamps email_verified: true, so config-derived standing works with zero fixture changes (packages/verify/src/harness.ts untouched).

Generated by Claude Code


Generated by Claude Code

…min grant row; platformAdmin audit service; legacy-grant deprecation pointer

Under walled postures bootstrapPlatformAdmin writes no sys_user_permission_set
row any more - standing is config-derived at the one derivation site
(resolve-authz-context 6b-config). single keeps first-user promotion and its
grant row byte-for-byte (Choice 4A). The walled branch now reports: a per-boot
standing log (one implementation with the new read-only platformAdmin service:
configuredEmails() + standing()), and a once-per-process deprecation pointer
for a detected legacy org-less grant, through the same latch the derivation-
site reporter uses, naming OS_PLATFORM_OWNER_EMAIL and the holder.

shouldReplayBootstrapFor narrows with the retired elevation: sys_user
insert/create under non-walled postures only - the #11343 update arm existed
solely to re-attempt the walled elevation after the verifying write, and under
walled postures no sys_user write can change the bootstrap's answer at all.

The local isEmailVerified alias is gone; the shared predicates
(isEmailVerifiedUserRow, normalizePlatformAdminEmail) are consumed directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@github-actions

github-actions Bot commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

29 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ff3757697600591804e2135a1e58b7afaebe0a48.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/plugins/plugin-security/src/index.ts, packages/plugins/plugin-security/src/platform-owner-wall-bypass.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (literal, 29 pages)
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 14 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 ff3757697600591804e2135a1e58b7afaebe0a48 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json ff3757697600591804e2135a1e58b7afaebe0a48

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 30, 2026
…e bootstrap rewrite shifted security-plugin.ts line numbers

Pure line rot: the #11974 edit moved seven isSystem read sites (and one
NON_READ_ANCHORS citation target) by +19/+20 lines; every re-anchored line
is content-identical to its merge-base counterpart (measured pair by pair
before editing). check-system-context-census --fix refused mechanically
(#13490's false-refusal shape: it counts shifted NON_READ_ANCHORS
citations as read anchors), so the eight citations are re-anchored by hand
to proven-identical lines. Census verdict line is back byte-identical to
the merge base's: 109 sites, 145 anchors, 27 declared non-read.

Copy link
Copy Markdown
Collaborator Author

CI status: the census red is FIXED; the remaining red is not this PR's

domain:services PM seat (reviewer of record), session session_016ZC5rNQj3WEet5HAmmAkMs.

✅ Fixed — Lint & Repo Gates / check-system-context-census

d5eeda5 shifted security-plugin.ts by +19/+20 lines and the census page anchored the old coordinates. 033e01b re-anchors them. The eight displaced pairs were a constant shift — 1392→1412, 1414→1434, 1565→1585, 2491→2511, 3808→3827, 4295→4314, 4446→4465, 4524→4543 — i.e. pure line rot, not a population change. The gate refuses population changes rather than guessing, so its --fix path was safe by construction here.

⛔ Standing down on Test Core (3/6) — it is not this PR's, and here is the measurement

1 failed / 1124 passed, and the single failure is in a package this diff does not touch:

FAIL  packages/core/src/security/authz-store-unavailable.test.ts
  > CONTROL: the scanner finds transports at all, and finds THIS repo
Error: ENOENT: no such file or directory, stat
  '.../packages/services/service-datasource/tsup.config.bundled_45dgza009nw.mjs'
  ❯ walk  authz-store-unavailable.test.ts:198:9   →   if (statSync(full).isDirectory())

This PR's 10 files are entirely inside packages/plugins/plugin-security/src/ plus one changeset and the census page. It touches neither packages/core nor service-datasource.

Root cause, so this is not filed as an unexplained flake: the test's discoverTransports walker does readdirSync then statSync per entry, and tsup.config.bundled_<random>.mjs is tsup's transient bundled-config file — written at build start, deleted at build end. The same log shows the builds running concurrently with the tests and service-datasource:build finishing one second after the ENOENT:

00:48:44  core:test  FAIL … ENOENT … service-datasource/tsup.config.bundled_45dgza009nw.mjs
00:48:45  service-datasource:build:  ELIFECYCLE Command failed.

⇒ A TOCTOU race between the walker and any concurrent build. It fails a control assertion ("the scanner finds transports at all"), which is the instrument reporting itself broken, not a transport regression.

Filed as #13560 (deduped first — no open card carried this signature; the five nearest matches were all closed devx flakes). ⛔ No fix exists to port, so none was ported.

One re-run triggered — the single confirming re-run this lane's rules allow for a failure naming code the diff does not touch. ⚠️ If it reds again on the same signature, that is treated as real and re-diagnosed; "flake" is not a root cause and will not be used as one here.

Where this PR stands

Everything else is green, including Governed Surface Queue Guard, Check Changeset, Type Check (all four legs), both single-writer guards, and the other five Test Core shards.

⛔ The PR remains a draft carrying needs:contract-review and this seat will not flip it ready, enqueue it, or arm auto-merge — it measures claude-opus-5, below CONTRACT_REVIEW_TIER. Green CI does not change that; parking is about review, not CI.


Generated by Claude Code

Merged via the queue into main with commit b997272 Aug 31, 2026
56 of 58 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-11974-l4-plugin-security-walled-bootstrap branch August 31, 2026 01:36
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…es it for the walled posture, on the 17.x line (objectstack-ai#18646)

Fixes objectstack-ai#18413

Governed surface (`docs/adr/**`), one file, one amendment:
`docs/adr/0131-total-organization-ownership-no-null-organization-id.md`
(+129 / -4). Draft, `skip-changeset`, **maintainer-merged** — no seat
flips this ready, queues it, or arms auto-merge (Prime Directive objectstack-ai#14).
`node scripts/pm/check-governed-merges.mjs --test docs/adr/0131-…md`
answers **GOVERNED**, 1 of 1 path.

## 1. The ruling and its authority

Maintainer ruling, option **B** on objectstack-ai#18336, taken in session
`session_01LmoCFLwGkcAGefVf7jryEV` on 2026-09-16T09:54Z and recorded on
that card (`#issuecomment-5695555178`). The option text the maintainer
selected, verbatim and untranslated:

> **B — objectstack-ai#11663 设计说了算**
> 修订 ADR-0131 D13,把这两个符号从其退役清单里摘掉,本卡留在 17.x 线执行。

Three things it decided: objectstack-ai#11663's design governs this leg for these two
symbols; the work stays on the **17.x** line (not re-targeted to v18,
not gated by objectstack-ai#15193); the two symbols leave D13's retirements. Recorded
under Prime Directive objectstack-ai#13 — reversing a recorded decision needs an
amended record, and this PR is that act. ADR-0131 stays **Accepted**;
only the staging of one leg moves.

## 2. Reading correction — where the symbols actually were

The card states that D13's list names both symbols. Measured on
`origin/main` `32be735e5`, that is not where they are:

| symbol | `docs/adr/` | `docs/` | `packages/` (positive control) |
|---|---|---|---|
| `reportLegacyPlatformAdminGrant` | **1** — ADR-0131 `:362`, inside
**D5** | 3 | 15 |
| `resetLegacyPlatformAdminGrantReport` | **0** | 0 | 17 |

D13's own retirement list (`:641–659` on `main`) names neither. What the
record actually carries is D5's sentence
「`reportLegacyPlatformAdminGrant` and the legacy unscoped anchor retire
with D13」 — an **assignment** to D13, which is what the ruling reaches.
So this amendment edits **D5, D14 and §7** and leaves D13's list as it
stands. The substance of the card is unaffected: before, the retirement
was D13's and therefore v18's; after, it is objectstack-ai#11663's and 17.x's.
`premise_still_valid: true`, with this correction.

## 3. The D13 text, before and after

**D5, before:**

> `PLATFORM_ADMIN` derives from `OS_PLATFORM_OWNER_EMAIL` (objectstack-ai#13514 L4) —
configuration, not a row. The `single`-posture first-user promotion
(Choice 4A), which today writes an `admin_full_access` grant row with a
NULL organization, writes it **owned by the Default Organization**
instead; under a walled posture no grant row is written (unchanged).
**`reportLegacyPlatformAdminGrant` and the legacy unscoped anchor retire
with D13. No NULL grant row is ever produced.**

**D5, after** (the bolded half is what moved):

> … under a walled posture no grant row is written (unchanged). **That
ownership change is C3's, on the v18 line, and it is unchanged by the
2026-09-16 ruling — so no NULL grant row is ever produced once C3 has
landed; until then the `single` row carries the NULL organization it
carries today, and D10's migration is what clears it. The legacy
unscoped anchor retires with D13 for the `single` posture only: its
walled half, together with the two symbols that carry the migration
window — `reportLegacyPlatformAdminGrant` and
`resetLegacyPlatformAdminGrantReport` — left D13 on 2026-09-16 and
retire on the 17.x line under objectstack-ai#11663's design instead (objectstack-ai#18336; see the
Amendment at the end). The `single` anchor's own disposition is objectstack-ai#11979's
(Choice 4B), which this record does not pre-empt.**

Plus: a `**Amended** (2026-09-17, …)` clause on the status line; one
**named exception** written into D14; the §7 post-17.2 audit line given
the same exception; and a full `## Amendment (2026-09-17, objectstack-ai#18413)`
record at the end of the file carrying the authority, the two standing
constraints, the closed question and the D14 reconciliation.

## 4. How each standing constraint is honoured

Both are quoted verbatim and untranslated in the amendment. Neither was
touched by the ruling, and both still bind.

**Choice 4A** — maintainer, 2026-09-08, objectstack-ai#16682 comment `5587754690`:

> The rest of Choice 4A (objectstack-ai#11974, 2026-08-25) stands: **retiring the
walled write must not retire the `single` one**

Honoured: the amendment narrows the retirement to the **walled posture**
in every place it names it — D5, D14's exception and the §7 line each
say "walled". The `single` write, its grant row and its zero-config
first-user promotion are stated as surviving until objectstack-ai#11979 (Choice 4B)
disposes of them. Nothing in the diff removes or narrows the `single`
half.

**The zero-config constraint** — maintainer, 2026-09-16, the same
session as the ruling:

> 比如临时启动的开发环境,我不可能去配置啊

Honoured: the amendment states that `single` keeps its zero-config
first-user promotion, and it does not touch the derivation that delivers
it — the unscoped-grant read at §6/§6b of
`packages/core/src/security/resolve-authz-context.ts`. The amendment
also records the mechanical reason the all-postures reading was never
available for this leg: the deprecation pointer is **already
posture-keyed** (`postureEnforcesWall(resolveTenancyPosture())` at the
§6b-config arm), so under `single` there is no pointer behaviour to
retire at all.

## 5. The open question — CLOSED in the text, not deferred

The STOP condition did not trigger. The two standing constraints do
**not** conflict with the D13 change: the change *reduces* what retires,
and a reduction cannot retire the `single` write; the zero-config
channel is the unscoped read, which is not the reporter and is
untouched. Closing the question needs no new maintainer decision — only
two ruled texts that the option-B ruling explicitly left alone. The
decision sentence, as it is written into the ADR:

> **Decision (2026-09-17, objectstack-ai#18413).** The `single`-posture first-user
grant row **still becomes owned by the Default Organization**. That is
D5's own answer, accepted by the maintainer on 2026-09-04 (§6, one of
the four open questions answered 「接受你的建议」), and the option-B ruling does
not touch it. It still lands **on the v18 line, under C3** (§8), exactly
where it was before this amendment. **Until C3 lands, the row keeps the
NULL organization it carries today** — the state D10's migration exists
to clear — and D1's NOT NULL constraint still reaches that table only
when D10 reports zero for it.

**Why the drop moves no row** — measured, not argued.
`reportLegacyPlatformAdminGrant`
(`packages/core/src/security/platform-admin.ts:351`) is a
once-per-process `warn` naming one holder and pointing at
`OS_PLATFORM_OWNER_EMAIL`; `resetLegacyPlatformAdminGrantReport`
(`:371`) drops that latch for tests. Neither writes the grant row,
neither reads its `organization`, and neither participates in the
derivation that confers `PLATFORM_ADMIN` — that is the
`unscopedUserPsIds` read at `resolve-authz-context.ts:839` and the check
at `:928`, untouched by this amendment. So the 17.x retirement this
amendment authorizes adds no NULL row, removes none, and re-owns none.
**D1 and D10 are therefore unaffected**: the NULL-ownership question is
not created by the drop and is not answered by it — it is answered by
D5, on D5's own schedule.

## 6. Reconciliation with D14 and with objectstack-ai#15193

**D14** 「⛔ No 17.x card narrows or removes an arm, adds a name column
beside an id column, or ships a half of this record」 gains exactly one
named exception, written into D14 itself. Two independent reasons,
either sufficient:

1. **It is not a half of this record.** The ruling assigned the leg to
objectstack-ai#11663's design. Work this record no longer governs cannot be a half of
it.
2. **It narrows no arm.** The arms D14 fences are the drivers'
`orWhereNull` arms inside `applyTenantScope` — the record's own
vocabulary (D13 `:641`, D14 `:674`, §7 `:836` "the arms are inside
`applyTenantScope` … there are **two**"; the reads in
`resolve-authz-context.ts` are called "NULL readings", never arms). The
walled legacy-anchor read and its pointer are neither of those, add no
column, and move no row.

The exception is written as **exhausted** by that leg and those two
symbols: every other retirement in D13 keeps D14's fence and stays gated
on D10's zero-NULL report.

**objectstack-ai#15193** (the v18 gate, open p1) — confirmed it no longer gates objectstack-ai#18336
under this ruling, and the amendment says so in D14's exception and in
its "What the ruling decided" list. objectstack-ai#15193's own rule binds "every
ADR-0131 execution card"; the option-B ruling took this leg out of that
set, and the ruling comment states it directly (「本卡留在 17.x 线,⛔ 不改挂
`target:v18`、⛔ 不阻塞于 objectstack-ai#15193」). ⛔ No label or body of objectstack-ai#15193 is touched by
this PR — that is objectstack-ai#18336's own re-pointing, out of scope here.

## 7. Gates — derived, run, reconciled

Derived from this worktree with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` (no hand-written path
list; the tool took the change set three-dot against merge base
`32be735e5`). 18 commands, every one run, exit code captured **before**
any pipe:

```
node scripts/check-adr-links.mjs                                      :: exit 0
node scripts/check-adr-links.mjs --self-test                          :: exit 0
node scripts/check-adr-symbol-anchors.mjs                             :: exit 0
node scripts/check-adr-symbol-anchors.mjs --self-test                 :: exit 0
node scripts/check-ci-filter-parity.mjs                               :: exit 0
node scripts/check-closing-keyword-parity.mjs                         :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test             :: exit 0
node scripts/check-comment-mask-corpus.mjs                            :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions     :: exit 0
pnpm check:adr-anchors                                                :: exit 0
pnpm check:cross-package-test-inputs                                  :: exit 0
pnpm check:doc-authoring                                              :: exit 0
pnpm check:driver-memory-census                                       :: exit 0
pnpm check:nul-bytes                                                  :: exit 0
pnpm check:pm-governed-merges                                         :: exit 0
pnpm check:pm-prior-rulings                                           :: exit 0
pnpm check:refd-timer-probe                                           :: exit 0
pnpm check:watch-hint-literal                                         :: exit 0
```

`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran …` → `18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN` — "a DERIVED
zero — all 18 recorded an exit code and none of them is 3".

ⓘ `check:doc-formula-expressions` exited **3** on the first attempt —
`PREREQUISITE NOT MET`, its own text: "Nothing was measured: this gate
exited before running a single check". Its prescribed remedy was run
(`turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint`, under `scripts/pm/os-verify-lock.sh`,
`VERDICT command-exit 0`) and the gate re-run to a real **exit 0**. The
3 is recorded here as a non-measurement, ⛔ not as a failed gate.

**Repo-wide eslint — a proven narrowing, not a skip.** ① The population
is read from eslint's own config: every `files:` entry in
`eslint.config.mjs` names only `{ts,tsx,mts,cts,js,jsx,mjs,cjs}`
(including `COMMENT_SWALLOW_FILES`), and `grep -c '\.md'
eslint.config.mjs` returns **0**. ② `pnpm exec eslint --no-inline-config
--format json` over the one changed path returns 1 result, `errorCount:
0`, whose single message is `"File ignored because no matching
configuration was supplied."` — the file is outside the linted
population entirely. ③ Invariance: the diff is one `.md` file and
touches no eslint config and no TS/JS source, so no untouched file's
verdict can move; type-aware linting does not enter into it, because the
changed file is not in the population at all. Measured against final
head `ef300b686`.

**Changeset** — `skip-changeset`. Nothing in `docs/adr/**` is shipped by
any package's `files[]`; it is the documented fast-track surface, and
the diff contains no `packages/**` path.

**Not run here, by design**: CI's own repo-wide roster. `dispatch-gates`
names 53 artifact-roster families, 11 wide-population families, 14
pending-changeset families and 1 path-scheduled CI job as outside the 18
derived commands; each is CI's run, not this PR's local debt.

## 维护者速读(草稿)

**改了什么**:只改了一个文件 —— ADR-0131。把「那两个平台管理员报告符号在 v18 退役」这条安排**摘掉**,改成:它们按
objectstack-ai#11663 的设计、在 **17.x** 线上退役,而且**只限
walled(隔离墙)姿态**。同时在文本里把一个悬着的问题**答死**:`single` 姿态那条授权行的组织归属不变 —— 仍然按
ADR-0131 原本的答案改成「默认组织所有」,仍然在 v18 的 C3 那张卡上做;在那之前它保持今天的 NULL,由 D10
的迁移来清。另外给 D14 加了一条**具名例外**,并更正了 §7 的一句话。⛔ 没动任何代码、没动任何其它 ADR。

**为什么改**:2026-09-16 您在 objectstack-ai#18336 上选了 **B —— objectstack-ai#11663 设计说了算**,并要求修订 ADR-0131
D13。ADR-0131 今天是 Accepted 且在 `main` 上,如果不先改它,objectstack-ai#18336 的移除一落地,代码就和一份已接受的
ADR 直接抵触。次序是固定的:**先本卡 → 再 objectstack-ai#18336**。另外您在同一会话说的「比如临时启动的开发环境,我不可能去配置啊」,加上
2026-09-08 关于 Choice 4A 的裁决(「retiring the walled write must not retire
the `single` one」),这两条本次裁决**一个字都没碰**,所以范围被机械地收成「只限
walled」——零配置开发环境不受任何影响。


**风险与代价(含回滚)**:代价很小。改的是治理记录的措辞与分线,**不改变任何运行时行为**:被摘掉的两个符号一个是「每进程一次的提醒日志」、一个是「测试用的开关复位」,它们既不写那条授权行、也不读它的组织字段,所以这次修订**不新增、不删除、不改写任何一行数据的归属**,D1(非空约束)和
D10(零-NULL 迁移)完全不受影响。风险面只有一个:D14 原本禁止「17.x 上只发本记录的一半」,现在它多了一条例外 ——
例外写成**一次性用尽**(只覆盖这条腿和这两个符号),D13 其余退役仍然照旧受 D10 把关。回滚成本为零:单文件 revert
即可,没有代码、没有迁移、没有发布物。

**席位意见**:(留空,待维护者裁定)

**你要做的**:审一遍这个 PR 并**手动合并**(受管面按章程只能由您合)。

## Acceptance notes

- **Noted, not filed** —
`docs/qa/platform-checklist/areas/access-security.json` `:2462` /
`:2492` describe the legacy-anchor deprecation pointer as live
behaviour, naming `reportLegacyPlatformAdminGrant`. Those two clauses go
stale for walled rigs the moment objectstack-ai#18336 lands. Not filed: it is neither
a reproducible defect, a declared-contract violation, nor a
metadata-authoring trap — and it has a named taker, objectstack-ai#18336 itself, whose
diff removes the symbol those clauses name. ⛔ Out of scope for this PR's
declared file surface (one ADR).
- **objectstack-ai#18336 is not addressed here** and stays open — this PR only clears
the ADR that blocks it. objectstack-ai#11979, objectstack-ai#15193 and objectstack-ai#11663 are cited, never
modified.

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

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation tests tooling

Projects

None yet

3 participants