Skip to content

feat(cli,driver-sql)!: a tenant table's organization-less row no longer reaches the __global__ counter, and a production server boot with no auth secret is refused (18.x step 7, part 7b-3) - #24845

Merged
objectstack-fleet[bot] merged 54 commits into
mainfrom
claude/issue-24583-7b3-global-counter
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 54 commits into
mainfrom
claude/issue-24583-7b3-global-counter

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #24583 — step 7, part 7b-3 (claim 6284514874, amended 6284744613). The card stays open until every step-7 part has landed.

Clause-②: yes (narrowing) — measured: two accept sets narrow (the driver's autonumber counter and os serve's production server boot), and four exports are added and one is removed.

#24681 (steps 5+6) has merged, and this branch merged main after it, at 112ea871 over main 307ce4c4. The diff against main is this part alone: 24 files, +637 / −88. check-governed-merges --branch puts 0 of the 24 paths on the register, and the size is under the human-merge line.

Why

Step 7 rests on the premise that the readers it retires are dead after certification. The first measurement on this part found one still live. A production os serve with the auth tier on and no OS_AUTH_SECRET skipped AuthPlugin with a warning and served anyway. With no AuthPlugin, sys_organization is never registered, so the engine answers no-organization-object, and the inline seed wrote rows with organization_id NULL and drew their autonumbers from the __global__ counter.

Measured on examples/app-showcase, on a fresh SQLite database, on the merged tree before this change:

  • the boot wrote its birth marker and answered health 200;
  • showcase_field_zoo then held 2 rows, both with NULL organization_id;
  • _objectstack_sequences held one row with tenant_id __global__.

The seat ruled option A (amendment 6284744613, under #3963's rule A1). Close the door at the boot, then retire the counter path where the organization object exists.

What changed

  1. os serve refuses a production server boot whose auth did not compose for a missing secret. packages/cli/src/commands/serve.ts.

    • Placement: beside A1's existing no-auth refusal, in the same prescriptive shape. It fires while the kernel is only composed: no plugin has started and the database is never opened.
    • The message names OS_AUTH_SECRET and --no-server.
    • Unchanged: --no-server (kernel only) still boots with the warning, --dev still uses its fallback secret, and the host-kernel reason (the cloud runtime) still only warns.
    • os start spawns serve, so it gets the same refusal.
  2. The counter refuses an organization-less row on a tenant table, wherever the driver registered the organization object. SqlDriver.resolveSequenceTenantId is the one rule both faces call; TursoDriver's remote face passes the table and field through.

    • The refusal is the new OrganizationlessCounterRefusal (packages/drivers/driver-sql/src/organizationless-counter-refusal.ts), with code ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED and status 500. That is the engine twin's code, registered for @objectstack/driver-sql in the error-code ledger.
    • It is resolved before the lazy sequences DDL and before any counter read, so nothing is minted and nothing is written.
    • __global__ stays the counter key for an object with no tenant column. It also stays for a composition with no organization object, i.e. a lean @objectstack/objectql/core embedding.
    • The composition fact is organizationObjectRegistered, set in computeAndRecordTenantField, the one funnel every registration door passes through.
  3. Texts:

    • objectql's refusal message and module docblock, with GLOBAL_TENANT no longer exported from @objectstack/objectql;
    • the app-plugin.ts seed-handoff docblock;
    • the UniqueScopeSchema TSDoc in field.zod.ts;
    • the lint rule texts for unique/legacy-organization-composite;
    • duplicates.ts's live-condition docblocks.

    Each now says that a NULL-organization row is residue an earlier release wrote, not something a new write produces.

  4. Pins:

    • New sql-driver-organizationless-counter-refusal.test.ts (driver-sql): refusal on create and bulkCreate, carried organization, no tenant column, lean embedding.
    • New serve-no-auth-secret-refusal.integration.test.ts (cli): the refused boot's database file never exists; the control boot with the secret creates it and reaches ready.
    • Rewritten to the new rule, not weakened:
      • objectql system-write-organization.test.ts and runtime system-write-tenancy-autonumber-split.integration.test.ts assert ADR-0131 D9 and organization_id = NULL, and assert that __global__ is absent. The runtime one also asserts it is the engine's refusal, not the driver's.
      • lint data-model-rules.unique-verdicts.test.ts.
      • runtime seed-tenancy-autonumber-split.integration.test.ts now lands its 17.x residue through the same driver call with the refusal's composition fact withheld (asA17xDriver), so the residue is the driver's own 17.x path.
    • Test rigs that boot a production server for an unrelated property now get a secret (E2E_AUTH_SECRET in test/helpers/serve-process.ts). serve-ready-degraded-boot.e2e.test.ts reaches its degraded banner kernel-only (--no-server).

What reads the orphaned __global__ counter rows a 17.x install left on tenant tables, after this change: the counter does not. os migrate duplicates still reports them in liveConditions, now documented as residue. The historic seed-tenancy backfill (kernel:ready, and the handoff on organization create) still merges them into the organization's counter. Its migration entry is not touched here.

Measured

On HEAD 112ea871 (after main was merged), with a fresh pnpm install and pnpm build (exit 0):

  • Premise, again.
    • Production os serve of app-showcase with no secret: refused, with "no auth secret is set" and OS_AUTH_SECRET named, and the database file never created.
    • The same boot with the secret: health 200. 65 tables carry organization_id, with 0 NULL rows, and the counter is keyed by the Default Organization.
    • examples/embed-objectql: 2/2 tests passed.
  • R1. pnpm dev -- --fresh on a random port: health 200, seeded-admin sign-in 200, 0 error lines.
  • R2 + R3. upgrade-18x-harness.dogfood.test.ts (over the ceremonied fixture) and organization-ownership-boot-refusal-order.dogfood.test.ts: 2 files, 12 passed.
  • R4. An isolated production os serve with the real @objectstack/organizations runtime composed (Organizations in the plugin list): health 200, 33 tables carry organization_id with 0 NULL rows, and 0 failure lines. The walled dogfood cases (the six wall-* files and the three *-walled* files): 9 files, 133 passed. Step 4's walled e2e cases (serve-organizations-host-resolution, serve-organizations-mount-failure) ran in the CLI nightly tier below, green.
  • Suites:
    • driver-sql: 224 files passed, 3683 tests passed;
    • driver-turso: 92 files, 2430 tests;
    • runtime: 431 files, 6782 tests;
    • cli unit: 313 files, 4635 tests;
    • objectql: 450 files, 8490 tests;
    • lint: 149 files, 6958 tests;
    • spec: 703 files, 21360 tests;
    • cli integration: 163 files passed, 1 skipped; 1411 tests passed, 9 skipped;
    • cli nightly tier (OS_TEST_TIERS=nightly, every *.e2e.test.ts): 83 files, 879 tests;
    • turbo run typecheck over the seven touched packages and their closure: 68 of 68 tasks.
  • Ablation, on b2fec9a9 (before the main merge; the ablated line is unchanged by it). node scripts/ablation-replace.mjs replaced the refusal line in resolveSequenceTenantId. The landing was proven on disk: anchor count 1 to 0, blob 0af77d32fad3 to 0828ff0cfadb. Then sql-driver-organizationless-counter-refusal.test.ts went 2 failed, 3 passed: both refusal cases red, the three __global__/carried cases still green. Restored: blob equals HEAD and git diff HEAD is empty.
  • Gates. node scripts/pm/dispatch-gates.mjs --commands on the final head derives 124 commands over the 24 paths. All 124 ran, each exit code captured to a file before any pipe. --ran reconciles 124 of 124.
    • 122 exited 0, among them check:adr-0087-registration, check-changeset-no-major, check:error-code-provenance, check:cli-test-child-env, check:type-check-debt and the spec check:generated (14 artifacts up to date).
    • check-engine-split-ratio --days 90 (exit 2) and check-plugin-teardown-shape --self-test (exit 3) refuse in this shallow clone: they need history older than the clone. NOT MEASURED here; this is checker health, not this diff.

Acceptance notes

  • Finding, batched by the seat into the Tier H ADR notes PR. ADR-0131 D9 reads "a write on a tenant-column object with no organization is derived under single and refused otherwise". Engine.resolveSystemInsertOrganization's no-organization-object exit still writes NULL. After this PR no served composition reaches that exit (os serve refuses it); a lean embedding still does, by design.
  • liveConditions in os migrate duplicates. The JSON shape is unchanged; only its meaning narrowed from forecast to residue. content/docs/deployment/cli.mdx still calls it "about to mint more duplicates". That page is outside this part's file surface, so the correction is left to the seat.
  • The spec .describe() on DeclaredIndexSchema.unique and the object.zod.ts comment describe the COALESCE DDL emitters, which are 7b-2's (held for a ruling). They are untouched.
  • Turso remote face. The refusal reaches it through the shared resolveSequenceTenantId. No new remote-face test was added; the driver-turso suite is green.
  • Files outside the claimed surface, each forced by the widened one:
    • scripts/check-cli-test-child-env.mjs: the new refusal pin joins its pinned census of files that spawn bin/run.js (13 to 14). The gate and its self-test pass.
    • packages/cli/test/helpers/serve-process.ts: gains E2E_AUTH_SECRET.
    • uncreatable-directory-doors.integration.test.ts and serve-ready-degraded-boot.e2e.test.ts: production server rigs whose property is not auth. The first now hands in the secret; the second boots --no-server. No assertion was weakened.
  • Not touched: docs/adr/**, content/docs/releases/, the COALESCE(…,'__global__') DDL emitters, the hash shadow and the index differ (7b-2), and historic migration entries.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H7Dv7HSDeT8d2fbDq3myx5


Generated by Claude Code

claude added 30 commits October 19, 2026 13:06
…r retired; webhook activation door

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…he 18.0 dual read; door pins for webhook

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…asserts no row and a dispatch

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…the checklist, the webhooks guide and the showcase; changeset

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…tcher's switch read

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…posture builds no wall, as its 18.0 seeded copy stamped with the Default Organization was

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…red writes; the declared-webhook double is drivable by the limit gate

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…does not implement

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…authors; checklist and admitsOrganization docblock say a declared webhook is deployment-wide under single

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…the fixture's customized declared-webhook mirror

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…n fail on a double delivery

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…0g-webhooks-registry

# Conflicts:
#	content/docs/permissions/tenant-audit-census.mdx
#	docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
…0g-webhooks-registry

# Conflicts:
#	content/docs/permissions/tenant-audit-census.mdx
#	docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
…-registry' into claude/issue-24581-ceremony-open
…he ceremony, then boot; boot-refusal-order step 2 back on the binary
…el — the cipher names no organization

Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V
Co-authored-by: Claude <noreply@anthropic.com>
…sys_flow_credential leave reach)

Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 7 package(s): @objectstack/cli, @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/lint, @objectstack/objectql, @objectstack/runtime, @objectstack/spec, touching 19 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/drivers/driver-sql/src/index.ts, packages/lint/src/data-model-rules.ts, packages/objectql/src/index.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

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

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/drivers/driver-sql/src/index.ts, packages/lint/src/data-model-rules.ts, packages/objectql/src/index.ts, …) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: os serve (command, 34 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 154 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 41646a5788f5362a66527a287200f9832fb00602 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 41646a5788f5362a66527a287200f9832fb00602

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 112ea8717bfd7bf4e1d1decdfac9d3a178298d1c
Local-runs: none
Scope: packages/spec/src/api/error-code-ledger.zod.ts, packages/spec/src/data/field.zod.ts + packages/cli/src/commands/serve.ts, packages/core/src/stack-auth.ts, packages/core/src/kernel.ts, packages/cli/src/commands/start.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-sql/src/organizationless-counter-refusal.ts, packages/drivers/driver-sql/src/index.ts, packages/drivers/driver-turso/src/turso-driver.ts, packages/objectql/src/index.ts, packages/objectql/src/core.ts, packages/objectql/src/tenancy/system-write-organization.ts, packages/objectql/src/plugin.ts, packages/spec/src/system/constants/system-names.ts, packages/spec/src/data/object.zod.ts, .changeset/24583-7b3-organizationless-counter-retired.md, scripts/check-cli-test-child-env.mjs, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, packages/cli/test/helpers/serve-process.ts, packages/cli/test/serve-no-auth-secret-refusal.integration.test.ts, packages/cli/test/serve-ready-degraded-boot.e2e.test.ts, packages/cli/test/uncreatable-directory-doors.integration.test.ts, packages/drivers/driver-sql/src/sql-driver-organizationless-counter-refusal.test.ts, packages/runtime/src/seed-tenancy-autonumber-split.integration.test.ts, packages/runtime/src/system-write-tenancy-autonumber-split.integration.test.ts, packages/objectql/src/system-write-organization.test.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/data-model-rules.ts, docs/adr/0131-total-organization-ownership-no-null-organization-id.md, docs/adr/0087-metadata-protocol-upgrade-contract.md

Merge-base 307ce4c4b42503347d7f85825f572e4ca058b05f; 24 files, +637/-88, as claimed. Read-only: the diff, the card comments 6284717698 / 6288405082, the PR body and the head's check-runs. Nothing built or run.

① Derived judgments

1. The serve refusal — holds.

  • Exactly the case. serve.ts:4682 refuses on flags.server && !authComposition.composes && authComposition.reason === 'no-secret'. resolvePlatformAuthComposition (stack-auth.ts:168-184) answers in order stack-supplies-auth, auth-tier-off, host-kernel, no-secret, so no-secret is reached only with the auth tier on, no stack-mounted AuthPlugin and not a host kernel. --no-server (server flag, allowNo: true, serve.ts:1443) drops flags.server; a dev boot composes through resolveAuthSecret({ isDev })'s fallback (stack-auth.ts:145-148, isDevelopmentBoot at 136); the host-kernel reason still only warns at serve.ts:4160. The A1 no-auth refusal at serve.ts:4660 is untouched and the new block sits directly beside it in the same shape.
  • Before any write. The throw is inside the composition phase: ObjectKernel.use() (kernel.ts:347-377) only loads and registers plugin metadata under the state !== 'idle' guard, no install or start runs; runtime.start() is at serve.ts:5402. The runtime state file is written only by writeRuntimeState on bound-port publication (serve.ts:1090-1111), after start. The pin's refused leg asserts the SQLite file was never created and the control leg that it was.
  • os start shares it by inheritance: it spawns bin/run.js serve (start.ts:524-536) and forces NODE_ENV=production when unset (start.ts:472). In practice start always hands the child AUTH_SECRET (start.ts:315-317, readOrCreateAuthSecret persisting one under the home dir, then start.ts:456), so a start child never reaches the refusal. True, but vacuous; noted, not a defect.
  • Prescriptive: the message names OS_AUTH_SECRET, --no-server and os dev / os serve --dev.
  • No mistaken refusal. resolveAuthSecret reads only OS_AUTH_SECRET and its two legacy spellings via readEnvWithDeprecation, after dotenv-flow has loaded the project's .env files (serve.ts:2360 precedes 4139), plus the dev fallback. There is no secret-file or other resolver in the tree (grep over core, cli, plugin-auth), and AuthPlugin itself reads the same environment (auth-manager.ts:4647). A deployment that would have composed auth before this PR still composes; the only boots now refused are the ones that previously skipped AuthPlugin with the warning — the exact door 6284717698 measured.
  • Forced and correct: serve-no-auth-secret-refusal.integration.test.ts spawns bin/run.js with NODE_ENV: undefined through childEnv, which is precisely the population check-cli-test-child-env.mjs pins (13 to 14), and its entry sits in sorted position (serve-mcp-stdio-early-initialize before it, serve-no-storage-boot-guard after, scripts/check-cli-test-child-env.mjs:2667-2672). E2E_AUTH_SECRET (serve-process.ts:803) is forced for uncreatable-directory-doors (a production server rig whose property is OS_HOME), and serve-ready-degraded-boot.e2e.test.ts moving to --no-server (:101) is the only remaining way to reach a production degraded boot; its assertions at :172-213 are unchanged.

2. The counter refusal — holds, with the per-driver edge named below.

  • The flag. organizationObjectRegistered (sql-driver.ts:5717) is set in computeAndRecordTenantField (:12169) on key === SystemObjectName.ORGANIZATION and never cleared. Every registration door funnels there: registerManagedObjectMetadata (:12342, keyed by StorageNameMapping.resolveTableName, which is the identity for sys_organization since the name carries no __ — system-names.ts:287-290), registerExternalObject (:12245, keyed by object name), the shard path (:12083, the base table), and Turso's remote initObjects and registerObjectMetadata (turso-driver.ts:4143-4165, :4221-4224) reach the same two. resolveSequenceTenantId (:7797-7807) orders no-tenant-column to GLOBAL_TENANT, carried organization to itself, registered to the throw, else GLOBAL_TENANT; it runs before ensureSequencesTable (:8165), and in resyncSequenceToDataMax (:8426) and autoNumberCounterKey (:8465); Turso's remote face at :2780 and :2883. One throw site, both faces.
  • Can a served composition see it false on a tenant table? Only for a DIRECT driver write (a) on a driver instance that never registers sys_organization — a secondary datasource, or a lean embedding — or (b) on the primary before sys_organization's registration reaches it. Engine writes never depend on the flag: resolveSystemInsertOrganization derives under single and refuses otherwise whenever the engine holds the object (ADR-0131 D9), and the ObjectQL plugin registers every object on its driver at start() (plugin.ts:1949-1953, :2015) before application seeds. No system object declares an autonumber (grep over platform-objects, plugin-auth, organizations), so no served plugin reaches (a) or (b). Acceptable as the declared edge (open question 2, option A); see residual R1 for the follow-up shape.
  • Code and status. ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED is listed under @objectstack/driver-sql (error-code-ledger.zod.ts:1467) beside the objectql row (:778); one code per emitting package is the ledger's own provenance convention (the INVALID_METADATA note at :764-768 says so). 500 matches the engine twin (status = 500, httpStatus = 500, system-write-organization.ts:335-337) and the caller class — server-side code that bypassed the engine, never a client's bad input — so an HTTP door answers the same number whichever layer caught it.

3. GLOBAL_TENANT un-exported — the removal is declared, the ADR-0087 marker is the wrong category.

  • Removed from the main entry only (objectql/src/index.ts:553); core.ts never exported it. No importer remains in the workspace: the four GLOBAL_TENANT importers read it from @objectstack/driver-sql or @objectstack/metadata-protocol; objectui has none.
  • The changeset declares the removal in its BREAKING body and gives a remedy. But it rides not-required (no-migration-prescription). ADR-0087's seventh category (Addendum 2026-10-12, D9, 0087:983-1095) exists for exactly this class — "a published runtime EXPORT retirement" — and closes with "The changeset itself is merged and is not re-graded: it stays on no-migration-prescription. New retirements use this category." The catch-all is admitted here only by the detector miss D9 names: the body's arrowless remedy line ("An import of GLOBAL_TENANT from @objectstack/objectql: import it from @objectstack/driver-sql … or spell '__global__'") is a FROM/TO rewrite the prescription detector cannot read. The marker's own text concedes the shape ("The one export removed … the remedy for it is stated below"). So "not-required (no-migration-prescription)" is not right for a removed export — B1.

4. Texts — hold. The field.zod.ts hunk is the UniqueScopeSchema TSDoc (:693-716), not a .describe(): no JSON-schema or docs artifact derives from it (the generated object.mdx tables carry DeclaredIndexSchema.unique's describe at object.zod.ts:566, untouched and still true of the COALESCE emitters; the retired phrase survives only in CHANGELOGs). In the touched source no false statement about __global__ remains: each site now says residue / 17.x / "wherever the organization object is registered". Over-broad or stale prose outside the surface is listed as residual R3.

5. Tests — hold. system-write-organization.test.ts:318-322 and system-write-tenancy-autonumber-split.integration.test.ts:198-204 turn toContain('__global__') into not.toContain plus positive ADR-0131 D9 / organization_id = NULL assertions, the runtime one also pinning the engine's own prefix; data-model-rules.unique-verdicts.test.ts swaps the phrase for the new one plus ADR-0131 D9; seed-tenancy-autonumber-split's asA17xDriver withholds the flag only after asserting it was set. Nothing weakened. The ablation claim is consistent with the code: removing the throw at sql-driver.ts:7805 reds exactly the two refusal cases (create, bulkCreate) and leaves the carried / no-tenant-column / lean-embedding cases green.

6. Semver and Clause-② — the line holds, one level does not. Clause-②: yes (narrowing) on the PR body and in the changeset; BREAKING banner; cli, driver-sql and objectql minor; lint and spec patch (texts and an additive ledger row). @objectstack/driver-turso is patch while the BREAKING bullet names its remote face's narrowing and its src/** moved — B2. check-changeset-no-major passes only through its "at least one moved package at minor+" quantifier, whose residual the gate itself prints ("it cannot catch a PR that … grades one minor and the other patch", check-changeset-no-major.mjs:964-969).

CI at review time. 25 success, 7 skipped, 0 failed; 8 still running: Test Core 1/6–6/6, Type Check · workspace, Type Check · consumer gates. Required-check status is therefore not yet final; nothing red.

② Semver level

minor with BREAKING under the launch-window convention (ADR-0087, amended 2026-09-13): correct for cli, driver-sql, objectql. driver-turso must be minor too (B2). The fixed group bumps as one, so the version outcome is unchanged; the declaration is what is wrong.

③ Boundary flags

B1 — ADR-0087 marker category (changeset). Replace the marker with the seventh category, naming the retired export: adr-0087: not-required (runtime-export-retired @objectstack/objectql#GLOBAL_TENANT) … with the why. Predicates as the gate checks them: 1 retired-from-entry (index.ts no longer exports it, core.ts never did, the merge base did); 2 no-metadata-surface (declared in packages/objectql/src/tenancy/system-write-organization.ts, not a *.zod.ts, not under spec/src/contracts/); 3 the inherited prescription refusal — the remedy line is prose with no arrow or framed table, and its FROM operand is the named export. Keep the "no key of any metadata schema is removed" sentence as the why; drop the claim that no-migration-prescription is the disposition.

B2 — @objectstack/driver-turso: patch in the same changeset. Grade it minor: its remote face refuses what it used to accept (turso-driver.ts:2780, :2883), its src/** moved, and the BREAKING bullet names it. One-line fix in the same file as B1.

Residuals (not blocking).

  • R1 — the composition fact is per driver instance and read at write time, so a direct driver write on a tenant table keys __global__ wherever that instance has not (yet) registered sys_organization: a secondary datasource, a lean embedding, or the primary before its registration. Engine writes are covered. Acceptable now (no served direct-driver autonumber writer exists); if one appears, open question 2's option B — the engine or the boot handing the fact to every driver as an option — is the robust shape, and the module header should say the fact is "registered on this instance", not "composed".
  • R2 — serve-no-auth-secret-refusal.integration.test.ts:66 unsets only OS_AUTH_SECRET; resolveAuthSecret also reads AUTH_SECRET and BETTER_AUTH_SECRET, and childEnv passes the parent environment through (it strips only vitest keys and NODE_PATH, serve-process.ts:858-889). A shell exporting a legacy spelling turns the refused leg red. Add both legacy names as undefined to the boot env.
  • R3 — prose outside the surface that still describes the 17.x write as current, for the seat (docs are off the release gate, ADR-0131 scope note 4): content/docs/deployment/cli.mdx:1801 and content/docs/deployment/seed-tenancy-repair.mdx:305 ("about to mint more duplicates"); content/docs/protocol/objectql/schema.mdx:609-611 ("every row on a single-organization deployment" carries no organization). Inside the surface, two nits: rule-explanations.ts:2687-2689 and data-model-rules.ts:595-597 say "since ADR-0131 D9 a new row carries its organization" without the "wherever the organization object is registered" hedge the TSDoc and the module header carry (false for the lean embedding this PR keeps by design); error-code-ledger.zod.ts:773-775 still says the fork "forks the autonumber counter" in the present tense — a why-comment, tolerable.
  • R4 — os start cannot reach the refusal because it always supplies a secret (start.ts:315-317, :456); the claim "os start shares it" is true by inheritance only. No change asked.
  • R5 — CI: the eight running check-runs above must reach success before merge.

Implemented-by: claude/issue-24583-7b3-global-counter
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: FAIL

…me-export-retired category and grades driver-turso minor; the refusal pin unsets the legacy secret spellings

Claude-Session: https://claude.ai/code/session_01H7Dv7HSDeT8d2fbDq3myx5
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Correction to the PR body, answering contract review 6288633042 (R4). The body says "os start spawns serve, so it gets the same refusal". That is wrong: os start always supplies a secret to the serve child it spawns, so it never reaches the refusal. The changeset now says so (commit 82c14dd).

The same commit also carries:

  • B1: the changeset's ADR-0087 disposition is now not-required (runtime-export-retired @objectstack/objectql#GLOBAL_TENANT), with the remedy as a FROM → TO line. check-adr-0087-registration verifies it.
  • B2: @objectstack/driver-turso is graded minor.
  • R2: the refusal pin also unsets the legacy AUTH_SECRET and BETTER_AUTH_SECRET.

main is merged again at ef624ed, bringing in 7a-1 and 7c.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review (re-review after FAIL at 112ea87)

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 82c14dd8f6cf6871d77b99091016678a8cab17ea
Local-runs: none
Scope: packages/spec/src/api/error-code-ledger.zod.ts, packages/spec/src/data/field.zod.ts + packages/cli/src/commands/serve.ts, packages/core/src/stack-auth.ts, packages/core/src/kernel.ts, packages/cli/src/commands/start.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-sql/src/organizationless-counter-refusal.ts, packages/drivers/driver-sql/src/index.ts, packages/drivers/driver-turso/src/turso-driver.ts, packages/objectql/src/index.ts, packages/objectql/src/core.ts, packages/objectql/src/tenancy/system-write-organization.ts, packages/objectql/src/plugin.ts, packages/spec/src/system/constants/system-names.ts, packages/spec/src/data/object.zod.ts, .changeset/24583-7b3-organizationless-counter-retired.md, scripts/check-cli-test-child-env.mjs, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs, packages/cli/test/helpers/serve-process.ts, packages/cli/test/serve-no-auth-secret-refusal.integration.test.ts, packages/cli/test/serve-ready-degraded-boot.e2e.test.ts, packages/cli/test/uncreatable-directory-doors.integration.test.ts, packages/drivers/driver-sql/src/sql-driver-organizationless-counter-refusal.test.ts, packages/runtime/src/seed-tenancy-autonumber-split.integration.test.ts, packages/runtime/src/system-write-tenancy-autonumber-split.integration.test.ts, packages/objectql/src/system-write-organization.test.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/data-model-rules.ts, docs/adr/0131-total-organization-ownership-no-null-organization-id.md, docs/adr/0087-metadata-protocol-upgrade-contract.md

Delta over the FAIL record at 112ea8717. Two commits on the branch since: ef624ed42 (the branch's own merge of origin/main, second parent 7c2eab929 = #24825, 7a-1) and 82c14dd8f (the fix). New merge-base 7c2eab929; origin/main is one commit further (41646a578, #24829, the console pin) and the PR reports mergeable. Read-only: both PR diffs, the ADR-0087 Addendum 2026-10-12 and D9, the gate check-adr-0087-registration.mjs (parseExportOperand, verifyRuntimeExportRetired, readFromBlock, metadataSurfaceReference), the 7a-1 commit, comment 6288818589, the head's check-runs. Nothing built or run.

The two-file claim — verified. git diff 307ce4c4b..112ea8717 and git diff 7c2eab929..82c14dd8f list the same 24 files (+637/−88 became +644/−88). Per file, ignoring @@ and index lines, only two differ: .changeset/24583-7b3-organizationless-counter-retired.md and packages/cli/test/serve-no-auth-secret-refusal.integration.test.ts. The other 22 hunks are byte-identical to the reviewed head, so every judgment of the FAIL record on them (items 1, 2, 4, 5) stands as written and is not re-derived here. One correction to the dev's comment and the seat's note: #24836 (7c) is still open; the merge brought 7a-1 only.

① Derived judgments

B1 — resolved. The marker now reads adr-0087: not-required (runtime-export-retired @objectstack/objectql#GLOBAL_TENANT) followed by the why, which keeps the "no key of any metadata schema is removed, renamed or re-shaped" sentence and no longer calls no-migration-prescription the disposition. Checked against the gate as it reads a marker:

  • the operand parses (parseExportOperand: a PACKAGE_NAME_RE package, #, a plain export name; the package, not a file, as D9 requires);
  • predicate 1, retired-from-entry: @objectstack/objectql is published (private unset), its exports["."].types is ./dist/index.d.ts, so the entry is src/index.ts; at the base 7c2eab929 it exports GLOBAL_TENANT (index.ts:553), at HEAD GLOBAL_TENANT is absent from every non-test file under packages/objectql/src, and the entry carries no export * at either rev, so absence is provable;
  • predicate 2, no-metadata-surface: the base declaration is packages/objectql/src/tenancy/system-write-organization.ts (metadataSurfaceKind null), a const, not a Zod projection; a word-match scan over every *.zod.ts, packages/spec/src/contracts/**, *.object.ts and **/objects/*.ts at the base finds no GLOBAL_TENANT (the homonyms in driver-sql, driver-turso and metadata-protocol live outside those surfaces, and GLOBAL_TENANT_KEY in generate.ts is a different word);
  • predicate 3, from-operands-named: the body's one rewrite is the arrowless list line "FROM GLOBAL_TENANT TO GLOBAL_TENANT imported from @objectstack/driver-sql, …". readFromBlock reads the code span between the two tokens as the FROM operand, canonicalFromOperand gives GLOBAL_TENANT, which is exactly the named operand; the line is a segment, so VERTICAL_FROM_RE does not record it as a vertical pair; nothing else in the body carries an arrow, a FROM/TO token outside a code span, or a framed table (the remedy bullets use an em dash, which ARROW does not match), so the residual detector pass has nothing to refuse — the same body minus this line already passed the detector at the old head.
    The remedy line is in the D9 shape (the worked example's - FROM … TO …). Empirically, the Check Changeset job, which runs check-adr-0087-registration.mjs --self-test and --base $MERGE_BASE, is green on the new head; that is consistent with the dev's reported verified: @objectstack/objectql#GLOBAL_TENANT (export, …).

B2 — resolved. '@objectstack/driver-turso': minor in the frontmatter; the other five levels are unchanged (cli, driver-sql, objectql minor; lint, spec patch).

R2 — handled. The pin's childEnv call now sets AUTH_SECRET: undefined and BETTER_AUTH_SECRET: undefined beside OS_AUTH_SECRET: undefined. childEnv copies process.env minus the vitest and resolution-base keys and spreads the overrides over it, and spawn drops an undefined-valued key, so all three spellings resolveAuthSecret reads are truly unset in both legs; an ambient legacy secret can no longer compose auth behind the refused leg.

R4 — handled. The changeset's first bullet is retitled to os serve alone and ends "os start never reaches the refusal: it always supplies a secret to the serve child it spawns" — the statement the FAIL record derived from start.ts:315-317, :456. Comment 6288818589 corrects the PR body's "os start spawns serve, so it gets the same refusal"; the body text itself still carries that sentence (comment-corrected, not edited). The CHANGELOG-bound text is right; non-blocking.

The merge — nothing changes a judgment. 7a-1 (7c2eab929, 19 files) and the PR intersect on four files (serve.ts, sql-driver.ts, turso-driver.ts, field.zod.ts); on each, the PR's hunks are byte-identical to the reviewed head, so the merge was clean textually. Semantically on sql-driver.ts: 7a-1 changes applyTenantScope (:15630, both orWhereNull arms gone, scope is equality or IN), readBackInsertedRows' scope options (:7651, the rows' tenant union scopes the read-back only when every row carries one) and a comment at :11481; 7b-3's organizationObjectRegistered (:5718, set at :12175) and resolveSequenceTenantId (:7803-7811, the throw at :7811, called at :8171, :8432, :8471) are untouched and not on 7a-1's paths. They coexist and agree: 7a-1's new comment says a composition that registers no organization object can still write organization-less rows and never reaches the scope with a tenant — the same lean-embedding exception 7b-3 keeps. The PR's own pins are not exposed to the retired arm: the counter pin's lean-embedding case creates with no tenantId, so applyTenantScope exits before scoping and the read-back finds the row; seed-tenancy-autonumber-split reads its residue through raw knex whereNull, not the chokepoint. 7a-1's turso-driver.ts hunk is a docblock at :2520, its serve.ts hunk adds six trustProxy lines at :4258, in the AuthPlugin options block, far from the refusal (reviewed at :4682, now six lines lower). No gate script in the PR's set was touched on main.

Nit (non-blocking, as instructed). The body cites "ADR-0131 D9, stage 8d" for the launch-window minor convention; the convention is ADR-0087's "Ratified: the pre-launch launch-window exemption", amended 2026-09-13 (#18003) for the level half (0087:472-500), and ADR-0131 contains no "stage 8d". No gate reads the citation: check-changeset-no-major.mjs reads the frontmatter, the BREAKING token and the Clause-② arm, never the ADR cite, and check-adr-0087-registration.mjs reads the marker and the rewrite shapes. "stage 8d" otherwise appears only in this PR's own source comments as the step's name.

CI at review time (10:36Z). 41 check-runs: 30 success, 7 skipped, 0 failed; 4 still running: Test Core (2/6), (3/6), (4/6), (6/6). Of the five required contexts, Build Core, Lint & Repo Gates and Governed Surface Queue Guard are green, TypeScript Type Check has all its jobs green (Type Check · workspace finished during this review), and Test Core waits on the four shards (1/6 and 5/6 are green). Nothing red.

② Semver level

minor with the BREAKING banner under the launch-window convention (ADR-0087, amended 2026-09-13), now declared for all four packages whose accept set narrows — cli, driver-sql, driver-turso, objectql — with lint and spec patch. Correct. Clause-②: yes (narrowing) on the body and in the changeset, unchanged.

③ Boundary flags

No blocking item remains. B1 and B2 are closed as above.

Residuals (not blocking).

  • R1 — unchanged from the FAIL record: the composition fact is per driver instance and read at write time; a direct driver write on a tenant table keys __global__ wherever that instance has not (yet) registered sys_organization. Option B (the engine or the boot handing the fact to every driver) remains the robust shape if a served direct-driver autonumber writer appears.
  • R3 — unchanged: the prose outside the surface (content/docs/deployment/cli.mdx:1801, seed-tenancy-repair.mdx:305, protocol/objectql/schema.mdx:609-611) and the two in-surface hedging nits (rule-explanations.ts:2687-2689, data-model-rules.ts:595-597) stand for the seat.
  • R4′ — the PR body still says os start gets the same refusal; the correction lives in comment 6288818589 and the changeset. An edit of the body would close it.
  • R5 — the four running Test Core shards must reach success before merge; Test Core is a required context.
  • R6 — the dev's comment and the seat's note say the merge brought 7c; refactor(plugin-webhooks,platform-objects)!: 18.x step 7c: retire the step 1–3 dual reads (declared-webhook mirror skip, sys_setting_audit global scope) #24836 is open and 7c2eab929 is the only step-7 part on the new base. A wording fix only.
  • R7 — the "ADR-0131 D9, stage 8d" citation above; a CHANGELOG-bound text, so worth correcting in a follow-up, but no gate reads it.

Implemented-by: claude/issue-24583-7b3-global-counter
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 10:39
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 20, 2026 10:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit df8bb40 Oct 20, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24583-7b3-global-counter branch October 20, 2026 11:25
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:data size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants