Repository navigation
Commit f93bf12
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 from `os 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 `unique` collides as its
declared scope says (`UniqueScopeSchema`, ADR-0120 D1 and D3):
- **Within one organization**, a second holder of the value is refused,
whatever the scope. On a `single`-posture boot that organization is the
install's only one: the untenanted install, with no wall, where every
write lands.
- **Across two organizations**, a `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`)
- **Population.** Every field of every declared object whose `unique`,
after the spec's own `FieldSchema.safeParse`, is declared
(`isUniqueDeclared`). This is the value a built artifact carries, so an
`autonumber` that authors no `unique` states `'organization'`. The
family never re-derives that default by hand.
- A unique `indexes[]` entry is NOT in the population. It is a different
surface, with its own meaning of bare `true` (refused there since
protocol 18), and the field-level-versus-`indexes[]` spelling is already
an author-time rule (R10).
- **Published sources each expectation is read from.**
- The scope: `FieldSchema.unique` and `isUniqueDeclared` /
`isGlobalUnique`.
- Its meaning: the `UniqueScopeSchema` docblock and ADR-0120 D1 / D3 /
S11.
- The organization column: `isTenancyDisabled` (ADR-0066).
- A wall: `postureEnforcesWall` over the stack's `tenancy()` posture.
- What the caller cannot set: `RUNTIME_OWNED_FIELD_TYPES` plus
`readonly` / `system`.
- The refusal's identity: `DuplicateRecordError`'s documented split,
`DUPLICATE_RECORD` in process and `UNIQUE_VIOLATION` on the wire.
- **Doors.** Each probe writes through one of two doors:
- **REST** as a signed-in caller: `POST /data/OBJECT`, and `PATCH
/data/OBJECT/ID` for the update probe.
- **The engine's own write as the system principal**: `hooks.run` with
`{ system: true }`.
- **Probes.** Per door, `within × insert` and `within × update`; on a
walled boot also `across × insert`.
- A first holder is written and read back.
- A second record is offered the value **as the first one stores it**,
so a normalizing hook (hotcrm upper-cases SKUs) does not hide the
collision.
- **Boots and legs.**
- `within` runs on every boot.
- `across` runs 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`).
- The admin is the first organization's caller when the boot binds it to
one; otherwise a second provisioned sign-up is.
- The system door names the organization on the row (`organization_id`),
which is how a system writer must address one under a wall: an org-less
system write is refused there with
`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (measured).
- On a `single` boot, `across` is reported **not run** on `report.boot`,
with the reason, never silently skipped.
- **Attribution.** A refusal proves a collision only when two things
hold:
- it is the platform's unique refusal at that door, naming the probed
object;
- it is this field's: it names the field, or it names none and a
**twin** (the same record with only the probed value changed) is
admitted.
- The twin exists because of a measured fact. A `true` /
`'organization'` constraint is the COALESCE expression index, and SQLite
reports it by index name, so the envelope carries no `field`
(`uniqueViolationColumn`'s contract). A `'global'` index, or an object
with no organization column, names the field.
- An update needs no twin: it changes nothing else.
- Any other refusal (an app's own dedupe, a rule, a permission, a
constraint on another field) is `unattributed`.
- **Exclusions, with reasons, counted.**
- A field the caller cannot set (`readonly`, `system`, a runtime-issued
`autonumber`) is probed through the **system door only**, which keeps an
explicit value (measured: REST answers `droppedFields: readonly` for an
autonumber; the system door stores it).
- A derived value (`formula`, `summary`) is `excluded`.
- These are `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.
- A one-to-one reference is probed within; its across leg is named, not
run, because the value is one organization's record.
- An object declaring its own `tenancy.tenantField` has its across leg
named, not run.
- **Fixtures.** Reused `deriveCrudCases` / `fillRelationalRefs`. The
family-local `fixtureBody` follows slice 2's cut-down shape (required
fields, dates spaced, unique values distinct); slice 2's copy is
module-private in `delete-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`:
- boots its own fresh stack (with `--multi-tenant`, the walled one);
- adds `uniqueScopes.summary.hardFailures` (contradicted plus failed) to
the exit status;
- emits `uniqueScopes` under `--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/dist` built from the merged tree
`5fae9e5a13`; the only later commit is the two `NOT_A_STAND_IN` rows.
**Fixture app** (`packages/verify/src/unique-scope.test.ts`, `Tests 12
passed (12)`):
- **Single boot:** 9 fields.
- Probed: 6 per organization, 2 global.
- 8 proven over 28 writes, all held.
- 1 not derivable (a unique `boolean`).
- `across` reported not run.
- **Walled boot** (`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:
- **within**: a field declared unique on a table that already holds a
duplicate when the declaration arrives.
- driver-sql cannot create the index and logs that the constraint "is
NOT enforced until the data is deduplicated" (ADR-0120 D4).
- Every second holder is admitted, and the family reports `contradicted`
on 4 of 4 writes (REST and system, insert and update), hardFailures 1.
- **across**: a field declared `unique: true` beside a declared
`indexes: [{ fields: ['name'], unique: 'global' }]`, the R10 double
declaration.
- The second organization is refused naming the field.
- The family reports `contradicted` on 2 of 2 across writes, with within
held.
**Attribution control:**
- An app hook's own dedupe (`DUPLICATE_VALUE`, the hotcrm shape) is
`unattributed` on 4 of 4 writes.
- A field-less unique refusal that the twin meets too (a hook stamps one
`code` on every record) is `unattributed` with `twin: refused`.
- Neither is ever credited.
**Ablation of the family's own comparison** (`judge`), through
`scripts/ablation-replace.mjs` (wrap mode) inside a scratch script with
`trap restore EXIT INT TERM` on absolute paths, at `ed93727ce2`. No
build is involved: the test imports the module by a relative path.
- **A.** `if (probe.expected === 'admitted') return { outcome: 'held'
};` was replaced by an unconditional `held` plus the marker
`ABLATION_15953_S3_A`.
- On disk: anchor 1 to 0, marker 0 to 1, blob `fee9f33f` to `e477ff09`,
marker 1 during the run.
- RED: `Tests 1 failed | 11 passed (12)`, exactly the within negative
control.
- Restored: blob `fee9f33f` equals HEAD and `git diff HEAD` is empty.
- **B.** The same on `if (probe.expected === 'refused') …` (marker
`ABLATION_15953_S3_B`).
- On disk: anchor 1 to 0, blob `fee9f33f` to `7b2dcfad`, marker 1 during
the run.
- RED: `Tests 1 failed | 11 passed (12)`, exactly the across negative
control.
- Restored to HEAD's blob, empty diff.
- Predicted direction for both: red. Observed: red.
**hotcrm, the first consumer.** Read-only clone at `1d7148bf`, fetched
first. `main` is at `0f84c9c7`, and `git diff 1d7148bf origin/main --
src` adds or removes no `unique` / `autonumber` line (the same grep hits
those files at HEAD). The clone's `git status` stayed empty throughout.
`os verify` itself cannot boot hotcrm on this tree (protocol `^17.7.0`
versus 18), so the family ran directly, as in slices 1-2.
- **Artifact.** Built by hotcrm's own CLI 17.7.0 into the scratchpad.
- **Snapshot.** This tree's `os migrate meta --from 17 --out`, with only
`engines.protocol` moved to `^18`.
- **Population.** 13 fields, all per-organization:
- `crm_account.name`, `crm_contact.email`, `crm_product.sku`;
- 10 autonumbers, nine of them `'organization'` by the parse default,
plus `crm_case.case_number`, which declares `true` by hand.
| run | engine / boot | legs | proven | contradicted | inconclusive |
writes held / total |
|---|---|---|--:|--:|--:|--:|
| A | hotcrm's pinned 17.7.0 `bootStack` over its own artifact, REST
door only (17.7.0 has no system door) | within (across not run: hotcrm
declares no `@objectstack/organizations`) | 2 | 0 | 11 | 4 / 6 |
| B | this tree, snapshot plus the artifact's 44 hook bodies grafted by
name, single | within | 9 | 0 | 4 | 23 / 32 |
| B | the same, walled (`multiTenant: true`, the open
`@objectstack/organizations`, `OS_AUTH_MEMBERSHIP_POLICY=invite-only`) |
within + across | 9 | 0 | 4 | 35 / 48 |
| C | this tree, snapshot with hooks inert, single | within | 11 | 0 | 2
| 28 / 32 |
| C | the same, walled | within + across | 11 | 0 | 2 | 42 / 48 |
**Against what the retired pins asserted (D halves only):**
- **Row 6, `test/account-name-tenant-scope.test.ts`, `crm_account.name`:
PROVEN on every run.**
- "Still rejects a duplicate account name WITHIN one organization":
refused through REST and system, insert and update, on the walled boot
and on the single boot.
- "On an UNTENANTED install too": the single boot.
- "Accepts the same account name in two different organizations":
admitted through REST and system on the walled boot.
- Run A shows the within half holding on hotcrm's own pinned engine.
- **Row 7, `test/case-number-tenant-scope.test.ts`,
`crm_case.case_number`: PROVEN on runs B and C.**
- Within: refused through the system door, insert and update, on both
boots.
- Across: admitted in a second organization on the walled boot.
- It is an autonumber, so REST is excluded (that door drops the value),
and Run A, which is REST only, names it inconclusive with that reason.
- 0 contradictions with what either file asserted.
**Extra evidence, not a table row:
`test/contact-email-tenant-scope.test.ts`.** This file's subject is
hotcrm's `contact_integrity` hook (an app dedupe, a K property), with
the `(organization, email)` index behind it.
- Hooks live: within is `unattributed`, because the hook answers its own
`DUPLICATE_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").
- Hooks inert: proven.
**Inconclusive on hotcrm, with the app's own reasons:**
- `crm_campaign_member.member_number` and
`crm_event_attendee.attendee_number`: a generic fixture names no lead or
contact party.
- `crm_contract.contract_number`: `contract_validation` ties the term to
the dates (hooks live only).
- `crm_contact.email`: as above.
## Verification
At head `9c89d3cc89`: this branch with `origin/main` `0cbfb7c32c`
merged, so 9 files and 1637 changed lines (`+1630 / -7`) against merge
base `0cbfb7c32`. Readings below are at that head unless another commit
is named.
**Suites.**
- **`@objectstack/verify`:** `pnpm --filter @objectstack/verify exec
vitest run --maxWorkers=2` reads `Test Files 34 passed (34)` / `Tests
270 passed (270)`, lock verdict `command-exit 0`. The family file alone
reads `Tests 12 passed (12)`.
- **Typechecks:** `pnpm --filter @objectstack/verify typecheck` and
`pnpm --filter @objectstack/cli typecheck` exit 0, and
`check:test-typecheck: OK` holds for both (at `5fae9e5a13`; no file of
either program changed since). `tsc --listFilesOnly -p
tsconfig.test.json` shows `unique-scope.test.ts` in verify's program (2
hits, with its module) and `verify-unique-scopes.test.ts` in the CLI's
(1 hit; control `verify-delete-behaviors.test.ts`, 1 hit).
- **CLI unit tier:** `pnpm --filter @objectstack/cli exec vitest run
--project unit --maxWorkers=2` reads `Test Files 283 passed (283)` /
`Tests 4270 passed (4270)` (at `5fae9e5a13`; the second merge changed no
`packages/cli` file).
- **CLI integration tier:** `pnpm --filter @objectstack/cli exec vitest
run --project integration --maxWorkers=2` over the six `verify-*` files,
this PR's new one included, reads `Test Files 6 passed (6)` / `Tests 18
passed (18)`. The rest of the integration tier is declared to CI.
- **Core `repo` project:** `pnpm --filter @objectstack/core exec vitest
run --project repo --maxWorkers=2` reads `Test Files 6 passed (6)` /
`Tests 83 passed (83)`. The `#22661` second-object read pin is among
them, green with no `CALLERS` row.
- **`pnpm check:verify-stand-in`:**
- Before the rows: exit 1, `2 problem(s)` (`runUniqueScopeProofs` and
`formatUniqueScopeReport` unclassified).
- After: exit 0, `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-test` OK.
**Gates.**
- **Derivation:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, with no paths, gives 115 commands (64
`pnpm`, 51 direct `node`).
- **Results:** every one exits 0, with its exit code captured before any
pipe. `--ran` reads `Run reconciliation — 115 derived, 115 run, 0
NOT-MEASURED, 0 UNRUN.`
- The derivation was current when the run began.
- The reconcile pass printed `STALE TREE`, because `origin/main` moved 4
commits during the 40-minute run (to `e448d3c27f`). The stale family
files are `.github/workflows/lint.yml`,
`scripts/check-system-context-census.mjs` and
`scripts/sdui-manifest.record.json`, none on this PR's paths.
- An earlier battery at `5e776ba287` recorded 3 `PREREQUISITE NOT MET`
(exit 3: `check:dual-build-cjs-loads`, spec `check:skill-examples`,
`check:i18n-coverage`, each needing packages outside the built
closures). After a full `turbo run build --filter=!@objectstack/docs`
(73 tasks), this battery measured all three, exit 0.
- Every command and its printed verdict line is in the report comment on
#15953.
**Lint, by proven narrowing** at `9c89d3cc89`: `eslint
--no-inline-config --format json` over the 6 touched source files.
1. **Population, read from eslint's own config:** `--print-config`
resolves for 6 of 6.
2. **Counts, read from the JSON:** 6 files, 0 errors, 0 warnings.
3. **Invariance:** `eslint.config.mjs` never enables type-aware linting
(no `parserOptions.project`, no typed `@typescript-eslint` rules, its
own comment at `:327-328`), so this diff cannot move any untouched
file's verdict.
The two `.md` / `.mdx` files 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):
- `bootStack` over an UNPARSED object literal registers the raw field.
An `autonumber` that authors no `unique` therefore gets no index there,
and a system-door duplicate is admitted (measured: raw literal 2 of 2
admitted; the same object through `defineStack` stores `'organization'`
and refuses the second).
- Every config `os verify` / `os validate` / `os build` load is
`defineStack`-built (`STACK_PROVENANCE_MISSING` otherwise), so no public
door reaches it.
- The family reads the parse's scope and would report such a
programmatic boot contradicted.
- This PR's positive fixture boots `defineStack` output for that reason.
- The ADR-0120 D3 NULL-organization bucket is unreachable through either
door on today's runtime: a `single` install 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-tenant` with the open `@objectstack/organizations`
refuses 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-tenant` alike.
The README states it beside the flag.
- The CLI-level control in
`packages/cli/test/verify-unique-scopes.test.ts` is a door that faults
on a duplicate (exit 1), not a contradiction. Through `os verify`, every
config is `defineStack`-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 in `packages/verify`.
- The walled-boot unit suites use `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
- PM mechanism assumption 2 was measured and partly falsified. The
per-organization refusal carries no `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.
- The conditional `NOT_A_STAND_IN` surface was used (2 rows), because
`check:verify-stand-in` asked. The `#22661` pin needed no `CALLERS` row:
the family reads a reference's target from the CRUD derivation and never
calls `referenceTargetOf` (the `repo` project 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.
- Commits carry the model-free trailer pair per AGENTS.md, and this body
ends with its session-URL footer rather than the harness's own
attribution form.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0a29bec commit f93bf12
9 files changed
Lines changed: 1630 additions & 7 deletions
File tree
- .changeset
- content/docs/deployment
- packages
- cli
- src/commands
- test
- verify
- src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1924 | 1924 | | |
1925 | 1925 | | |
1926 | 1926 | | |
| 1927 | + | |
1927 | 1928 | | |
1928 | 1929 | | |
1929 | 1930 | | |
| |||
1966 | 1967 | | |
1967 | 1968 | | |
1968 | 1969 | | |
| 1970 | + | |
| 1971 | + | |
| 1972 | + | |
| 1973 | + | |
| 1974 | + | |
| 1975 | + | |
| 1976 | + | |
| 1977 | + | |
| 1978 | + | |
| 1979 | + | |
| 1980 | + | |
| 1981 | + | |
| 1982 | + | |
| 1983 | + | |
| 1984 | + | |
| 1985 | + | |
| 1986 | + | |
| 1987 | + | |
1969 | 1988 | | |
1970 | 1989 | | |
1971 | 1990 | | |
| |||
1994 | 2013 | | |
1995 | 2014 | | |
1996 | 2015 | | |
1997 | | - | |
| 2016 | + | |
| 2017 | + | |
| 2018 | + | |
1998 | 2019 | | |
1999 | 2020 | | |
2000 | 2021 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
22 | 24 | | |
23 | 25 | | |
24 | 26 | | |
25 | 27 | | |
| 28 | + | |
26 | 29 | | |
27 | 30 | | |
28 | 31 | | |
| |||
91 | 94 | | |
92 | 95 | | |
93 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
94 | 102 | | |
95 | 103 | | |
96 | 104 | | |
97 | 105 | | |
98 | 106 | | |
99 | | - | |
| 107 | + | |
100 | 108 | | |
101 | 109 | | |
102 | 110 | | |
103 | 111 | | |
104 | 112 | | |
105 | 113 | | |
106 | 114 | | |
| 115 | + | |
107 | 116 | | |
108 | 117 | | |
109 | 118 | | |
| |||
123 | 132 | | |
124 | 133 | | |
125 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
126 | 139 | | |
127 | 140 | | |
128 | 141 | | |
| |||
202 | 215 | | |
203 | 216 | | |
204 | 217 | | |
| 218 | + | |
205 | 219 | | |
206 | 220 | | |
207 | 221 | | |
| |||
331 | 345 | | |
332 | 346 | | |
333 | 347 | | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
334 | 362 | | |
335 | 363 | | |
336 | 364 | | |
| |||
355 | 383 | | |
356 | 384 | | |
357 | 385 | | |
358 | | - | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
359 | 391 | | |
360 | 392 | | |
361 | 393 | | |
362 | 394 | | |
363 | 395 | | |
364 | | - | |
| 396 | + | |
365 | 397 | | |
366 | 398 | | |
367 | 399 | | |
368 | 400 | | |
369 | 401 | | |
| 402 | + | |
370 | 403 | | |
371 | 404 | | |
372 | 405 | | |
| |||
0 commit comments