Uh oh!
There was an error while loading. Please reload this page.
feat(objectql): declare a legitimately org-less write instead of inferring it from NULL - #14923
feat(objectql): declare a legitimately org-less write instead of inferring it from NULL#14923os-musk wants to merge 10 commits into
Conversation
…rring it from NULL `resolveSystemInsertOrganization` decided per object plus posture, so an object holding BOTH org-stamped and adjudicated org-less rows fit no verdict the #13491 ledger could express: `tenant-scoped` refuses its own ruled-legitimate writes on a walled install, `global` abandons the org-stamped majority. Both specimens were parked in `unclassified`, which put the tenant-audit control's blind spot on two of the largest write populations in the platform namespace. The root of it is that one `NULL` meant both "deliberate" and "bug". This adds the channel that separates them: a per-write `orgLessWrite` declaration naming its own object and an adjudicated reason, a fourth ledger verdict (`conditional`) that ADMITS such an object into #8844's derive-or-refuse machinery, and a refusal for every declaration the ledger does not admit — so the option has no silently-ignored spelling. The check runs ahead of every early return in the resolver, which is where that property lives. The declaration rides the write OPTIONS, not the row: the post-hook declared-field door judges the row payload and would refuse the declaration before the resolver it exists to inform ever saw it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…gless-write-declaration
…he engine import block moved `check:system-context-census` anchors the elevation-read page at file:line, and the #13636 import block shifted every anchor below it in `engine.ts` (and the read-audit flush). Repaired with the gate's own `--fix`; no prose changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
📓 Docs Drift CheckThis PR changes 6 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 134 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b0baca9119c370c033fd76f03ab8a1e1a98eed0b && git checkout b0baca9119c370c033fd76f03ab8a1e1a98eed0b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 09cc6be43414214b9abfc7791b8497ea4f9dd2fc 9dce2135072fb1e3933c3fdbe5893f1ba03fe54e && git checkout -B drift-repro 09cc6be43414214b9abfc7791b8497ea4f9dd2fc && git merge --no-ff 9dce2135072fb1e3933c3fdbe5893f1ba03fe54e
node scripts/docs-audit/affected-docs.mjs --json 09cc6be43414214b9abfc7791b8497ea4f9dd2fc
|
…e wire code, and align the vocabulary with the writer B1 — `check-orgless-write-declarations.mjs` spelled the repo tooling root as a quoted literal, which the dispatch derivation reads as this gate DECLARING that tree as a population it reads. It excludes it. Respelled as an anchored regex (remedy (b), `check-published-files.mjs`' worked instance), so the escapable- literal row discharges by construction rather than by a new ledger line. B4 — `ERR_ORGLESS_WRITE_DECLARATION_REFUSED` is `status` 500 and REST forwards a string code on any `status >= 500`, so it is wire vocabulary and belongs in the ADR-0112 ledger beside its sibling `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED`. One entry plus its generated rows. Contradiction — the reason vocabulary and the `sys_audit_log` ledger evidence claimed a population no writer declares (a subject whose organization column is present but NULL). `audit-writers.ts` deliberately leaves that case to the refusal; both texts now say so, and both are runtime strings that reach operators. A1 — an empty batch built no row hook contexts, so a bogus declaration on one was the single spelling of the option that was silently ignored. It now reads the caller's options on that path, and two pins hold both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ine edit `check-system-context-census.mjs --fix`: 9 anchors rewritten, ZERO files refused — a pure line shift, which is the gate's only signal that no elevation read site arrived or vanished. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…gless-write-declaration
The merge's os-regen deferral, discharged as its own commit on a known-good base (the driver merges these paths with exit 0 while silently keeping one side, so only a regeneration on the merged tree is honest). `--fix` rewrote 16 anchors and REFUSED ZERO files. One of them — `auth-plugin.ts:1380` -> `:1405` — is drift this branch did not cause: it arrived with the 15 commits merged in above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
check:doc-authoring Rule 3b (cross-package prose-id leg) flagged the OrgLessWriteDeclarationRefusedError message: a runtime string reaching operators and generated surfaces cannot resolve `#13636`. Strip the id, keep the customer-resolvable maintainer-ruling date, and move the id to an adjacent `//` comment — the same pattern platform-object-tenancy.ts's `evidence` strings already use for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…gless-write-declaration # Conflicts: # content/docs/permissions/system-context.mdx
The merge of origin/main into this branch conflicted in content/docs/permissions/system-context.mdx (line-number anchors into packages/objectql/src/engine.ts). Taking the origin/main side blindly left a stale anchor: this branch's own orgless-write-declaration change shifts engine.ts line numbers, so the correct anchor is the pre-merge (this branch's) value, confirmed against check:system-context-census's diagnostic (ledger-excused site 10069, read site 10086). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
os-musk
commented
Sep 3, 2026
Merging paused on maintainer instruction — handed over, not abandonedProvenance. Instructed by the maintainer (elon@objectstack.ai) in the
This PR stays draft — which is the actual mechanism behind the pause, since a draft PR can neither be merged nor have auto-merge armed. It was never armed; the ⛔ UNARMED notice the implementing seat put at the top of this body stands. Card #13636 moved Three facts about what you are picking up, so none of them is a surprise:
⛔ No further work will be pushed to this branch by this seat. Generated by Claude Code |
…hen an organization authored them, no platform organization Rewritten on the maintainer's 2026-09-04 rulings (verbatim in the record): declared metadata is never seeded; Studio/build-agent metadata is DB-resident and UI-editable with no organization column; organization-level template editing stays closed (copy-on-write if ever opened); references by name, registry-first; deployment-level state has no column; #13636 / PR #14923 superseded (§1.6); the platform organization of the first draft is recorded as a rejected alternative. Refs #13564#13636 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
os-musk
commented
Sep 3, 2026
Voided on maintainer instruction — closing without mergingProvenance. Instructed by the maintainer (elon@objectstack.ai) in the
Closing this PR unmerged on that instruction. This is a maintainer disposition, not an error and not an abandonment by the seat — recorded explicitly so a sibling seat does not read the closure as a mis-sweep and reopen it. Scope of the voiding, stated preciselyThe instruction names 14923, this pull request. It does not name card #13636, and this seat does not extend it: #13636 stays open at What is discarded with itHead
One correction this PR established that outlives itIts census re-derivation settled that the ledger header at ⛔ Nothing further will be pushed to this branch. Generated by Claude Code |
hotlong
commented
Sep 3, 2026
Closed unmerged on the maintainer's ruling (2026-09-04, live chat: 「13636 的 pr 应该作废是吧」→ 「接受你的建议」). This PR makes a legitimately org-less write a declared state. ADR-0131 (PR #14976, §1.6) resolves the same root the other way: a row that is legitimately org-less is a row on an object that should carry no organization column — #13636 stays open until ADR-0131 merges and then closes as superseded (ADR-0131 §8, C11). Nothing here is lost: the two specimens it measured ( 🤖 Generated with Claude Code |
Fixes#13636
Implements direction B of the maintainer's 2026-08-31 ruling (总监席第 7 场决裁批 #17): the platform gets an explicit per-write "legitimately org-less" declaration, and
resolveSystemInsertOrganizationuses it to tell 「有意的环境级/无租户行」 from 「漏 stamp 的 bug」.Verified at
f5d20ac01, merged up toorigin/main.Constraint 3 first — the class-size census, before the channel
Re-derived at⚠️ That resolver does not read
f5d20ac01with the engine's OWN reach rule,resolveTenantFieldName(tenancy/system-write-organization.ts), applied to the registered schema — that is, afterapplySystemFields/resolveInjectedSystemColumns, which is what the engine and the driver actually see.managedBy, and an earlier revision of this body counted as though it did; the figures below replace it.origin/mainpackages/resolveTenantFieldNameanswers null)tenant-scopedglobalconditionalunclassifiedThe ledger header at
platform-object-tenancy.ts:32-37has NOT drifted. Its 84 / 25 / 59 reproduce exactly under an independent re-measurement, and the earlier claim in this body that it had drifted to 28 / 56 was wrong — it came from treatingmanagedBy: 'better-auth'as "no tenant field", which the resolver never does.sys_member,sys_teamandsys_invitationare better-auth managed and declareorganization_id: Field.lookup(...)with notenancyblock (sys-member.object.ts:20,232;sys-team.object.ts:130;sys-invitation.object.ts:200), so all three resolveorganization_idand are in reach.The writer-site split is re-derived against the pool the choice was actually made from — the 50 objects that were
unclassifiedonorigin/main. Predicate, stated so it is checkable: some tracked non-test.tsunderpackages/contains a call to one of the three engine write doors naming the object as a literal first argument (insert('name',update('name',delete('name').origin/main⇒ The class is the two ruled specimens. No unclassified object carries the citable both-populations writer fact the admission bar demands, so nothing was picked by guess. The ruling's ⛔ 不从 #13491 的 cannot-determine 里凭猜挑成员 is satisfied by measurement rather than by restraint, and
conditionalPlatformObjects()is pinned as a list (['sys_audit_log','sys_metadata']), not a count.cloudandobjectosare outside this session's repo scope, and they are where host/environment-level write paths live. This census says nothing about them. The 31 in-repo objects with no local writer are the visible edge of that gap: their writers are somewhere this tree cannot read. A member admitted from either repo needs its own census run there.Zone 2 assumptions — all four measured, all four hold
resolveSystemInsertOrganization, both readingrowHookContextsPLATFORM_PROVISIONED_COLUMNSis exactly['id','created_at','updated_at']with a landed ⛔ against a per-key exemption listExecutionContext, which the door never inspectstenancy/platform-object-tenancy.ts+tenancy/system-write-organization.tsSo a declaration carried as a key on the row payload is refused before the resolver it informs ever sees it. Confirmed, and it is why the declaration rides the write options.
Why not the context route (assumption C)
Engine.insert's option type is an intersection, so objectql contributes its own member without editingExecutionContextorDataEngineInsertOptions, and the caller's options object survives by reference intorowHookContexts[i].input.options, which the resolver's call site reads.insert()already carries a precedent for an objectql-owned option key (__partialRowErrors).A context-borne declaration would also have been wrong on the merits:
ScopedContext.sudo()copies the context across every write it makes, so one object's declaration would ride onto another's row.packages/specchange WAS required, and an earlier revision of this body denied itOrgLessWriteDeclarationRefusedErrordeclaresstatus = 500, and the REST layer forwards a stringcodeon anystatus >= 500(error-response.ts:1082-1092).error-code-ledger.zod.ts:10-16rules that every service-specific code a route may put inerror.codeis registered there, and an unregistered code fails schema parse. SoERR_ORGLESS_WRITE_DECLARATION_REFUSEDis wire vocabulary whether or not a route means it to be, and it is now registered under@objectstack/objectql, beside its exact siblingERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED. Precedent for a refusal's code shipping in the refusal's own PR:FILE_FIELD_BULK_WRITE_REFUSEDaterror-code-ledger.zod.ts:377, added bya5302c7a3(PR #7224).The diff into that single-owner surface is one ledger entry plus the two generated rows it produces (
content/docs/references/api/contract.mdx,.../error-code-ledger.mdx— 3 lines, regenerated bycheck:generated --fix), and nothing else.status-bearing wire codes, so they are not precedent for leaving this one out — but the absence of a gate means the next author is in the same position, and only a reader catches it.The shape
conditional— both populations, split per ROW. It ADMITS an object into System-context writes land untenanted at RUNTIME, so a single-tenant install keeps re-forking the autonumber scope and minting duplicate business identifiers — the producer #8686's backfill cannot reach (17.0.0 GA) #8844's derive-or-refuse machinery, so it is the strictest verdict, not a softer one: after this PR an undeclared org-less system write onsys_metadata/sys_audit_logis derived on a single-organization install and refused loudly on a walled one, exactly as atenant-scopedobject's is.orgLessWrite: { object, reason }on the write options, with a closed reason vocabulary. It names its own object, which is what stops it travelling.ERR_ORGLESS_WRITE_DECLARATION_REFUSED, 500) — wrong object, unadmitted reason, non-conditional object, malformed value, and (since this round) an unadmitted declaration on an empty batch, which was the one spelling that used to be silent. The check runs ahead of every early return in the resolver, deliberately: a check one line lower would silently ignore a declaration on any early-return object, and an option with a silently-ignored spelling is the 「静默可选标记」 the ruling disqualified by name. That placement is pinned by its own tests.loud · checkable · countable
PLATFORM_OBJECT_TENANCY, whoseconditionalentries each cite a writer factpnpm check:orgless-write-declarationsenumerates every declaration in the tree, holds each to the same ledger, and prints the count:6 declaration(s) across 6 file(s) — sys_audit_log=5, sys_metadata=1; 2 object(s) classified 'conditional'extractWatchHints(scripts/pm/dispatch-gates.mjs) reads any quoted span carrying a separator as the gate DECLARING that population — while the literal's whole job is to EXCLUDE that tree. The false declaration collapsed to a bare top-level wordhintCoversrefuses as too generic, so the gate scoredsilentfor every card in the tree: a count that exists and that no dispatch derivation can ever name.check:pm-dispatch-gatesreds on exactly that, and it did. Repaired by remedy (b) — stop spelling a bare root, respell the literal to say what the predicate means, as an anchored regex, the worked instance beingcheck-published-files.mjs. ⛔ Remedy (a), theROOT_DIR_WATCH_HINTSsubtree declaration, would have been a false declaration here (it would name the gate for every repo-root tooling edit it does not read) and was refused for that reason; ⛔ nothing was added to the shrink-only escapable-literal ledger — the row discharged by construction. The gate's own docblock now carries both refusals so the next author does not reach for either.The declaration is a plain literal key rather than a factory call because
@objectstack/metadata-protocolwritessys_metadataand cannot import from@objectstack/objectql— objectql depends on it, so the edge would be a cycle. One spelling is writable and countable from anywhere in the tree; the type safety a factory would have bought is bought instead by the runtime refusal and by the gate's literal-only rule.The six declaring writers, each on a test a reader can check
sys-metadata-repository.tsorganizationId == null) — #6190 option Aaudit-writers.tsread-audit.tsauth-event-audit.tsadmin-import-users.tssys_userconfig-change-audit.tsglobal⭐
audit-writers.tsis deliberately conditional, not blanket. Case 2 of its own enumeration — the subject's organization column is present but its value is NULL — is indistinguishable at that call site from the missing-stamp defect, so declaring it would have handed the platform's largest write population back the blindness this card exists to remove.sys_audit_logledgerevidenceused to contradict that, and this round fixed the texts rather than the writer. Both said the reason covered "a record whose own column is NULL"; no writer declares that case. Both are RUNTIME strings that reach operators — a reason describing a population no writer produces reads as a claim the platform makes — so both now state case 1 only, and both say explicitly that case 2 keeps meeting the refusal. ⛔ The writer was not widened to match the prose.Zone 3 — the update path
The mirrored post-hook door does not need the declaration, and the reason is the one
system-write-organization.tsalready records: this resolver is INSERT-only by design, because a session write stamps the organization on insert (injectTenantOnInsert) and no write path stamps it on an update. With nothing resolving an organization on the update path there is no reader for a declaration there, and adding one would invent a second rule rather than follow the session write. The declaration also rides the options, not the row, so the update door never sees it either way.Verification
All at
f5d20ac01(a clean tree; the gate union and the tests below were run on that commit, after the last one).vitest run src/tenancy-orgless-write-declaration.test.ts src/tenancy-by-object-classification.test.ts— 2 files, 41 tests passed (was 39; +2 empty-batch pins). Refusals assert the ADR-0112 envelope (code+status), never a baretoThrow().if (orgLessDeclaration !== undefined) return undefined;→ 3 failed / 38 passed: exactly the three acceptance pins, while the refusal, ledger and both new empty-batch pins stayed green (correctly — none depends on that line). Mutation confirmed on disk before the run: blobc4e4e10b…→b1b4de52…, target-line occurrences 1 → 0, injected marker 0 → 1. Restore pinned toHEAD(never a baregit checkout --, which reads the index and hands the mutation back) and proven after: blob back toc4e4e10b…, marker count 0,git diff HEADfor the file empty. Nodisthop — the test imports./engine.jsinside its own package, so the mutation is read from source; the dependency closure was rebuilt beforehand either way.AssertionError: promise resolved "[]" instead of rejecting, 1 failed / 21 passed), then green after.typecheck— objectql, spec, plugin-audit, plugin-auth, service-settings, metadata-protocol: all Done.tsconfig.jsonexcludes**/*.test.ts, so that half is carried bycheck:test-typecheckagainsttsconfig.test.json— verified to actually compile the edited file (tsc --listFilesnamestenancy-orgless-write-declaration.test.ts, 1 occurrence), not assumed.admin-import-users+durability-swallow-repair48 passed.Failed to resolve entry for package "@objectstack/rest", i.e. an unbuilt dependency. They pass once the closure is built.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no path arguments — 113 commands over 20 changed paths, identical before and after the merge, and the deriver reports the tree is no longer stale for what it derives from.check:pm-dispatch-gates(1288 cases pass, 0 failed) andcheck:orgless-write-declarations(+ its--self-test). Also:error-code-casing,dispatcher-error-vocabulary,nul-bytes,watch-hint-literal,declared-population-live,parse-guard,role-word,page-declaration-shape,ratchet-remedy-authority,comment-mask-adoption,comment-mask-corpus,engine-double-contract,test-source-alias,cross-package-test-inputs,changeset-gate-self-tests,durability-log-level,swallow-census-controls,pnpm-filter-targets,published-files,required-contexts,merge-driver,pm-half-states,bare-root-worklist --self-test,self-test-wired,self-test-workflow-commands,system-context-census,tenant-audit-census,closing-keyword-parity,ci-filter-parity,undeclared-dep-imports,aggregator-roster,changeset-no-major,empty-changeset,adr-0087-registration, speccheck:error-code-provenance, speccheck:generated(15 of 15 artifacts current on the merged tree).check-system-context-censuswas red again after this round's engine edit — the added lines shifted theengine.ts:lineanchors on the elevation-read page. Repaired with the gate's own--fix, twice (once before the merge, once on the merged tree):--fixREFUSED ZERO files on both runs, which is the gate's only signal that no elevation read site arrived or vanished — a pure re-anchor, not a population change.auth-plugin.ts:1380→:1405, is drift this branch did not cause; it arrived with the 15 commits merged in.eslint --no-inline-config --format jsonover the 14 changed source files: 14 files linted, 0 errors, 0 warnings, exit 0 — the count read from eslint's own JSON output, and 14 of 14 came back with no "file ignored" message, so none was excluded by config. The narrowing is sound because this repo's singleeslint.config.mjsnever enables type-aware linting for any file (noparserOptions.project, no typed@typescript-eslintrules — stated and measured with a positive control ateslint.config.mjs:327), so this diff cannot move the verdict on a file it does not contain. The repo-wide sweep is CI's run.check:nul-bytesgreen, plus an independent control-character scan over the whole branch diff: zero hits.node scripts/pm/check-governed-merges.mjs— the 20-path diff hits no governed surface (docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md); ordinary queue landing would apply, but see the unarmed notice at the top.check:type-check-debtandcheck-test-completenessneed the whole workspace built and a savedturbo run testlog respectively. Neither is a finding; CI measures both.Scope
packages/spec: one error-code ledger entry and its two generated rows, and nothing else — see the caveat above. No governed surface. Nocontent/docs/releases/edit. Changeset isminor— new exported symbols on published packages.system-write-organization.tssaid a deliberately org-less population is declared on the OBJECT and "never a per-write bypass flag". That sentence is about a whole-object population and still stands; it is annotated rather than deleted, with the argument for why this declaration is its opposite on all three counts (it narrows, it cannot travel, and misuse throws).🤖 Generated with Claude Code
https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68