Repository navigation
feat(verify): the unique-scope derived proof family, runnable as os verify --unique-scopes (#15953 slice 3) - #23069
Conversation
deriveUniqueScopeCases / runUniqueScopeProofs / formatUniqueScopeReport: every field a stack declares `unique` collides as its declared scope says (ADR-0120 D1), within one organization through the REST and system doors, and across two organizations on a walled boot. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
…with its controls Within: a declaration the database cannot enforce (duplicates on disk when it arrives, ADR-0120 D4) is reported contradicted through both doors. Across: a per-organization field a declared global index overrides is reported contradicted. Attribution: an app's own dedupe refusal and a field-less refusal the twin meets too are never credited. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
…t-in) A fresh stack of its own; the cross-organization leg runs on the --multi-tenant boot and is reported not run without it. Its hard failures join the exit status, and --json carries the report as `uniqueScopes`. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
…changeset Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
A write path that normalizes (an upper-casing hook) changes what is held, so the probe offers the stored value instead of reporting the field not run. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
…-unique-scope-family
…sure gate runUniqueScopeProofs takes the same VerifyStack handle as runCrudVerification; formatUniqueScopeReport formats a report this package produced. Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6 Co-authored-by: Claude <noreply@anthropic.com>
…-unique-scope-family
📓 Docs Drift CheckThis PR changes 2 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 30 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 2e84e8ac290602763b4ebda2b61eee23df1e2bc1 && git checkout 2e84e8ac290602763b4ebda2b61eee23df1e2bc1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e448d3c27f4bd9031428347f6bdd865445486497 9c89d3cc89a967ef9eb61627c02855d831a158ab && git checkout -B drift-repro e448d3c27f4bd9031428347f6bdd865445486497 && git merge --no-ff 9c89d3cc89a967ef9eb61627c02855d831a158ab
node scripts/docs-audit/affected-docs.mjs --json e448d3c27f4bd9031428347f6bdd865445486497
|
Contract reviewServed-tier: Read at 2026-10-12T16:13Z, adversarially, against AGENTS.md Prime Directives #10 and #12 and Post-Task Checklist step 3. Inputs: card #15953 (body and all 15 comments: the classification table ① Derived judgmentsThe three functions and eleven types, reachable and coherent: right.
The expectation's source: right, and every excluded shape is excluded, not modelled. Attribution: right on every named path, with one residual the seat owes a carrier (see ③). The two Docs and changeset sentences against the diff: right, two of them narrower than the code. Changeset: the export list is the full set (3 functions, 11 types), the signature line is the real one, the refusal identities, the twin sentence, the exclusions and First-consumer evidence (hotcrm): consistent with the retired pins, as reported. Rows 6 and 7's D halves proven on every run that has the door ( ② Semver level
③ Boundary flagsDev report
Check-runs on the head, read four times in this act, the last just before posting (latest run per name). At the final read every run had concluded: 34 success, 7 skipped, 0 failure, 0 in progress. Concluded success: Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #15953
Clause-②: yes
What this is
Slice 3 of #15953: the D halves of the classification table's rows 6-7 (comment 6115219467). It adds the unique-scope derived proof family to
@objectstack/verify, runs it fromos verify --unique-scopes(opt-in), and documents it. Rows 6-7's K residues stay in hotcrm, and their R residues (driver-sql DDL, ADR-0120 D3) are not in this PR. #15953 stays open for its remaining rows.Property, for any app. Every field declared
uniquecollides as its declared scope says (UniqueScopeSchema, ADR-0120 D1 and D3):single-posture boot that organization is the install's only one: the untenanted install, with no wall, where every write lands.true/'organization'value is admitted. A'global'value, or any value of an object with no organization column (tenancy.enabled: false, which degrades the scope to the column alone), is refused.The family (
packages/verify/src/unique-scope.ts)unique, after the spec's ownFieldSchema.safeParse, is declared (isUniqueDeclared). This is the value a built artifact carries, so anautonumberthat authors nouniquestates'organization'. The family never re-derives that default by hand.indexes[]entry is NOT in the population. It is a different surface, with its own meaning of baretrue(refused there since protocol 18), and the field-level-versus-indexes[]spelling is already an author-time rule (R10).FieldSchema.uniqueandisUniqueDeclared/isGlobalUnique.UniqueScopeSchemadocblock and ADR-0120 D1 / D3 / S11.isTenancyDisabled(ADR-0066).postureEnforcesWallover the stack'stenancy()posture.RUNTIME_OWNED_FIELD_TYPESplusreadonly/system.DuplicateRecordError's documented split,DUPLICATE_RECORDin process andUNIQUE_VIOLATIONon the wire.POST /data/OBJECT, andPATCH /data/OBJECT/IDfor the update probe.hooks.runwith{ system: true }.within × insertandwithin × update; on a walled boot alsoacross × insert.withinruns on every boot.acrossruns only on a walled boot (os verify --multi-tenant,bootStack({ multiTenant: true })). There the family provisions a second organization the way a user makes one: a sign-up that creates its own (POST /auth/organization/create).organization_id), which is how a system writer must address one under a wall: an org-less system write is refused there withERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED(measured).singleboot,acrossis reported not run onreport.boot, with the reason, never silently skipped.true/'organization'constraint is the COALESCE expression index, and SQLite reports it by index name, so the envelope carries nofield(uniqueViolationColumn's contract). A'global'index, or an object with no organization column, names the field.unattributed.readonly,system, a runtime-issuedautonumber) is probed through the system door only, which keeps an explicit value (measured: REST answersdroppedFields: readonlyfor an autonumber; the system door stores it).formula,summary) isexcluded.not-derivable: a closed or tiny value space (boolean,select, …), a multi-valued reference, a target the app does not declare, an object the CRUD derivation cannot synthesize.tenancy.tenantFieldhas its across leg named, not run.deriveCrudCases/fillRelationalRefs. The family-localfixtureBodyfollows slice 2's cut-down shape (required fields, dates spaced, unique values distinct); slice 2's copy is module-private indelete-behavior.ts, outside this claim. Fixtures go through the probe's own door and principal, with per-writer base records. Run on a fresh stack of its own.os verify --unique-scopes:--multi-tenant, the walled one);uniqueScopes.summary.hardFailures(contradicted plus failed) to the exit status;uniqueScopesunder--json.Without the flag, nothing changes.
Evidence
All readings at this PR's head unless stated otherwise. The fixture and hotcrm runs used
packages/verify/distbuilt from the merged tree5fae9e5a13; the only later commit is the twoNOT_A_STAND_INrows.Fixture app (
packages/verify/src/unique-scope.test.ts,Tests 12 passed (12)):boolean).acrossreported not run.multiTenant: 'posture-only', real organizations and per-caller stamping, see below): 8 proven over 39 writes, all held. Across, per-organization values are admitted, and a'global'value or one with no organization column is refused.Negative controls, one per leg, no engine change:
contradictedon 4 of 4 writes (REST and system, insert and update), hardFailures 1.unique: truebeside a declaredindexes: [{ fields: ['name'], unique: 'global' }], the R10 double declaration.contradictedon 2 of 2 across writes, with within held.Attribution control:
DUPLICATE_VALUE, the hotcrm shape) isunattributedon 4 of 4 writes.codeon every record) isunattributedwithtwin: refused.Ablation of the family's own comparison (
judge), throughscripts/ablation-replace.mjs(wrap mode) inside a scratch script withtrap restore EXIT INT TERMon absolute paths, ated93727ce2. No build is involved: the test imports the module by a relative path.if (probe.expected === 'admitted') return { outcome: 'held' };was replaced by an unconditionalheldplus the markerABLATION_15953_S3_A.fee9f33ftoe477ff09, marker 1 during the run.Tests 1 failed | 11 passed (12), exactly the within negative control.fee9f33fequals HEAD andgit diff HEADis empty.if (probe.expected === 'refused') …(markerABLATION_15953_S3_B).fee9f33fto7b2dcfad, marker 1 during the run.Tests 1 failed | 11 passed (12), exactly the across negative control.hotcrm, the first consumer. Read-only clone at
1d7148bf, fetched first.mainis at0f84c9c7, andgit diff 1d7148bf origin/main -- srcadds or removes nounique/autonumberline (the same grep hits those files at HEAD). The clone'sgit statusstayed empty throughout.os verifyitself cannot boot hotcrm on this tree (protocol^17.7.0versus 18), so the family ran directly, as in slices 1-2.os migrate meta --from 17 --out, with onlyengines.protocolmoved to^18.crm_account.name,crm_contact.email,crm_product.sku;'organization'by the parse default, pluscrm_case.case_number, which declarestrueby hand.bootStackover its own artifact, REST door only (17.7.0 has no system door)@objectstack/organizations)multiTenant: true, the open@objectstack/organizations,OS_AUTH_MEMBERSHIP_POLICY=invite-only)Against what the retired pins asserted (D halves only):
test/account-name-tenant-scope.test.ts,crm_account.name: PROVEN on every run.test/case-number-tenant-scope.test.ts,crm_case.case_number: PROVEN on runs B and C.Extra evidence, not a table row:
test/contact-email-tenant-scope.test.ts. This file's subject is hotcrm'scontact_integrityhook (an app dedupe, a K property), with the(organization, email)index behind it.unattributed, because the hook answers its ownDUPLICATE_VALUE(the attribution control holding on a real app). On the single boot the system insert reaches the index and is credited: the hook scopes a system write by the row's own organization stamp, and that write carries none. Across, the value is admitted through both doors ("lets ANOTHER organization know the same person").Inconclusive on hotcrm, with the app's own reasons:
crm_campaign_member.member_numberandcrm_event_attendee.attendee_number: a generic fixture names no lead or contact party.crm_contract.contract_number:contract_validationties the term to the dates (hooks live only).crm_contact.email: as above.Verification
At head
9c89d3cc89: this branch withorigin/main0cbfb7c32cmerged, so 9 files and 1637 changed lines (+1630 / -7) against merge base0cbfb7c32. Readings below are at that head unless another commit is named.Suites.
@objectstack/verify:pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2readsTest Files 34 passed (34)/Tests 270 passed (270), lock verdictcommand-exit 0. The family file alone readsTests 12 passed (12).pnpm --filter @objectstack/verify typecheckandpnpm --filter @objectstack/cli typecheckexit 0, andcheck:test-typecheck: OKholds for both (at5fae9e5a13; no file of either program changed since).tsc --listFilesOnly -p tsconfig.test.jsonshowsunique-scope.test.tsin verify's program (2 hits, with its module) andverify-unique-scopes.test.tsin the CLI's (1 hit; controlverify-delete-behaviors.test.ts, 1 hit).pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2readsTest Files 283 passed (283)/Tests 4270 passed (4270)(at5fae9e5a13; the second merge changed nopackages/clifile).pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2over the sixverify-*files, this PR's new one included, readsTest Files 6 passed (6)/Tests 18 passed (18). The rest of the integration tier is declared to CI.repoproject:pnpm --filter @objectstack/core exec vitest run --project repo --maxWorkers=2readsTest Files 6 passed (6)/Tests 83 passed (83). The#22661second-object read pin is among them, green with noCALLERSrow.pnpm check:verify-stand-in:2 problem(s)(runUniqueScopeProofsandformatUniqueScopeReportunclassified).OK check-verify-stand-in-erasure: 2 stand-in check(s) guarded, 13 candidate(s) explicitly exempt, 10 call site(s) reached, 0 asserted driver arguments.--self-testOK.Gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, gives 115 commands (64pnpm, 51 directnode).--ranreadsRun reconciliation — 115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN.STALE TREE, becauseorigin/mainmoved 4 commits during the 40-minute run (toe448d3c27f). The stale family files are.github/workflows/lint.yml,scripts/check-system-context-census.mjsandscripts/sdui-manifest.record.json, none on this PR's paths.5e776ba287recorded 3PREREQUISITE NOT MET(exit 3:check:dual-build-cjs-loads, speccheck:skill-examples,check:i18n-coverage, each needing packages outside the built closures). After a fullturbo run build --filter=!@objectstack/docs(73 tasks), this battery measured all three, exit 0.@objectstack/verifyor a platform regression test in dogfood, never an app test (epic hotcrm#1579, step 5c) #15953.Lint, by proven narrowing at
9c89d3cc89:eslint --no-inline-config --format jsonover the 6 touched source files.--print-configresolves for 6 of 6.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules, its own comment at:327-328), so this diff cannot move any untouched file's verdict.The two
.md/.mdxfiles are outside eslint's population.Acceptance notes
Noted, not filed (none is a reproducible defect through a public door, a contract violation or an authoring trap; carrier: none):
bootStackover an UNPARSED object literal registers the raw field. Anautonumberthat authors nouniquetherefore gets no index there, and a system-door duplicate is admitted (measured: raw literal 2 of 2 admitted; the same object throughdefineStackstores'organization'and refuses the second).os verify/os validate/os buildload isdefineStack-built (STACK_PROVENANCE_MISSINGotherwise), so no public door reaches it.defineStackoutput for that reason.singleinstall derives its one organization for a system write, and REST stamps the caller's. So "untenanted install" is the single-posture boot, plus objects with no organization column. The NULL-row DDL fact stays row 7's R residue (driver-sql).os verify --multi-tenantwith the open@objectstack/organizationsrefuses to boot until a membership policy is declared (WalledMembershipPolicyError;OS_AUTH_MEMBERSHIP_POLICY). This is by that package's design, and it applies to--rls --multi-tenantalike. The README states it beside the flag.packages/cli/test/verify-unique-scopes.test.tsis a door that faults on a duplicate (exit 1), not a contradiction. Throughos verify, every config isdefineStack-built on a fresh database, so no metadata makes a same-organization duplicate land. The across leg needs--multi-tenant, whose runtime package no framework package may declare, so CI's test graph cannot boot it. The two contradiction controls are inpackages/verify.multiTenant: 'posture-only': a walled posture with real organizations (/auth/organization/create) and per-caller stamping, measured. The family's subject is which rows collide, which the index decides from each row's organization; read isolation is not under test. The real-package walled boot is exercised by the hotcrm runs above.Deviations
field(SQLite names the expression index), so "proved only on that code naming the probed field" would prove no per-organization field at all. Attribution therefore also accepts a field-less unique refusal when the twin is admitted, and pins it with the attribution control above.NOT_A_STAND_INsurface was used (2 rows), becausecheck:verify-stand-inasked. The#22661pin needed noCALLERSrow: the family reads a reference's target from the CRUD derivation and never callsreferenceTargetOf(therepoproject is green without one).origin/main(df7a137923, 4 commits, none on this surface) was merged before the PR, per AGENTS.md section 10. The build was refreshed, and every suite and gate here was run on the merged tree.Generated by Claude Code