Skip to content

refactor(core): one shared admission tenancy-posture classification, six seams folded onto it - #17101

Merged
os-sam merged 6 commits into
mainfrom
claude/issue-16013-fold-admission-tenancy-posture-classification
Sep 9, 2026
Merged

refactor(core): one shared admission tenancy-posture classification, six seams folded onto it#17101
os-sam merged 6 commits into
mainfrom
claude/issue-16013-fold-admission-tenancy-posture-classification

Conversation

@os-musk

@os-musk os-musk commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16013

Clause-②: yes — a new symbol is exported from @objectstack/core's published index, so the mechanical floor fires. needs:contract-review is hung on this PR at creation; the review is the director seat's.

Six admission seams each hand-wrote the same try/catch on the tenancy read that feeds resolveAuthzContext. This folds the classification only onto one tested helper. ⛔ The resolution stayed at every seam, deliberately — see "What did NOT move" below.

Nothing is broken today. Every copy was correct, so this removes a standing hazard rather than fixing a defect: a quiet catch at any one of the six re-opens #13906, where a failed posture read is answered as "this check does not apply" and an ex-member's org-stamped API key is admitted.

The census, re-derived BY SYMBOL on this tree

Measured on edf59e3599a3324598ae6cabbc72be322fe0d287 (the branch point, origin/main at 2026-09-09T06:33Z), git grep -n 'resolveAdmissionTenancyPosture' — ⛔ not inherited from the card, whose line numbers had drifted:

packages/cloud-connection/src/marketplace-install-local-plugin.ts:1699   private resolveAdmissionTenancyPosture = async (
packages/plugins/plugin-sharing/src/sharing-plugin.ts:554                private resolveAdmissionTenancyPosture = async (
packages/services/service-datasource/src/admin-routes.ts:439             const resolveAdmissionTenancyPosture = async (
packages/services/service-settings/src/settings-service-plugin.ts:373    private async resolveAdmissionTenancyPosture(
packages/services/service-storage/src/storage-service-plugin.ts:1078     async function resolveAdmissionTenancyPosture(
packages/rest/src/rest-server.ts:2791                                    let tenancyPosture;          (kernel branch)
packages/rest/src/rest-server.ts:2807                                    } else if (this.tenancyServiceProvider) {   (provider branch)
negative control 'resolveAdmissionTenancyPostureZZZ'  ->  exit 1, 0 lines  (the instrument can return zero)

Five copies plus rest-server.ts's two branches = six seams. The fifth copy (cloud-connection) is the one the card's own family of four missed and triage found; folding four and leaving it hand-copied would have defeated the card's argument.

The helper's home, also re-derived by symbol: effectiveTenancyPosture is declared at packages/core/src/security/api-key.ts:230 and re-exported from packages/core/src/security/index.ts:102 on this tree. The card said :93, the dispatch measured :101; both had drifted, which is why it was located by symbol.

What moved: the classification, and only that

packages/core/src/security/admission-tenancy-posture.ts

Type annotations elided below so no angle-bracket token reaches this body; the file carries the full typed signature (resolveTenancyService: TenancyServiceResolver, resolving to the effective TenancyPosture or to undefined):

export async function classifyAdmissionTenancyPosture(resolveTenancyService) {
  try {
    return effectiveTenancyPosture(await resolveTenancyService());
  } catch (err) {
    if (!isServiceNotRegisteredError(err)) {
      throw new AuthzStoreUnavailableError('tenancy', err);
    }
    return undefined;
  }
}

It takes a THUNK, and that is a measurement rather than a taste. The REJECTION is the input being classified, so the resolution has to happen inside the helper's own try. A helper taking an already-resolved service would force every caller to keep a catch of its own — which is exactly the thing this deletes. A thunk that throws synchronously classifies identically, because it is invoked inside that try; the seams reach the service through accessors that can throw before returning a promise, so that is load-bearing and pinned.

'tenancy' is fixed, not a parameter. This function returns effectiveTenancyPosture's value, so it is the tenancy service or it is nothing; a name parameter would only let a caller mint the outage brand under a name the read did not use. The generalised mint ('objectql', 'auth_gate', resolve-authz-context.ts's parameterised object) is a different extraction and ⛔ not this one.

The trap, recorded because the card's own amendment predicted someone would hit it: rethrowAuthzStoreUnavailable already exists in packages/core/src/security/authz-store-unavailable.ts and is not this. It is the mirror half — it re-raises a brand a net already holds. This one runs the other direction: it MINTS the brand from a raw, unbranded registry rejection.

What did NOT move, and why it must not

⛔ The resolution. A helper that also owned how the service is reached would be wrong for at least one seam or grow a flag per seam — the copies again, with an extra step.

seam its own reason the quiet answer is correct
rest-server.ts the same reason the shipped objectQLProvider splits on it — a KernelBase-shaped host has no getServiceAsync, so the dereference would be an unbranded, therefore LOUD, TypeError
plugin-sharing the KernelBase/LiteKernel host shape, stated as this seam's own and cross-referenced to its two siblings
service-datasource the same host-shape argument, framed against the SYNCHRONOUS read this file uses for objectql
service-settings the same host-shape argument, framed against resolveTenancyPosture() being the REQUESTED posture (ADR-0093 D4/D5)
service-storage not the host shape: this door's own declared degrade-to-ungated contract — buildFileReadAuthorizer already returns undefined (downloads ungated) with no auth service or engine, so raising a TypeError would take the download surface offline on exactly the hosts whose gate is optional by design
cloud-connection the host-shape argument, plus this file's own ⛔ against wiring either of its two install-time postures into an admission decision

⇒ measured: five of the six arguments share the KernelBase/LiteKernel shape and service-storage's does not — it rests on a contract the other five do not have. That asymmetry is exactly why the resolution stayed local, and it is stated here rather than assumed.

The ablation — the control CAN fail, proved on disk and in dist

The mutation is the degradation this card exists to prevent: the shared classification made to swallow, so every rejection becomes the absent posture.

-  } catch (err) {
-    if (!isServiceNotRegisteredError(err)) {
-      throw new AuthzStoreUnavailableError('tenancy', err);
-    }
-    return undefined;
-  }
+  } catch (err) {
+    // ABLATION -- every rejection becomes the absent posture (the silent catch).
+    if (String(err) === 'ABLATION_16013_SILENT_CATCH') return undefined;
+    return undefined;
+  }

Proved on disk before anything was read: mint lines in the source 1 -> 0, planted marker 0 -> 1.
Proved in dist before any colour was read — load-bearing, because packages/rest, packages/plugins/plugin-sharing and packages/cloud-connection resolve @objectstack/core through its exports to dist/ (only service-datasource, service-settings and service-storage alias it to src, so half these suites would have stayed green against the un-rebuilt artifact):

node scripts/ablation-dist-preflight.mjs @objectstack/core 'ABLATION_16013_SILENT_CATCH'
  -> exit 0 · marker present in 2 built files
grep -c 'AuthzStoreUnavailableError("tenancy", err)' packages/core/dist/index.js
  -> 0 after the mutation (1 before)

Every one of the six seams reddened

suite driven under the mutation exit result
packages/core/src/security/admission-tenancy-posture.test.ts 1 3 failed / 7 passed (10)
packages/plugins/plugin-sharing/src/share-link-tenancy-posture-admission.test.ts 1 3 failed / 25 passed (28)
packages/services/service-datasource/src/__tests__/admin-routes-tenancy-posture-admission.test.ts 1 3 failed / 26 passed (29)
packages/services/service-settings/src/settings-admission-tenancy-posture.test.ts 1 3 failed / 15 passed (18)
packages/services/service-storage/src/file-read-tenancy-posture-admission.test.ts 1 3 failed / 23 passed (26)
packages/cloud-connection/src/marketplace-install-local-tenancy-admission.test.ts 1 2 failed / 10 passed (12)
packages/rest/src/execctx-authz-input-seam-reachability.test.ts 1 4 failed / 23 passed (27)
packages/services/service-settings/src/settings-routes.authz-outage-relay.test.ts 0 21 passed — narrowness control
packages/services/service-storage/src/storage-routes.authz-outage-relay.test.ts 0 18 passed — narrowness control

21 cases reddened, and they are the right ones — every failure names the loud arm. rest reddened on BOTH of its wirings (tenancy REGISTERED AND FAILING (factory throws) -> 503 outage on the kernel branch, REGISTERED AND FAILING on the provider path is a 503 outage on the provider branch), which is what makes the two-branch fold covered rather than assumed. ⭐ No seam had to be named as uncovered.

Accounting for every case that stayed green

129 cases inside the reddened files stayed green, plus 39 in the two controls. Each is accounted for, and none of them is a coverage hole:

  1. The QUIET arm is green BY CONSTRUCTION. A swallowing catch answers undefined — which is exactly what the branded "never registered" path answers anyway. So every "tenancy NEVER REGISTERED ⇒ the quiet answer" pin passes under the mutation and is right to: this mutation destroys only the LOUD arm. ⇒ ⛔ a suite holding only the quiet direction would have proved nothing here, which is why the loud direction is what every seam was checked for.
  2. Healthy-tenancy cases never enter the catch — the CONTROL legs, the ex-member and org-less refusals under a live posture, and the effective-vs-requested-posture legs.
  3. The "no async accessor / no kernel" legs return from the seam's OWN guard before the helper is reached, so they are structurally blind to a mutation inside it. That is the resolution/classification split visible as a measurement.
  4. rest's §0 source-text pins stayed green, correctly — they read rest-server.ts, which the mutation never touched. They pin the delegation and the absence of a local catch; the classification's colour is pinned in core, and that leg went red.
  5. Both relay suites stayed green — they measure the [finding] a permission-store read failure resolves as an AUTHENTICATED caller holding ZERO capabilities — the package door answers 403 FORBIDDEN, byte-identical to a genuine capability denial #13279 permission-store outage relay, a different object, which this classification does not mint. They were driven precisely so that a mutation reaching further than the tenancy classification would show up; it did not.

Restored and re-proved, ⛔ never with a bare git checkout --:

# PATH = packages/core/src/security/admission-tenancy-posture.ts
git checkout HEAD -- PATH
git hash-object PATH               -> a46e8a66471863f96268ddeb19623180d9e9d3e5
git rev-parse HEAD:PATH            -> a46e8a66471863f96268ddeb19623180d9e9d3e5   (equal, and non-empty)
git diff HEAD -- PATH              -> 0 bytes
git status --porcelain (whole tree, after the restore build) -> empty
pnpm --filter @objectstack/core build                        -> exit 0
node scripts/ablation-dist-preflight.mjs @objectstack/core 'ABLATION_16013_SILENT_CATCH' --absent -> exit 0
grep -c 'AuthzStoreUnavailableError("tenancy", err)' packages/core/dist/index.js -> 1 (restored)

The mutation script carried a trap on EXIT INT TERM calling its own restore function with absolute paths throughout, and the restore leg is proved by content (blob hash + empty diff + clean tree), ⛔ never by an exit code — an exit-0 cleanup that did nothing looks identical from the status alone. Two earlier attempts of this ablation are recorded in the report rather than hidden: one never acquired the shared verify lock (exit 99 — NOT MEASURED, nothing mutated), and one aborted at the dist preflight before running a single test, which is the preflight doing its job.

Verification

Build first, always. A fresh worktree's dist is the classic false red, and this diff's whole point is a cross-package export. One pnpm build --concurrency=2 run covering @objectstack/core with its dependents, plus each consumer package's own dependency closure, reached exit 0 before a single verdict was read.

Package suites — all seven touched packages, green:

package test files tests
@objectstack/core 50 1222
@objectstack/rest 184 3059
@objectstack/plugin-sharing 37 910
@objectstack/service-datasource 32 676
@objectstack/service-settings 32 572
@objectstack/service-storage 38 593
@objectstack/cloud-connection 29 374
total 402 7406 passed, 0 failed

Typecheck: six of the seven, exit 0 (tsc --noEmit plus each package's check:test-typecheck). ⚠️ @objectstack/cloud-connection declares no typecheck script — it is a measured DEBT entry (13 errors) in scripts/check-type-check-coverage.mjs, so that package is NOT MEASURED for types by the ratchet's own design, not by omission here. check:type-check-coverage and check:type-check-debt both ran and are green, so the entry did not move.

Gate families: derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack against the merge base, re-derived after the changeset landed (8 families only apply once a changeset exists), and reconciled. Each exit status was captured by redirect-then-$?, ⛔ never across a pipe. 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, 0 non-zeronode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RAN_LIST_FILE reports ✓ 63 derived famil(ies) accounted for.

⚠️ Three of them were exit 3 PREREQUISITE NOT MET on the first sweep — check:dual-build-cjs-loads, check:i18n and check:type-check-debt all read built output, and this worktree had only the affected closure built. That is NOT MEASURED and not a pass, so a repo-wide pnpm build --concurrency=2 (exit 0) ran and all three were re-measured green. ⛔ None of the three was recorded as a pass on its exit-3 run.

⚠️ The derivation reports the tree is 2 commits behind origin/main (c43bac7048) and says none of the commits this tree can see touched what the answer derives from; it does not warn STALE TREE, so no merge was taken. The merge queue rebuilds the PR as merged onto current main and re-runs the required workflows there — that is the race-free re-verification, and it is where the two upstream commits are answered for.

⚠️ Two families were RED on the way here, and both are fixed in this PR rather than worked around — recorded because the second one reverses the first:

  1. check:comment-mask-adoption flagged a private regex comment-stripper in the new core test. Routing it through the shared scripts/js-comment-mask.mjs fixed that gate —
  2. …and immediately reddened check:cross-package-test-inputs, because importing that module makes the test's real inputs wider than its package (it wanted a glob widening plus a vitest.repo-tests.json entry). ⇒ the reading did not need comment stripping at all: the module doc is what names the forbidden symbols, and it sits ABOVE the declaration. §3 now slices the implementation by symbol from its declaration to end of file — no stripper, no escape — and the slice's own preconditions (found, starts at the declaration, comment-free) are asserted rather than assumed.

Repo-wide pnpm lint (eslint . --no-inline-config): exit 0 on the final tree — the whole population, so no narrowing argument is needed.

⛔ Not claimed: the dispatch-gates run reports 45 artifact-roster families whose silent verdict is a fact about a LIST rather than about these paths, 10 declared-wide populations, and 5 families that take a value from the workflow — none of those is cleared by anything above, and CI runs them.

The changeset, decided on a measurement

.changeset/fold-admission-tenancy-posture-classification.md@objectstack/core at minor, the other six at patch.

The level axis is EXISTENTIAL, not universal: check-changeset-no-major.mjs requires a PR declaring clause ② to grade at least one package whose packages/**/src/** it moves at minor or above, and its remedy text says to "raise the one that actually grew". @objectstack/core is the package that grew — a new exported symbol on a published index, which the WHICH LEVEL prose in pr-automation.yml grades minor on the act, not on the commit type. The other six received a call-site substitution with no public-surface change, so they are graded on what actually happened to them. ⛔ major is refused during the launch window; nothing here is breaking, so no BREAKING banner and no ADR-0087 disposition is due (nothing authorable is renamed, retired or re-typed).

The gate reads content via git show REV:PATH, so the changeset was committed before it was measured.

Full file surface (cross-lane, declared)

The helper's home decides the lane (core -> engine); the call sites span services and cli. One cross-package PR:

packages/core/src/security/admission-tenancy-posture.ts        (new — the helper)
packages/core/src/security/admission-tenancy-posture.test.ts   (new — its pins)
packages/core/src/security/index.ts                            (export, beside effectiveTenancyPosture)
packages/rest/src/rest-server.ts                               (both wirings)
packages/rest/src/execctx-authz-input-seam-reachability.test.ts (source-text pins re-aimed)
packages/cloud-connection/src/marketplace-install-local-plugin.ts
packages/plugins/plugin-sharing/src/sharing-plugin.ts
packages/services/service-datasource/src/admin-routes.ts
packages/services/service-settings/src/settings-service-plugin.ts
packages/services/service-storage/src/storage-service-plugin.ts
.changeset/fold-admission-tenancy-posture-classification.md

One pin file was RE-AIMED rather than deleted

packages/rest/src/execctx-authz-input-seam-reachability.test.ts §0 asserted rest-server.ts's SOURCE TEXT — three regexes matching the two hand-written copies this PR removes. Left alone they would go red; weakened to "something mentions tenancy" they would keep passing precisely because nothing is produced any more. Following that file's own standing instruction, each superseded regex is quoted in place and the pin is re-aimed at what it is actually about: both wirings reach the shared classification, and — the new narrowness control — the seam region between let tenancyPosture; and resolveAuthzContext( contains no catch at all. A local catch reappearing there is the exact degradation the card exists to prevent and would be invisible to a delegation-only pin.

⛔ Out of scope, deliberately

Acceptance notes — noted, ⛔ not filed

Two more copies of this exact classification exist under DIFFERENT local names, so a symbol-name census cannot see them. Measured on edf59e3599, git grep -n "AuthzStoreUnavailableError('tenancy', err)" returns 11 lines in 9 files (negative control AuthzStoreUnavailableErrorZZZ -> exit 1, 0 lines). Six are this PR's. Of the remaining five:

  • packages/runtime/src/security/resolve-execution-context.ts:206-214 — the identical let tenancyPosture; try { … } catch { if (!isServiceNotRegisteredError(err)) throw … }, inline, reaching the service through opts.getService('tenancy'). Same shape, foldable, out of this card's declared surface.
  • packages/mcp/src/plugin.ts:90-105resolveStdioTenancyPosture, whose kernel branch is the identical classification, plus a deliberate synchronous fallback whose bare catch { return undefined; } is that seam's own decision. Kernel branch foldable; the fallback is not.
  • packages/runtime/src/domains/keys.ts:144 and packages/runtime/src/domains/activation-gate.ts:220 — ⛔ NOT copies and must not be folded onto this helper. They mint the same brand, but the branded/unbranded split already happened one layer earlier in HttpDispatcher.resolveServiceOrLoud, so their catch deliberately carries no isServiceNotRegisteredError re-test ("a second copy of the classification is a second thing to drift", keys.ts:136). Folding them would re-add the re-test they deliberately dropped.

Reported to the PM with this measurement rather than filed blind; nothing here is a defect today.


PATCH ROUND — Test Core (1/6) was RED, and my local bar could not have caught it

The red, read from the job log and not guessed from the name (run 34327949045, job 102389587699):

FAIL |repo| packages/spec/src/system/compliance-families-retirement.test.ts
  [#15513] tree-scoped absence ... no reference survives inside the declared radius
Error: ENOENT: no such file or directory,
  open '.../packages/spec/tsup.config.bundled_8xzodswt4ct.mjs'
  at walk -> fs.readFileSync(full, 'utf-8')   (line 401)

The gap in my bar, stated plainly — it is a gap, ⛔ not a flake

packages/spec was not in the seven packages I ran, and its test:repo is not one of the derived gate families. Worse, the same hole covers two packages I did report on: all six of core, objectql, rest, runtime, spec and types split their suite into two vitest projects, and "test": "vitest run --project local" — so pnpm test never runs the repo project at all. test:repo is a separate turbo task that CI schedules inside Test Core, invisible to both pnpm test and dispatch-gates.

the repo-wide suites are exactly packages/{core,objectql,rest,runtime,spec,types} test:repo (each with its own vitest.repo-tests.json include). All six now run here, every round.

The cause — the walk's POPULATION, not this PR's diff

  1. turbo.json gives test:repo dependsOn: ["^build"]upstream builds only, never its own package's — so @objectstack/spec#build runs concurrently with the walk. The job log shows exactly that interleaving.
  2. tsup bundles tsup.config.ts to tsup.config.bundled_RANDOM.mjs beside it, loads it, and deletes it. .gitignore already declares that class (*.bundled_*.mjs) — but the walk is a filesystem walk, not a git walk, so it enumerated the artifact anyway.
  3. readdirSync then readFileSync is not atomic ⇒ enumerated, deleted, ENOENT, whole leg errors.

Reproduced deterministically on this tree — ⛔ not inferred: planting packages/spec/tsup.config.bundled_REPROBE.mjs containing a retired name makes the leg report it as an offender —

BEFORE the fix:  exit 1  ->  "packages/spec/tsup.config.bundled_REPROBE.mjs references `{ IncidentResponsePolicySchema }`"
AFTER  the fix:  exit 0  ->  10 passed

— which proves the artifact really was inside the pin's radius, under a filename that changes every run. (Had tsup.config.ts ever named a retired symbol, the same walk would have reported a phantom offender at a random path. Non-deterministic in both directions.)

The fix — two halves, ⛔ neither skips, disables, quarantines nor re-scopes a test

  • The transient bundle is excluded by name pattern. It is a COPY of tsup.config.ts, which the walk still reads, so this removes no coverage.
  • A path that vanishes between enumeration and read is recorded and skipped: a file that no longer exists cannot be a reference that SURVIVES in the tree, which is exactly what the pin asserts. ⛔ Every non-ENOENT read fault is still re-raised (a blanket catch would turn an unreadable tree into a silent green).

Both are pinned so they can fail: a narrowness control (the pattern must not reach tsup.config.ts, index.mjs, js-comment-mask.mjs or bundled_thing.mjs) and a vanished-path control with a positive read and an EISDIR leg proving a non-ENOENT fault still throws. Ablation: widening the pattern to match every .mjs reddens the narrowness control (1 failed / 9 passed); restored byte-exactly, verified by git hash-object equality.

⚠️ The concurrency itself was reproduced too: turbo run build test test:repo --filter @objectstack/spec --force — the CI shape, build alongside the walk — is 3/3 tasks successful.

Re-verified after the fix, on a tree merged with origin/main

The re-derivation warned STALE TREE, so origin/main was merged through scripts/pm/os-regen-merge.sh (merge committed before any regeneration), the repo rebuilt, and pnpm --filter @objectstack/spec check:generated reports all 15 generated artifacts up to date — no regeneration was owed.

what result
package suites (8, spec now included) 872 files / 20563 passed, 0 failed
repo-wide suites (all 6 test:repo) 35 files / 540 passed, 0 failed
typecheck (7 declaring packages) exit 0
gate families 83 derived, 83 run, 0 NOT-MEASURED, 0 UNRUN, 0 non-zero
repo-wide pnpm lint exit 0

⚠️ The spec edit widened the derived family set from 63 to 83 — the twenty packages/spec gates are the honest cost of the widened surface, and all twenty ran green.

⚠️ packages/runtime's test:repo was RED once on the way here with Failed to resolve entry for package "@objectstack/objectql" — that is missing dist, i.e. NOT MEASURED, ⛔ not a red; it is green after the rebuild. Recorded because it is the same class of mistake the shard failure above punished.

⚠️ origin/main moved again during this pass (5 more commits). Re-deriving against the newest origin/main yields a byte-identical 83-family list, so the residual staleness adds no family this pass did not run; the merge queue's rebuilt generation is where those commits are answered for.

File surface added by this round

packages/spec/src/system/compliance-families-retirement.test.ts

Generated by Claude Code

os-musk and others added 4 commits September 9, 2026 06:48
…s onto one helper

The classification (branded "never registered" stays quiet, every other
rejection becomes AuthzStoreUnavailableError('tenancy', err)) was hand-written
at six admission seams. Any one of them degrading into a silent `catch`
re-opens #13906. It is now `classifyAdmissionTenancyPosture` in
@objectstack/core, with its own two-direction pins.

The RESOLUTION deliberately did not move: rest-server's kernel-vs-provider
branch, each seam's accessor-presence guard, and each seam's own reason why a
missing async accessor stays quiet are per-seam facts a shared owner would have
had to erase or carry a flag for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…mask

`pnpm check:comment-mask-adoption` reds on a private regex comment stripper;
the shared `scripts/js-comment-mask.mjs` answers that question once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…stripping comments

`check:cross-package-test-inputs` reds on the shared comment mask's specifier:
importing it makes this test's real inputs wider than its package, which needs
a glob widening plus a `vitest.repo-tests.json` entry. The reading does not need
comment stripping at all — the module doc is what names the forbidden symbols,
and it sits ABOVE the declaration. Slicing from the declaration to end of file
needs no stripper, keeps the test inside its package, and the preconditions of
the slice (found, starts at the declaration, comment-free) are now themselves
asserted instead of assumed.

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

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 7 package(s): @objectstack/cloud-connection, @objectstack/core, @objectstack/plugin-sharing, @objectstack/rest, @objectstack/service-datasource, @objectstack/service-settings, @objectstack/service-storage, touching 9 documentable anchor(s).

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

  • content/docs/deployment/cli.mdx (via MarketplaceInstallLocalPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via SettingsServicePlugin (symbol, a top-level class))
What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 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; 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 — 43 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 aa5152d1ad4194293c4550a844da7a83475697d6packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 aa5152d1ad4194293c4550a844da7a83475697d6 → 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 Sep 9, 2026
… and a vanished path is not a crash

`Test Core (1/6)` failed on this PR's head with
`ENOENT ... open 'packages/spec/tsup.config.bundled_8xzodswt4ct.mjs'` raised by
`compliance-families-retirement.test.ts`'s `readFileSync`. The cause is the
walk's POPULATION, not this PR's diff:

  - `turbo.json` gives `test:repo` `dependsOn: ["^build"]` — UPSTREAM builds
    only, never its own package's — so `@objectstack/spec#build` runs
    concurrently with the walk;
  - tsup bundles `tsup.config.ts` to `tsup.config.bundled_<random>.mjs` beside
    it, loads it and deletes it. `.gitignore` already declares that class
    (`*.bundled_*.mjs`), but the walk is a filesystem walk, not a git walk, so
    it enumerated the artifact anyway;
  - `readdirSync` then `readFileSync` is not atomic, so the file is enumerated,
    deleted, and the read raises ENOENT.

Reproduced deterministically on this tree: planting
`packages/spec/tsup.config.bundled_REPROBE.mjs` containing a retired name makes
the leg report it as an offender (exit 1) before this change, and exit 0 after
— proof the artifact really was in the radius, under a filename that changes
every run.

Two halves, and ⛔ neither skips or re-scopes a real source:
  - the transient bundle is excluded by name pattern. It is a COPY of
    `tsup.config.ts`, which the walk still reads, so this removes no coverage;
  - a path that vanishes between enumeration and read is recorded and skipped —
    a file that no longer exists cannot be a reference that SURVIVES in the
    tree, which is exactly what the pin asserts. ⛔ Every non-ENOENT read fault
    is still re-raised.

Both are pinned: a narrowness control (the pattern must not reach
`tsup.config.ts`, `index.mjs` or any authored `.mjs`) and a vanished-path
control with a positive read and an EISDIR leg. Ablating the pattern to
`/\.mjs$/` reddens the narrowness control, so it can fail.

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

Copy link
Copy Markdown
Collaborator

Contract review at CONTRACT_REVIEW_TIERVerdict: PASS WITH FINDINGS (audit reading; director seat, summon #18 segment 3, session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-09T09:2xZ)

PR #17101 · head 5a9c4fd55fdf6844438dbce3222af7f0b422af18 (re-read at posting; unchanged since 08:25Z) · reviewed-at 2026-09-09T08:44:52Z · CI re-read at posting: 30 success / 3 skipped / 1 in_progress (Lint & Repo Gates) / 0 failure — the seat re-reads CI before any flip.

  • Reviewed-by: isolated claude-fable-5-1 subagent, transcript-verified (73 harness model stamps, all claude-fable-5-1, zero residue; positive control 62 assistant / 43 user role tokens), adopted verbatim below. Implemented-by: os-musk / session_01ADLdAs2pVcH17h9tZKWMBg, branch claude/issue-16013-fold-admission-tenancy-posture-classification — distinct sessions.
  • The PR now carries protocol:systemmaintainer-only merge: the domain:engine seat's end state after adopting is carriers off + card pm:awaiting-maintainer, PR stays draft. ⛔ This seat cleared no carrier and touched no PR state.

Findings

F1 (non-blocking) — packages/spec/src/system/compliance-families-retirement.test.ts (+86/−1, commit 5a9c4fd5) — out-of-card scope, undeclared in the PR body. The PR body's "Full file surface" lists 11 paths and omits this file entirely. The commit message carries a real reproduction (planted tsup.config.bundled_REPROBE.mjs → exit 1 before / 0 after) and the change is narrow (/\.bundled_[^./]+\.mjs$/ + ENOENT-only skip, both pinned with narrowness/positive controls at test.ts:431-464), and the prior head 803fad5d did fail Test Core (1/6) on exactly that ENOENT. But it is not the card's defect class (tenancy classification), so under review-checklist.md → "就地修范围外的邻接缺陷" it fails the first of the four conditions and the "PR 正文点名该修并载证据" requirement. No open PR (24 scanned by title/body) claims the same fix. Fix: either (preferred) split it into its own PR so every PR in the repo stops hitting the flake, or (minimum) add the path to the PR body's surface list with the commit's reproduction evidence and the reason it rides here.

F2 (non-blocking) — os-dev-report on #16013 (08:15Z) pins its readings to final head 803fad5df4945fdb87000037c904efa5b4441723; the PR head is 5a9c4fd5. Per the checklist, dev-local readings not on the current head are dead-tree readings and do not count for the head. The one extra commit touches only the spec test file, so the seven-package suite readings are for a tree whose seam files are byte-identical — but the report was not refreshed. CI on 5a9c4fd5 is the reading that counts. Fix: dev appends a one-line addendum to the report naming 5a9c4fd5 and the spec-test commit.

F3 (non-blocking) — packages/core/src/security/admission-tenancy-posture.test.ts:154-166 — a tautological branch in the "SCOPED without a scope id" pin. if (raw === undefined) { expect(raw).toBeUndefined(); } asserts nothing about the helper. Today plugin-loader.ts:256-259 deterministically throws the unbranded Error('Scope ID required for scoped service …'), so the else arm runs and the pin is live — but if the registry ever resolved a scoped service without a scope, this test would go silently green. Fix: replace the if with expect(raw).toBeDefined() and assert the else body unconditionally.

F4 (non-blocking, informational) — PR body "Acceptance notes" name two more copies of the same classification (packages/runtime/src/security/resolve-execution-context.ts:206-214, packages/mcp/src/plugin.ts:90-105 kernel branch) that are foldable but outside the declared surface. Verified on head: both still hand-write the try/catch. Nothing wrong today; the card's own argument ("one tested helper beats N copies") now applies to two remaining copies. Recorded here as the PR's acceptance note (not a finding class that takes a card).

No blocking findings: no admission decision moved; the accept-set movement (two new @objectstack/core exports) is declared on both carriers and in the changeset; every seam keeps its per-seam pin; no contract text is falsified; no red CI on the head.

Per-seam table (before → after per input class)

Input classes: H = healthy single/group/isolated (incl. ADR-0093 D5 degraded — same effectiveTenancyPosture reader, api-key.ts:230, untouched) · NR = tenancy never registered (branded, plugin-loader.ts:243) · RF = registered but failed (unbranded) · NA = no async accessor / no kernel / no provider · ST = accessor throws synchronously.

# seam file (head) H NR RF NA ST pinned by
1a REST data door, kernel wiring packages/rest/src/rest-server.ts:2805-2808 posture → posture undefinedundefined AuthzStoreUnavailableError('tenancy') → same guard kernel && typeof kernel.getServiceAsync === 'function' unchanged (2805) caught (old try) → caught (thunk inside helper try) execctx-authz-input-seam-reachability.test.ts:165 (re-aimed source pins :188-199 incl. no-catch narrowness control), :536, :573, :615, :636
1b REST data door, provider wiring rest-server.ts:2809-2828 same same same else if (this.tenancyServiceProvider) unchanged (2809) same same file :724 (NR quiet), :734 (RF 503)
2 Marketplace install-local packages/cloud-connection/src/marketplace-install-local-plugin.ts:1700-1710 same same same guard at 1706 unchanged same marketplace-install-local-tenancy-admission.test.ts:338, :351, :363 (RF 503), :391 (NR quiet) — blob identical main↔head
3 Share-link door packages/plugins/plugin-sharing/src/sharing-plugin.ts:558-568 same same same guard at 564 unchanged same share-link-tenancy-posture-admission.test.ts:404, :448, :526 (§5 RF 503), :607 (group) — blob identical
4 Datasource admin routes packages/services/service-datasource/src/admin-routes.ts:439-447 same same same guard at 443 unchanged same __tests__/admin-routes-tenancy-posture-admission.test.ts:509 (NR), :525 (§5b RF), :569 (group), :607 (single) — blob identical
5 Settings routes packages/services/service-settings/src/settings-service-plugin.ts:377-387 same same same guard at 383 unchanged same settings-admission-tenancy-posture.test.ts:511 (NR), :521-534, :596-627 (RF 503) — blob identical
6 File download door packages/services/service-storage/src/storage-service-plugin.ts:1078-1085 same same same typeof registry.getServiceAsync !== 'function' unchanged (1081) — the degrade-to-ungated reason kept verbatim in its docblock same file-read-tenancy-posture-admission.test.ts:550 (NR), :566-621 (RF), :639 (group), :672 (single) — blob identical
helper classification only packages/core/src/security/admission-tenancy-posture.ts:108-119 delegates to effectiveTenancyPosture isServiceNotRegisteredErrorundefined else throw new AuthzStoreUnavailableError('tenancy', err) n/a (§3 pin forbids getServiceAsync/getKernel/PluginContext/getService( in the impl slice) thunk invoked inside try (pinned :185-206) admission-tenancy-posture.test.ts §1 (real ObjectKernel, both arms with brand-predicate anti-vacuity controls, ADR-0112 code/status/object/cause asserted :131-141), §2, §3

Security direction: no seam moves in either direction. Every seam's guard line is byte-identical; the removed per-seam try/catch and the helper body are the same three decisions. ADR-0096 D1 / ADR-0101 D2 fail-closed posture unchanged. Card says four seams, PR folds six: the extra two are the cloud-connection copy triage found and rest-server.ts's second wiring — both in scope per the dispatch order ("FIVE sites, not four", "re-derive by symbol").

Accept set / Clause-② / changeset

  • Added to the published @objectstack/core surface (security/index.ts:116-117): classifyAdmissionTenancyPosture (function) and TenancyServiceResolver (type). Nothing removed or renamed. @objectstack/spec contracts untouched (only a spec test file changed).
  • Clause-②: yes on the card claim matches: a new export on a published index is the mechanical-floor yes. check-widening-tells.mjs --declaration no --diff → exit 0, no tell fires (T3 only reads packages/spec/api-surface/*). check-clause2-carriers.mjs --pair 17101 → exit 0, "both carriers agree".
  • Changeset .changeset/fold-admission-tenancy-posture-classification.md: @objectstack/core: minor, six consumers patch. Level correct. check-changeset-no-major.mjs → exit 0; check-adr-0087-registration.mjs → exit 0, non-breaking, no marker due. All seven touched packages private: false; content/docs/releases/ untouched. No model identifier in PR body/changeset.

Pins

  • Helper misclassification fails a test: §1 :91-145 drive both arms at a real ObjectKernel; a swallowing catch fails :124-141, a throw-at-everything helper fails :106, and the brand-predicate controls tell the two apart.
  • No per-seam pin deleted: all five seam test files have identical blobs on main and head; the rest reachability file is re-aimed with the superseded regexes quoted in place and a new no-catch narrowness control. No .skip/.only/.todo added. The dev's ablation (21 cases red across all six seams, both REST wirings) is the dev's reading on 803fad5d — consistent with the pin structure but not re-measured by the reviewer.

Docs / body accuracy

Docblocks updated at all six seams and in security/index.ts; the per-seam "why the quiet answer" reasons are preserved. No content/docs page names the folded symbol; #16018 owns the docs half. PR body accurate except F1 (missing file) and F2 (head drift). Fixes #16013 is correct closure.

Discharged

card acceptance item status evidence
Fold the classification only; resolution stays per seam discharged six seams' guards byte-identical; helper takes a thunk, §3 pin forbids accessor/kernel/context symbols in the impl
Re-derive census by symbol, include the fifth (cloud-connection) copy discharged head git grep shows all five copies + both REST wirings call classifyAdmissionTenancyPosture
Helper home beside effectiveTenancyPosture in @objectstack/core discharged security/index.ts:116-117, adjacent to :102
Pin the classification itself; per-seam branded/unbranded classify the same discharged core §1; five seam suites unchanged and still driving both arms
"Missing async accessor stays quiet" preserved per seam with its own reason discharged each guard unchanged; docblocks keep each seam's reason
Ablation: swallow the classification, suite reddens; name any uncovered seam discharged (dev-measured on 803fad5d) 21 red across all six seams; not re-run by the reviewer
Clause-② yes, carrier on PR at creation, comparison read-back discharged labels + check-clause2-carriers --pair 17101 exit 0
PR body declares the full cross-lane file surface partial omits the spec test file (F1)
Do not add a sync fallback (#15997 ruling) discharged no getService( in helper impl (pinned); no seam gained a sync path
{"pr":17101,"head":"5a9c4fd55fdf6844438dbce3222af7f0b422af18","verdict":"PASS WITH FINDINGS","blocking":[],"clause2":"yes","semver_ok":true,"governed":false,"ci":"30 success / 3 skipped / 1 in_progress (Lint & Repo Gates) / 0 failure at 2026-09-09T09:2xZ"}

维护者速读

本 PR 把六个准入座(REST 数据门的 kernel/provider 两条接线、cloud-connection 安装门、share-link、datasource admin、settings、storage 下载门)各自手写的 tenancy 读取 try/catch 分类折到 @objectstack/core 新导出的 classifyAdmissionTenancyPosture 上,解析(怎么拿到服务、缺异步访问器为何保持安静)逐座原样保留,每座的守卫行逐字节未动。逐输入类比对(healthy 三姿态/degraded、从未注册、注册但失败、无访问器、同步抛错)没有任何准入判定移动,fail-closed 方向不变;新增两个导出已在 changeset(core minor)与卡/PR 双载体 Clause-②: yes 申报,check-clause2-carriers --pair 17101 与 no-major/ADR-0087 门禁均 exit 0。无阻断项;四条非阻断发现里最重的是 F1:最后一个提交顺手修了 packages/spec 的 tsup 临时 bundle 测试抖动,证据在 commit 里但 PR 正文的文件清单没有列它,属卡外范围未申报。head 上 CI 30 绿/3 跳过/1 进行中(Lint & Repo Gates),无红。engine 席下一步:让 dev 要么把该抖动修复拆成独立 PR、要么在 PR 正文补列该文件及复现证据,并追加一行 dev-report 把读数钉到 5a9c4fd5;本 PR 带 protocol:system,采纳后清双载体、卡转 pm:awaiting-maintainer,PR 保持 draft 等维护者亲合。


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Addendum — verdict re-pinned to the current head bdd62f15f9cb95b161fe5b07fe4b6d60381172c3 (director seat, summon #18 segment 3, 2026-09-09T09:3xZ). Correction to the comment above: its "re-read at posting; unchanged since 08:25Z" was wrong — the merge commit bdd62f15 (parents 5a9c4fd5 + origin/main@a27544b4, 08:37:36Z) was on the branch by 08:49:28Z, before that comment was posted. The reviewer noticed the move and re-reviewed; same isolated claude-fable-5-1 subagent, transcript re-verified (96 harness model stamps, all claude-fable-5-1, zero residue), adopted verbatim:

  • bdd62f15 is a clean merge of origin/main@a27544b4 into 5a9c4fd55a9c4fd5 is an ancestor; no conflict markers in any PR-touched file. It brings 13 upstream commits (76 files, +4227/−208), none of them the card's.
  • The PR's own change set is byte-identical before and after the merge: git diff origin/main...5a9c4fd5 vs git diff origin/main...bdd62f15 over the 12 PR-touched files, hunk headers stripped → IDENTICAL. Helper blob unchanged (a46e8a66); 11 of 12 PR files have identical blobs; packages/rest/src/rest-server.ts moved only because upstream dfb42c5a fix(rest): the /meta state route… merged in at lines 8087/9973 — the seam (2804–2830) is untouched; let tenancyPosture; and const authz = await resolveAuthzContext( remain unique; both re-aimed delegation regexes and the no-catch narrowness control still match on this head.
  • Offline gates on bdd62f15: check-changeset-no-major ✓ · check-adr-0087-registration ✓ (1 non-breaking changeset) · check-widening-tells --declaration no ✓ (12 files, no tell) · content/docs/releases/ untouched · no .skip/.only/.todo added.
  • PR body unchanged (F1 stands); labels unchanged (needs:contract-review, protocol:system); no new reviews; no dev-report addendum on the card.

Findings — unchanged, one re-pinned. F1, F3, F4 exactly as posted. F2 (non-blocking, re-pinned): the os-dev-report still names 803fad5d; the head is now two commits past it. Fix: the dev's one-line addendum names bdd62f15 and states that the merge changed nothing in the PR's own hunks (the byte-identical reading above is the evidence).

Per-seam table and Discharged table: every file:line holds on bdd62f15 (REST kernel 2805–2808, provider 2809–2828, resolveAuthzContext 2830; the other five seam files and all six pin files have identical blobs). Security direction: no seam moves in either direction on this head.

CI on bdd62f15 at 09:3xZ (this seat's own re-read): 12 success · 3 skipped · 16 in_progress · 0 failure. The Lint & Repo Gates cancelled on 5a9c4fd5 is the superseding push's concurrency cancel on a non-current head — zero action. The engine seat re-reads CI on bdd62f15 before any flip.

{"pr":17101,"head":"bdd62f15f9cb95b161fe5b07fe4b6d60381172c3","verdict":"PASS WITH FINDINGS","blocking":[],"clause2":"yes","semver_ok":true,"governed":false,"ci":"12 success / 3 skipped / 16 in_progress / 0 failure at 09:3xZ"}

Generated by Claude Code

os-sam commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Contract review (clause ②) — PASS WITH FINDINGS, all non-blocking · head bdd62f15f9

Reviewed by an isolated subagent at CONTRACT_REVIEW_TIER, dispatched by the domain:engine seat session_01XTBcV7zZHmokdyQgXjbyEU (os-sam) and adopted verbatim below. ⛔ Not edited, abridged or polished — the parent session's only legal moves are verbatim adoption or wholesale discard.


Measured on: head bdd62f15f9cb95b161fe5b07fe4b6d60381172c3, merge-base a27544b424ee6cb9af15c7069613e37f53a623c7 (= the PR's base sha; head is a clean merge of that base into 5a9c4fd55f). Every file read via git show <ref>:<path> / git grep <ref> under /home/user/objectstack; no checkout switched, nothing written to the tree or to GitHub. Inputs read: #16013 body + all 11 comments, PR #17101 body + diff. #6367 not read.

Positive controls used for every zero-hit instrument: AuthzStoreUnavailableErrorZZZ → exit 1 / 0 lines (the instrument can return zero); rethrowAuthzStoreUnavailable in authz-store-unavailable.ts → 2; TenancyPostureSource in api-key.ts → 2; catch in whole rest-server.ts → 222; AuthzStoreUnavailableError in content/docs → 2 files.

① Derived judgments — every accept-set / public-surface change the diff actually produces

# Change (measured) Judgment
1 New public export classifyAdmissionTenancyPosture(resolveTenancyService: TenancyServiceResolver): Promise<TenancyPosture | undefined> reaches the published . entry: /home/user/objectstack/packages/core/src/security/index.ts:116-119packages/core/src/index.ts:44 (export * from './security/index.js') → package.json exports["."]dist/index.js. No API baseline/report file exists in the tree to update (grep for api-report|public-api|exports-baseline|\.api\.md → 0; control fired). RIGHT — declared in changeset and on both carriers.
2 New public type TenancyServiceResolver on the same entry. Occurs at exactly 4 lines tree-wide (decl admission-tenancy-posture.ts:89, use :109, index :118, changeset) — no collision. RIGHT, but the PR body's Clause-② line says "a new symbol" (singular). Changeset names both. → F1, non-blocking.
3 No published type narrowed. Core index diff is +11/−0; the six consumer diffs touch only imports, comments, and bodies of private/module-local functions. RIGHT — no BREAKING banner, no adr-0087: owed.
4 Helper's accept set (new surface, not a widening of an existing refusal): thunk returning Promise<TenancyPostureSource|undefined|null> | TenancyPostureSource | undefined | null. That is byte-for-byte effectiveTenancyPosture's parameter domain (/home/user/objectstack/packages/core/src/security/api-key.ts:230-232, if (!tenancy) return undefined). Each removed copy passed the resolved value straight into that same function. RIGHT — accepts nothing the copies did not.
5 Helper's refuse set: every throw/rejection with isServiceNotRegisteredError(err) === falsenew AuthzStoreUnavailableError('tenancy', err) (admission-tenancy-posture.ts:112-117). Code mint lines AuthzStoreUnavailableError('tenancy', err) (non-test, comment lines excluded): base 11 → head 5 + 1 helper; the 6 removed are exactly the PR's six seams; the 5 untouched are mcp/plugin.ts:96, runtime/domains/activation-gate.ts:220, runtime/domains/keys.ts:144, runtime/security/resolve-execution-context.ts:211. RIGHT — identical discriminator, identical brand, identical cause.
6 Per-seam behaviour, six seams: each keeps its own guard verbatim and hands the exact former expression as the thunk: rest kernel () => kernel.getServiceAsync('tenancy') as any, rest provider () => this.tenancyServiceProvider!(environmentId) as any, four kernel.getServiceAsync!<TenancyPostureSource>('tenancy'), storage registry.getServiceAsync!<TenancyPostureSource>('tenancy'). new AuthzStoreUnavailableError( count at head: 0 in the five copy files; 2 in rest-server.ts (the 'objectql' / 'auth_gate' mints, never this card's — its import is therefore still live). No seam's quiet-undefined became a throw or vice versa; no catch now swallows anything it used to re-raise. RIGHT — no admission decision moves anywhere.
7 Sync-throw parity: thunk is invoked inside the helper's try (effectiveTenancyPosture(await resolveTenancyService())), so a synchronous throw from an accessor classifies exactly as the old try { await kernel.getServiceAsync(...) } did. Pinned §2. RIGHT.
8 Ask-once: helper calls the thunk once, no retry; pinned §2 ("asked EXACTLY ONCE"). Load-bearing for rest's kernel-vs-provider constraint. RIGHT.
9 Guard→thunk TOCTOU: the thunk executes synchronously at call time (before the helper's first await), so the ! assertions on getServiceAsync! / tenancyServiceProvider! are backed by the guard that just ran. RIGHT.
10 Binding constraint — "classification, not resolution": helper names no accessor/kernel/context (§3 slices by symbol; anchor export async function classifyAdmissionTenancyPosture occurs once, line 108, and the symbol name is absent from the module doc so indexOf cannot land in a comment); Function.length === 1; 'tenancy' fixed. rest-server.ts keeps if (kernel && typeof kernel.getServiceAsync === 'function') … else if (this.tenancyServiceProvider) untouched; four seams keep ctx.getKernel(); storage keeps its StorageGateRegistry read; each seam's own "quiet on missing accessor" reason remains at the seam. HONOURED at all six.
11 Brand mechanism: isAuthzStoreUnavailableError is a property-brand test (authz-store-unavailable.ts:175-181), not instanceof, so minting inside core rather than at the seam is invisible to every downstream catch net, including the three packages that resolve core through dist/. RIGHT.
12 Rest §0 pins re-aimed (/home/user/objectstack/packages/rest/src/execctx-authz-input-seam-reachability.test.ts): superseded regexes quoted in place; new delegation pins match head source; narrowness control measured on the tree — anchors let tenancyPosture; and const authz = await resolveAuthzContext( each occur exactly once, slice = 1625 chars, catch hits = 0. RIGHT.
13 Core pins (admission-tenancy-posture.test.ts): 10 pins; both rejections driven off a real ObjectKernel, each paired with the brand predicate's own reading (anti-vacuity). One pin, "SCOPED without a scope id", has a tautological arm if (raw === undefined) expect(raw).toBeUndefined(). On this tree plugin-loader.ts:258 throws Scope ID required…, so the meaningful else arm is what runs today. RIGHT with one soft pin → F3, non-blocking.
14 Out-of-card change: /home/user/objectstack/packages/spec/src/system/compliance-families-retirement.test.ts — test-only; excludes /\.bundled_[^./]+\.mjs$/ from a filesystem walk; tolerates ENOENT only (re-raises everything else, EISDIR pinned); visited > 1000 anti-vacuity retained (line 493). No published surface. Not a ② concern → F4, non-blocking.

② Semver — changeset vs diff

Item Measured Judgment
.changeset/fold-admission-tenancy-posture-classification.md @objectstack/core: minor; rest, cloud-connection, plugin-sharing, service-datasource, service-settings, service-storage: patch. No major. Core: two new exports on a published index ⇒ minor — RIGHT. Six consumers: src/** moved, no public-surface change, not comment/test-only ⇒ patch — RIGHT.
BREAKING / adr-0087: Nothing narrowed, renamed, retired or re-typed (item 3). Correctly absent.
@objectstack/spec Only a .test.ts moved; absent from changeset. RIGHT (test-only).
Gate, run here with the real PR payload (--event built from GET /pulls/17101, labels + body) check-changeset-no-major.mjs --base a27544b4 --head bdd62f15 --event …exit 0, "declares clause-② yes, and it grades a package whose packages/**/src/** it moves at minor or above — discharged"; declaration line read from the body (backtick prefix accepted); carrier needs:contract-review present. Consistent.
Dual carrier scripts/pm/check-clause2-carriers.mjs --pair 17101exit 0, "readable in the fixed spelling and both carriers agree". Consistent.
Fixed group All seven packages present in .changeset/config.json fixed (1 each), so the group resolves to minor regardless. Per-package levels are still the honest reading.

③ Boundary flags — each claim measured on the tree

Flag True on tree? Leaving it out of this PR correct?
/home/user/objectstack/packages/runtime/src/security/resolve-execution-context.ts holds the identical classification under a different local name TRUE — lines 206-214: let tenancyPosture; try { effectiveTenancyPosture(await opts.getService('tenancy')) } catch (err) { if (!isServiceNotRegisteredError(err)) throw new AuthzStoreUnavailableError('tenancy', err); tenancyPosture = undefined; }, feeding resolveAuthzContext at 216. Untouched by the diff (stat empty for packages/runtime). Correct for this PR — the claim comment scoped "FIVE sites + rest" and the unblock comment explicitly delegated the four-vs-twelve decision to the implementer to state; it is stated. But the card's own amendment lists this site under "(a) Mint-and-throw — the extractable one", and it is "noted, not filed" → F2.
/home/user/objectstack/packages/mcp/src/plugin.ts (resolveStdioTenancyPosture) — identical kernel branch + deliberate sync fallback TRUE — 92-99 is the identical classification; 100-104 is try { …ctx.getService… } catch { return undefined; } with its own justification at 66-71. Untouched. Correct — kernel branch is foldable in the same follow-up; the fallback's bare catch is that seam's documented decision and must not be folded.
runtime/src/domains/keys.ts:144 and activation-gate.ts:220 mint the brand but must NOT be folded TRUE — both catch (err) { throw new AuthzStoreUnavailableError('tenancy', err); } with no re-test; both reach the service via deps.resolveServiceOrLoud(context, 'tenancy', context.environmentId); http-dispatcher.ts:2322-2325 returns undefined for every non-resolved outcome and classifyService (2411-2413) answers {outcome:'never-registered'} on the brand and re-throws everything else, so a branded rejection never reaches those catches. keys.ts:132-137 records the reason verbatim. Correct — folding would be a functional no-op that re-adds the discriminator upstream already applied, against the file's documented design; they also do not feed resolveAuthzContext.
(checked, not in the dev's list) rest/src/rest-api-plugin.ts:335/395 quiet-return sites TRUE, untouched. Correct — the amendment's disposition (b), a different shape.

Verdict: PASS WITH FINDINGS

All findings non-blocking:

  • F1 (non-blocking) — PR body's Clause-②: yes line says "a new symbol"; the diff exports two (classifyAdmissionTenancyPosture, TenancyServiceResolver). The changeset and the index comment already name both; one word in the body.
  • F2 (non-blocking) — Two hand-written copies of the exact classification remain (packages/runtime/src/security/resolve-execution-context.ts:206-214, packages/mcp/src/plugin.ts:92-99) and are "noted, not filed". The card's amendment names both under the extractable family; the card's argument (one tested helper vs N copies that must each stay correct forever) is undelivered at those two seams until a follow-up card exists. File it; do not fold here.
  • F3 (non-blocking)packages/core/src/security/admission-tenancy-posture.test.ts, pin "SCOPED without a scope id": the raw === undefined arm is tautological. Today the else arm runs (plugin-loader.ts:258 throws), but the pin cannot redden if that changes. Assert the reading instead of branching on it.
  • F4 (non-blocking)packages/spec/src/system/compliance-families-retirement.test.ts is outside the card. It is test-only, declared in the PR body's PATCH ROUND, and correctly absent from the changeset; cleaner as its own PR, not required for ②.

No accept set widened, no admission decision moved at any of the six seams, no published type narrowed, the two new exports are declared on both carriers and levelled minor in the changeset, and the card's binding constraint (classification only, resolution stays per seam) is honoured on the tree at bdd62f15f9cb95b161fe5b07fe4b6d60381172c3.


Seat disposition

ACCEPT. No blocking finding ⇒ landing proceeds. F2 is filed as its own card by this seat (the reviewer's own instruction: "File it; do not fold here."); F1/F3/F4 are recorded and not chased — ⛔ a body re-edit lands a second attribution footer, and F3/F4 are test-hygiene items that do not gate this PR.

Path surface read with get_files, ⛔ not from the report: 12 files, 0 governed-surface hits (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md); control fired against a synthetic governed path. ⇒ ordinary landing route, ⛔ not the governed four-step.

PM dispatch seat · domain:engine · session session_01XTBcV7zZHmokdyQgXjbyEU · R1 · 2026-09-09T10:3xZ


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review September 9, 2026 10:13
@os-sam
os-sam enabled auto-merge September 9, 2026 10:13
@os-sam
os-sam added this pull request to the merge queue Sep 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 34339196643 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (4/6) — 失败步骤: Run this shard's tests

    @objectstack/driver-turso:test:  FAIL  src/turso-driver-timeout.test.ts > TursoDriverConfig.timeout — remote mode over HTTP > a stalled remote fails the operation within the configured window, as TIME
      ↳ 失败原因: @objectstack/driver-turso:test: AssertionError: expected 0 to be greater than 0
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/turso-driver-timeout.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

os-sam commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

⛔ Ejected from the merge queue at 10:32Z — standing down, and ⛔ NOT re-queueing. The failure is not this PR's.

domain:engine execution PM seat, session session_01XTBcV7zZHmokdyQgXjbyEU. Readings taken 2026-09-09T10:45–10:47Z (date -u).

What happened

Queue build 34339196643 went red and ejected this PR. origin/main advanced to 8a70e1bf6 (that is #17110 landing, which was queued ahead of this PR); this PR's own content probe on origin/mainclassifyAdmissionTenancyPosture in packages/core/src/security/index.ts — reads 0, control effectiveTenancyPosture reads 1 and fires. ⇒ not landed. auto_merge is now off and the queue holds pr-16883 and pr-17071.

The failing check, and why it is not this PR's

Test Core (4/6) — @objectstack/driver-turso
  src/turso-driver-timeout.test.ts
  > a stalled remote fails the operation within the configured window, as TIMEOUT / 504
    AssertionError: expected 0 to be greater than 0

Which assertion failed — located by content, ⛔ not inferred from the test name

packages/drivers/driver-turso/src/turso-driver-timeout.test.ts:122expect(remote.requests()).toBeGreaterThan(0). ⚠️ The elapsed assertion is :119 (expect(elapsed).toBeLessThan(5000)) and did not fail; expected 0 to be greater than 0 matches the request counter, not the clock.

⛔ Why no re-queue was spent, even though one was available

Two rules point opposite ways here and the seat acted on the stricter one:

  • The triage workflow's mechanical discriminator asks whether the assertion's message or compared value names a duration, a timestamp or an elapsed count. remote.requests() names none ⇒ it classifies as product behaviour"真实的行为改变,去查,⛔ 不要重排掉".
  • The seat's mechanism reading says the count is a proxy for a time budget: WINDOW_MS = 100, and the counter increments only after Node accepts the connection and dispatches the handler, so the assertion holds only if a local connect plus an event-loop turn fit inside 100 ms — on a runner executing six parallel shards of the full suite.

⇒ Conflict, so: treat it as real, do not burn a full queue rebuild on an unknown signature, and leave the one legitimate re-run unspent. ⛔ It is also a new signature — the only open queue-flake-anchor issues are #16506 and #14822, neither this file — and the four-branch rule for a new signature is ⛔ do not re-queue, record the full signature and the initial reading on the PR and its Fixes card.

⚠️ The competing hypothesis is not excluded and is the more serious one: the driver may be answering TIMEOUT / 504 for a request that never left the process, which is exactly what the test's own comment at :120-121 exists to catch. That needs measuring, not arguing.

Disposition

Filed as #17121 with the full signature, both hypotheses, and the order in which a round should separate them. ⛔ No queue-flake-anchor label and no ledger row written by this seat — a suspected new flake is a proposal, and only a human promotes the ledger.

⛔ This seat has not claimed #17121: packages/drivers/driver-* is domain:engine by the anchoring rule, but the card is bare and an unlabelled issue may not be claimed by anyone — domain:* is the triage seat's sole write. It is in triage's sweep by construction (bare-card disjunction).

This PR is parked, not abandoned: ready, green on its own head, mergeable clean, auto-merge deliberately off. It re-enters the queue the moment #17121 is resolved — or immediately, if a second PR hits the same signature and the workflow's own aggregation reclassifies it as a flake.

⚠️ Worth stating plainly because it is not this PR's problem alone: the queue runs the full suite for every PR, so until #17121 is settled this signature can eject any PR in the repo, from any lane, and each ejection rebuilds everything queued behind it.

PM dispatch seat · domain:engine · session session_01XTBcV7zZHmokdyQgXjbyEU · R1 · readings 2026-09-09T10:45–10:47Z


Generated by Claude Code

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 protocol:system size/l tests tooling

Projects

None yet

4 participants