Skip to content

Commit b3fc122

Browse files
docs(qa): 18.0 sweep anchor round 13 — QA round 3 surface:cli findings in cli.json, plus three carried items (#24850)
Refs #24579 Clause-②: no Round 13 of the 18.0 sweep's wave anchor (claim `6288038866`, PM loop round 22). It covers QA round 3's `surface:cli` checklist-accuracy findings C1–C9 (QA record #24786) and B6 (QA record #24787) in `cli.json`, plus three items carried on the anchor. Only checklist text changes. Twelve items in three area files (`cli.json` 9, `access-security.json` 2, `approvals.json` 1) each take one `revision` bump and one `history` row naming the record and the finding. There is no product code, test, pin, changeset text or `RUNNER.md` change. Every finding was re-read in the code at base `7697ddcf94` before it was written, and all of them hold. Every source cited below is byte-identical at the merged `origin/main` `5cb6e6e0aa`. A scripted textual editor made the edits: it re-parses each area file and asserts that only the target items changed, and in each of them only the declared fields, plus `revision` (+1) and one appended `history` row. The raw file is never re-serialized. ## Changes | finding | item | field | old → new | code anchor or pin (read at `7697ddcf94`) | |:--|:--|:--|:--|:--| | #24786 C1 | `cli.migrate-duplicates-inventory` | `acceptance[3].verify` | "plan's stdout carries '[Registry] Registered view: NAME'" → under `--json`, stdout is the plan document alone and every boot line goes to stderr; only the human path prints the registry lines on stdout | `packages/cli/src/utils/json-stdout.ts#reserveStdoutForJson`, taken by `bootSchemaStack` when `jsonOutput` is set; the objectql registry logs through `console.log`; the `[metadata_spec_invalid]` lines are `console.error` / `console.warn` | | #24786 C2 | `cli.migrate-duplicates-inventory` | `fixtures.requires[1]` | appended: the stock showcase has one org-scoped identifier (`showcase_field_zoo.f_autonumber`), so declare a second one; drop `uniq_OBJ_organization_id_FIELD` before seeding the repeat; plan then reports that drop as `index_mismatch` | `packages/drivers/driver-sql/src/schema-drift.ts#buildIndexName`, and the declared-index-missing arm there (`index_mismatch`, actual `(absent)`); the autonumber default `unique: 'organization'` | | #24786 C3 | `cli.dev-boot-contract` | `fixtures.requires[2]` (new) | none → the console leg needs `pnpm objectui:build`; on an unbuilt tree `/_console/` answers 503 "The ObjectStack Console is not built …", recorded `blocked(environment)` | `packages/cli/src/utils/console.ts#createConsoleNotBuiltPlugin`; `scripts/build-console.sh` | | #24786 C4 | `cli.dev-boot-contract` | `variants[6]` | appended: the scratch stack's object must declare `sharingModel`, or the compile fails on `security-owd-unset` and `os dev` exits 1 | `packages/lint/src/validate-security-posture.ts#SECURITY_OWD_UNSET` (severity `error`); `dev.ts` "Compile failed — fix errors above before starting dev server", then `process.exit(1)` | | #24786 C5 | `cli.migrate-meta-codemod` | `steps[6]` | "… resolve the manual changes above, then run `os validate`" → "Migrated stack does not yet pass schema validation — N refusal(s) left after the chain. Resolve them, then run `os validate`:", then each refusal | `packages/cli/src/commands/migrate/meta.ts#printSchemaVerdict` | | #24786 C6 | `cli.doctor-health-report` | `acceptance[0].clause`, `acceptance[0].verify`, `steps[0]` | six unconditional rows → seven (adds `Package-authored scheduled work`, ON or OFF), plus the config block's `Platform protocol` row; the verify names the rows by their push sites and checks instead of a count | `packages/cli/src/commands/doctor.ts#scheduledWorkCheck`; the run body's unguarded `results.push` calls; `checkProtocolVersionGap` in the config block | | #24786 C7 | `cli.hook-body-extraction-gates` | `acceptance[6].verify` | "the forbidden seven" → one hook per `FORBIDDEN_PATTERNS` entry, with no number (and the warning block lists at most 20) | `packages/cli/src/utils/extract-hook-body.ts#FORBIDDEN_PATTERNS`, 14 entries at base | | #24786 C8 | `cli.validate-v18-authoring-verdicts` | `automated.ref` | a bare list of seven pins → narrowed to acceptance[0] ONLY; acceptance[1] and acceptance[2] are named as needing the CLI | the seven spec, formula and lint unit files, none of which runs `os validate` or `os build` | | #24786 C8 | `cli.verify-derived-proof-families` | `automated.ref` | a bare list of five pins → acceptance[0] for all five, and acceptance[2] via the delete-behaviors negative control; acceptance[1] (on the showcase) needs the live run | `packages/cli/test/verify-delete-behaviors.test.ts` "NEGATIVE CONTROL: reports a cascade a hook undid, counts it, and exits 1" (it spawns the CLI and asserts exit 1, `contradicted`, `contradicted 1, hardFailures 1`); every pin uses its own fixture stack | | #24786 C9 | `cli.qa-suite-execution` | `fixtures.requires[0]` | appended: the boot needs a built workspace and no console build | `scripts/check-console-sha.mjs` "ℹ No console dist … skipping SHA check", exit 0; `scripts/check-dev-prereqs.mjs` runs first in the root `dev` chain | | #24787 B6 | `cli.build-own-contract` | `acceptance[3].clause`, `acceptance[3].verify`, `fixtures.knownGaps[0]` (new) | "the failure payload is { success: false, … issues }" → each command's payload: the rule failure is build `success/error/issues` against validate `valid/errors`; the load-time refusal is `error` + `code` (`STACK_SCHEMA_INVALID`) with no finding list on both; the judged parity stays warnings and conversions | the `emitJson` payloads in `compile.ts` and `validate.ts`; `packages/cli/src/utils/format.ts#errorCodeFields` copies `code` and `httpStatus` only. The governing-text gap: no spec docblock or ADR states either command's `--json` shape (only the `conversions` element, `packages/spec/src/stack-provenance.ts#stackConversionsOf`) | | carried 11 | `access-security.no-active-org-session-semantics` | `fixtures.knownGaps[3]` | sharpened with ADR-0131 D7's reading (below) | ADR-0131 D7 and D9; `packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts` (`systemFields: { tenant: false }`, `tenant_id` docblock, `defaultRowScope` `sys_audit_log_org`) | | carried 12 | `access-security.owd-save-gate` | `acceptance[0].verify`, `acceptance[0].evidence` | "a follow-up GET finds no qa_owd_probe" → that GET plus the draft read `GET …/qa_owd_probe?state=draft` for the `?mode=draft` leg; the evidence names both reads | `packages/rest/src/meta-item-read-gate.ts#META_ITEM_READ_PARAMS` (`state`, never `mode`) | | carried 13 | `approvals.email-action-token-door` | `automated.ref` | "Full clauses: acceptance[0]–[3] only." → acceptance[0]–[3] and negative[2] | `action-page-member.integration.test.ts` "a dead link — unknown, expired, consumed — is told on the page with 200, through GET and POST alike": each page byte-equal to `renderResultPage(kind)` with no request, which renders the summary rows only when handed one (`packages/plugins/plugin-approvals/src/action-link-pages.ts#renderResultPage`); the file's two parity cases hold the self-hosted mount to the same bytes | ### Carried item 11 — the governing-text decision The rule applied: governing text decides a clause, and a clause is re-scoped only where an ADR, spec docblock or AGENTS.md rule states the answer. ADR-0131 D7 states the attribution reading. `sys_audit_log` is deployment-level state with no organization column. The organization a row is about "becomes a plain attribution field under a name the tenant-field resolver does not claim, never the tenancy anchor". Such objects "are governed by object permission, not by the wall". D9 scopes the no-NULL rule to tenant-column objects ("column ⇒ in scope"). This moves acceptance[4]'s premise: its "invisible to RLS readers" was written for #8245, when the ledger was walled. It does not answer the question, though. No ADR states that a member-less actor's audit row may carry a NULL attribution, and none states that the platform operator reads such rows. The operator's 200 and the user's 403 that QA run #24783 measured come from the implementation: the object's `defaultRowScope` `tenant_id == current_user.organization_id`, which a NULL row matches for no organization reader. So the clause stays as written, and knownGaps[3] gains the D7 reading. This falsifies the dispatch's hypothesis 2 in its conclusion: D7 does state the attribution reading, but not the two facts an alignment needs. ## Items left alone, with reasons - `access-security.no-active-org-session-semantics` acceptance[4]: kept as written, by the decision above. - `cli.hook-body-extraction-gates` steps[0] and acceptance[0].verify still say "fourteen at base 9a7838e". `FORBIDDEN_PATTERNS` still holds 14 entries at `7697ddcf94`, so both are true as dated readings. - `cli.dev-boot-contract` acceptance[0] (clause, verify, evidence): unchanged. Its console leg stands; the new requires[2] says what the leg needs and how to record it when the Console is unbuilt. - `cli.build-own-contract` acceptance[0]'s success key list: unchanged. B6 names only acceptance[3], and QA run #24787 scored acceptance[0] a pass. - PM hypothesis 1 (a roster constant can replace each count): it holds for C7, where `FORBIDDEN_PATTERNS` is a declared array and is now the anchor. It does not hold for C6: doctor has no row roster, because its rows are pushed inline in `run()`. That verify now names the push sites and checks, and states no count. ## Gates The derived list is `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, computed on the actual change set: 3 paths vs merge base `5cb6e6e0aa`, 118 changed lines. It gives 13 commands. Each one ran at head `b66554b1ad`, with its exit code captured before any pipe, and each exited 0: `pnpm check:platform-checklist` · `pnpm check:nul-bytes` · `pnpm check:doc-authoring` · `pnpm check:cross-package-test-inputs` · `pnpm check:driver-memory-census` · `pnpm check:gitlink-declared` · `pnpm check:refd-timer-probe` · `pnpm check:watch-hint-literal` · `pnpm --filter @objectstack/lint run check:doc-formula-expressions` (after a locked `turbo run build --filter=@objectstack/lint... --concurrency=2`) · `node scripts/check-ci-filter-parity.mjs` · `node scripts/check-closing-keyword-parity.mjs` · `node scripts/check-closing-keyword-parity.mjs --self-test` · `node scripts/check-comment-mask-corpus.mjs` The `--ran` reconciliation reads "13 derived, 13 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero because all 13 recorded an exit code. No per-PR CI job runs `pnpm check:platform-checklist`, so this local reading is its evidence: | reading | base `7697ddcf94` | head `b66554b1ad` | |:--|:--|:--| | items | 16 areas, 328 items | 16 areas, 328 items | | line citations | 0 survive across 21 swept documents | 0 survive across 21 swept documents | | symbol anchors | 954/964 resolved, 10 on the named residual, 18 file floors held | 965/975 resolved, 10 on the named residual, 18 file floors held | The history rows' `ref` are written as this PR's number in the commit that follows this PR's creation. That commit changes only those twelve strings, and the gate readings at that final head are in the report on #24579. Serial check: `origin/main` was merged before this PR was opened. None of the 14 open PRs touches `cli.json`, `access-security.json` or `approvals.json`, or any `docs/qa/` path, so no `git merge-tree` probe was owed. Changeset: `docs/qa/**` ships in no package. No public package's `files[]` names it, and the root package is private. So this PR carries `skip-changeset` and touches no `.changeset/*.md`. ## Acceptance notes - The load-time refusal payload of `os build --json` and `os validate --json` carries the error's message and `code`, but not the structured zod `issues`. The `STACK_SCHEMA_INVALID` ledger row (`packages/spec/src/api/error-code-ledger.zod.ts`) says that error carries them, and `errorCodeFields` copies only `code` and `httpStatus`. The message renders those issues as text: the `StackSchemaInvalidError` docblock in `packages/spec/src/stack.zod.ts` says "the message already renders those, and the envelope's job is the machine-readable half". The CLI payload declares no `issues` key, so this is not a breach of a declared contract. It is recorded in `cli.build-own-contract` knownGaps[0] and filed nowhere (carrier: none). --- _Generated by [Claude Code](https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c849af2 commit b3fc122

3 files changed

Lines changed: 82 additions & 36 deletions

File tree

‎docs/qa/platform-checklist/areas/access-security.json‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,7 +1406,7 @@
14061406
"title": "OWD save gate: the retired external dial is refused on every object write, and a packaged object's OWD cannot be changed at runtime — the managed-content seal refuses the write before any OWD rule judges it — server-side",
14071407
"since": "v16",
14081408
"status": "active",
1409-
"revision": 5,
1409+
"revision": 6,
14101410
"priority": "P1",
14111411
"surface": "api",
14121412
"personas": [
@@ -1435,8 +1435,8 @@
14351435
{
14361436
"clause": "the server refuses the retired externalSharingModel on ANY object write, draft included: the writable-object PUT carrying it answers 422 INVALID_METADATA with one issue at externalSharingModel carrying the prescription (\"`object.externalSharingModel` was removed in @objectstack/spec 18 … Delete the key.\"), and nothing is stored",
14371437
"oracle": "api",
1438-
"verify": "PUT /api/v1/meta/object/qa_owd_probe (active state, and again with ?mode=draft) answers 422 with code INVALID_METADATA, the issue path names externalSharingModel and its message carries the prescription; a follow-up GET finds no qa_owd_probe. ⛔ security-external-wider-than-internal and 403 owd_external_wider are both RETIRED spellings: the lint rule left with the key, and object-posture-gate.ts R2 was retired by the maintainer ruling on #8310",
1439-
"evidence": "the two PUT traces + the read"
1438+
"verify": "PUT /api/v1/meta/object/qa_owd_probe (active state, and again with ?mode=draft) answers 422 with code INVALID_METADATA, the issue path names externalSharingModel and its message carries the prescription; a follow-up GET /api/v1/meta/object/qa_owd_probe finds no qa_owd_probe, and for the ?mode=draft leg so does the draft read GET /api/v1/meta/object/qa_owd_probe?state=draft — a GET takes `state`, never the save door's `mode`, which it refuses with 400 (packages/rest/src/meta-item-read-gate.ts#META_ITEM_READ_PARAMS). ⛔ security-external-wider-than-internal and 403 owd_external_wider are both RETIRED spellings: the lint rule left with the key, and object-posture-gate.ts R2 was retired by the maintainer ruling on #8310",
1439+
"evidence": "the two PUT traces + the plain and the ?state=draft reads"
14401440
},
14411441
{
14421442
"clause": "the server admits the same object without the key: the PUT with only sharingModel saves 2xx — the refusal keys on the retired key, not on the posture",
@@ -1493,7 +1493,8 @@
14931493
{ "revision": 2, "date": "2026-08-18", "change": "corrected clauses 1-2 and steps 2-3 for the #8310 retirement (maintainer ruling 2026-08-13). The item tested R2 owd_external_wider / 403 at the ?mode=draft door; R2 is RETIRED from object-posture-gate.ts as a duplicate of the runtime lint door, whose refusal is 422 INVALID_METADATA rule security-external-wider-than-internal raised at the ACTIVE-state door — and draft saves deliberately defer to the draft-to-active promotion gate (#4463 D1), so the clause was unobservable where it was pointed (#9453 CF-2). ⚠️ #9453 CF-3 (that the R1 knownGap mispredicts which layer answers) was NOT applied: object-posture-gate.ts states verbatim that R1 applies only to overlay writes over an artifact-backed object and that 'the default deploy already 403s these before this gate runs' (if (!ctx.isArtifactBacked) return), which is what the knownGap and clause 4 already say", "ref": "#9386" },
14941494
{ "revision": 3, "date": "2026-08-19", "change": "the R1 knownGap, step 5 and clause 4 named the WRONG answering layer, and #9453 CF-3 was RIGHT — re-measured on a genuinely stock showcase boot and OVERTURNING the revision-2 note above (#9477). Proven stock from the server process's own /proc/<pid>/environ: zero METADATA_WRITABLE occurrences among 147 vars. Target artifact-backed, proven by GET ?layers=true returning a populated `code` layer (_packageId com.example.showcase) with `overlay: null`. A widening PUT then answered 403 declaredCode owd_widening_forbidden — reproduced twice, object byte-identical after. The mechanism the old text missed: saveMetaItem runs runAuthoringGate BEFORE the overlay/package doors on the host-config kernel the showcase boots (environmentId undefined, so protocol.ts's env-partitioned NOT_OVERRIDABLE branch never arms — the #7674 note in that file says the showcase is exactly that shape). So the deploy posture does not select the layer; the body's DIRECTION does. Control leg (same boot, same object, tightened body) passed R1 and answered 403 NOT_OVERRIDABLE, i.e. the door the knownGap called 'earlier' is in fact LATER. Six legs measured (widen/tighten x stock/hatch x ?package= present/absent) and WRITABLE_PACKAGE_REQUIRED appeared on none. ⛔ Not a product defect: every widening leg refused and the object never changed — object-posture-gate.ts behaved exactly as its executable guard (if (!ctx.isArtifactBacked) return) specifies; only the prose parenthetical in that file's header, and this item, mispredicted the door order", "ref": "#9477" },
14951495
{ "revision": 4, "date": "2026-10-13", "change": "clause 4, acceptance[3], re-pointed to the managed-content seal, with step 5, the R1 knownGap, the first knownGap's packaged tighten-only parenthetical, the R1 variant, the title and two new source notes (stale spec, #23104 A3). Revision 3 measured, at #9477, that a widening PUT on showcase_announcement met R1 first and answered 403 PERMISSION_DENIED with declaredCode owd_widening_forbidden. Two later changes reorder that. #22338 made saveMetaItem ask the package door on every kernel topology, ahead of the checks that judge the body. ADR-0131 D6 (#22401, e02833c24) made the OS_METADATA_WRITABLE hatch stop opening an item a managed package ships. A packaged object, whose type opens no overlay channel, is now refused 403 NOT_OVERRIDABLE (ITEM_LOCKED with ?package=) whatever the body says, so R1 is unreachable on this door on stock. The protection holds: nothing widens. Per the #23121 triage, no fixture package is built to reach R1. Its oracle is its unit pins, named in the clause: plugin-security object-posture-gate.test.ts for the verdict, and rest meta-object-owd-gate.test.ts, whose R1 block now pins the package door answering first, hatch open", "ref": "#23121" },
1496-
{ "revision": 5, "date": "2026-10-14", "change": "the external clauses retired with the key (#23268, ruling of record 6159300530, letter A): externalSharingModel was removed from the object schema (ADR-0049), so the Studio external-wider clause goes and the external-wider refusal, admit and shipped-declarations clauses and the D11 knownGap are rewritten: clause 1 now asserts the tombstone's 422 refusal on every object write, draft included, clause 2 the admit without the key, and the last clause that the showcase ships no retired key. The packaged-object seal clauses are unchanged (the seal is now clause 3, and step 4 names it so). surface mixed → api: no browser step remains", "ref": "#23268" }
1496+
{ "revision": 5, "date": "2026-10-14", "change": "the external clauses retired with the key (#23268, ruling of record 6159300530, letter A): externalSharingModel was removed from the object schema (ADR-0049), so the Studio external-wider clause goes and the external-wider refusal, admit and shipped-declarations clauses and the D11 knownGap are rewritten: clause 1 now asserts the tombstone's 422 refusal on every object write, draft included, clause 2 the admit without the key, and the last clause that the showcase ships no retired key. The packaged-object seal clauses are unchanged (the seal is now clause 3, and step 4 names it so). surface mixed → api: no browser step remains", "ref": "#23268" },
1497+
{ "revision": 6, "date": "2026-10-20", "change": "acceptance[0] (verify, evidence) adds the draft re-read — carried finding 3 of round 12's report on PR #24824 (wave anchor #24579 round 13), from QA run #24779: 'a follow-up GET finds no qa_owd_probe' named no read for the ?mode=draft PUT leg, and a GET with ?mode=draft answers 400. A GET takes state (META_ITEM_READ_PARAMS, meta-item-read-gate.ts at base 7697ddcf94), so the verify now re-reads the probe with ?state=draft beside the plain GET, and 'nothing is stored' holds for both legs. No clause changed", "ref": "#24850" }
14971498
]
14981499
},
14991500
{
@@ -2358,7 +2359,7 @@
23582359
"title": "A session with no active organization is a LEGAL state with fail-closed semantics: tenant-scoped reads resolve to nothing, tenant-scoped writes are refused loudly naming the missing org, and nothing is ever NULL-stamped",
23592360
"since": "v17",
23602361
"status": "active",
2361-
"revision": 7,
2362+
"revision": 8,
23622363
"priority": "P0",
23632364
"surface": "api",
23642365
"personas": [
@@ -2376,7 +2377,7 @@
23762377
"The state needs a producer, and two are durable and non-racy on the walled boot (knownGaps[2]), both measured by QA run #24783 on `isolated` and `group`. (1) NO membership: under OS_AUTH_MEMBERSHIP_POLICY=invite-only a user minted by POST /api/v1/auth/admin/create-user answers membershipCreated:false and holds no sys_member row, because the reconciler answers `policy-skip` under invite-only (packages/plugins/plugin-auth/src/reconcile-membership.ts#reconcileMembership, packages/plugins/plugin-auth/src/admin-user-endpoints.ts#bindUserToSoleOrganization), so every session it mints reads activeOrganizationId null. (An explicitly declared `auto` binds nobody under a wall either, ADR-0093 D1 `no-target-org`; no run has measured that boot.) (2) A member who CLEARS its active organization: POST /api/v1/auth/organization/set-active {\"organizationId\": null}, after which GET /api/v1/auth/get-session reads null. The two differ under `group`, where the scope is the membership set (acceptance[0]), so record WHICH producer each clause was scored on. Two former producers no longer yield the state. The signup race is gone: the session seam settles a newly created user's membership before its first session resolves an organization (ADR-0123 D3; packages/plugins/plugin-auth/src/auth-manager.ts#composeDatabaseHooks, its defaultActiveOrg hook), so only a policy that binds nobody leaves that session without one. The removed-member path ends the session's claim instead: removing a user's last membership revokes each live session that named that organization, and a user still holding another membership has such a session re-pointed to it (packages/plugins/plugin-auth/src/membership-ended-session.ts#endSessionClaimsForEndedMembership); QA run #24783 measured the removed member's token answering 401 UNAUTHENTICATED",
23772378
"`invite-only` is no longer a fixture gap: the walled boot must declare a membership policy anyway (RUNNER.md 'A walled boot is a fixture of its own', prerequisite 3), and declaring `invite-only` gives knownGaps[0]'s producer (1). SSO-JIT-pending still has no stock fixture: that producer is blocked(fixture) unless the run configures SSO.",
23782379
"requires[0]'s tenancy-walled deployment is a walled boot. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses an os dev boot naming itself, while a bare os serve in production mode judges none of them until OS_AUTH_SECRET is set (that entry). The item stays blocked (fixture, #9334) because every clause is a walled leg and the stock showcase provisions no walled boot, not for want of a producer (knownGaps[0]); its pin (automated.ref) boots the posture in-process.",
2379-
"acceptance[4] has an open question for a user who holds NO membership at all (producer (1) of knownGaps[0]): its login/logout rows in sys_audit_log carry tenant_id null, because no organization exists to stamp and ADR-0123 D3's ordering fix 'must not manufacture a membership that policy says must not exist'. QA run #24783 read those rows only as the platform operator (GET /api/v1/data/sys_audit_log filtered by user_id answered 200), while the user itself gets 403 on sys_audit_log, on both postures. ADR-0123 does not state whether that is the intended answer: D3 forbids a NULL-tenanted row under this state 'in any subsystem' and pays the audit ledger by ordering, which binds nobody under invite-only. The clause stands as written. Until the question is ruled (raised by QA run #24783's finding F3 for wave anchor #24579; no card carries it yet), score acceptance[4] on that user from what is measured (the raw rows, their tenant_id and the readers that see them) and name this question beside the verdict",
2380+
"acceptance[4] has an open question for a user who holds NO membership at all (producer (1) of knownGaps[0]): its login/logout rows in sys_audit_log carry tenant_id null, because no organization exists to stamp and ADR-0123 D3's ordering fix 'must not manufacture a membership that policy says must not exist'. QA run #24783 read those rows only as the platform operator (GET /api/v1/data/sys_audit_log filtered by user_id answered 200), while the user itself gets 403 on sys_audit_log, on both postures. ADR-0123 does not state whether that is the intended answer: D3 forbids a NULL-tenanted row under this state 'in any subsystem' and pays the audit ledger by ordering, which binds nobody under invite-only. ADR-0131 D7 moves the clause's premise without answering the question: sys_audit_log is a deployment-level ledger with no organization column, its tenant_id is an attribution field the tenant-field resolver does not claim, 'never the tenancy anchor', and such objects are 'governed by object permission, not by the wall'. So Layer 0 is inert on the ledger, and the clause's 'invisible to RLS readers' (written for #8245, when the ledger was walled) no longer names its read path; ADR-0131 D9 scopes the no-NULL rule to tenant-column objects ('column ⇒ in scope'). Neither ADR states whether a member-less actor's row may carry a NULL attribution, nor who must be able to read it. The operator's 200 and the user's 403 are the implementation: packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts, whose tenant_id docblock says NULL means a deployment-level action and whose defaultRowScope sys_audit_log_org reads tenant_id == current_user.organization_id, which a NULL row matches for no organization reader. The clause stands as written. Until the question is ruled (raised by QA run #24783's finding F3 for wave anchor #24579; no card carries it yet), score acceptance[4] on that user from what is measured (the raw rows, their tenant_id and the readers that see them) and name this question beside the verdict",
23802381
"On `group`, producer (2), a member with no active organization, gets 500 {\"code\": \"ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED\"} on a tenant-scoped create, where `isolated` and a member-less caller answer the acceptance[1] 403 naming the missing organization; nothing is written (QA run #24783: acceptance[1], acceptance[5], acceptance[6] and negative[3] FAIL on `group` only). That is the open product card #24784. The clauses stand as written: score them against it until it lands, and score the member-less caller's write separately",
23812382
"acceptance[7] (the walled `group` leg for a member with no active organization) stays red until #24784 lands: on a walled group boot at 580fd4ed (QA run #24783, re-derived under RUNNER rule 7) the create answers 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED and writes no row. It is scored only on a live walled group boot; a run without one records it blocked(fixture) citing RUNNER.md's 'A walled boot is a fixture of its own' — ⛔ never from automated.ref, whose group CONTROL stops at the plugin-security layer (RUNNER rule 6, a pin scores only the layer it drives). When #24784's fix merges, re-run the clause and drop this line"
23822383
]
@@ -2507,6 +2508,12 @@
25072508
"date": "2026-10-20",
25082509
"change": "acceptance[7], steps[8] and knownGaps[5] added, and automated.ref now names the layer its pin drives — for #24784, which the walled pass of round 3 (QA run #24783, 580fd4ed) found. Rounds 1 and 2 scored the walled legs from the pin because a walled boot was refused then; the pin's group CONTROL 'lands' a write by a member with no active organization at the plugin-security layer, and on a full walled group boot the engine refuses that write with a 500. The new clause asserts only no 5xx and no NULL stamping (which answer group gives is #24784's to decide) and stays red until #24784's fix lands. Merged onto revision 6 (round 11 of wave anchor #24579, #24801), whose knownGaps[0] producer (2) is the one steps[8] drives and whose knownGaps[4] names the same card for acceptance[1], [5], [6] and negative[3]; knownGaps[5] adds only the new clause's red state and the rule that it is never scored from the pin",
25092510
"ref": "#24802"
2511+
},
2512+
{
2513+
"revision": 8,
2514+
"date": "2026-10-20",
2515+
"change": "fixtures.knownGaps[3] sharpened with ADR-0131 D7's reading — carried item of wave anchor #24579 round 13 (PR #24801 named it in knownGaps[3]; #24343 reads sys_audit_log.tenant_id as D7's attribution field, not the anchor). Decided by governing text, read at base 7697ddcf94: D7 makes sys_audit_log a deployment-level ledger with no organization column, whose tenant_id is an attribution field and never the tenancy anchor, governed by object permission and not by the wall, and D9 scopes the no-NULL rule to tenant-column objects. No ADR states that a member-less actor's audit row may read NULL, or that the platform operator reads such rows, so acceptance[4] stays as written and knownGaps[3] names the question with the D7 reading. No clause changed",
2516+
"ref": "#24850"
25102517
}
25112518
]
25122519
},

0 commit comments

Comments
 (0)