Repository navigation
Commit 2ce2e36
docs(spec): put the
Fixes #24291
Clause-②: no
## What changes
This PR rewrites one sentence in the `forceCbpMasterDetailRequired`
docblock and adds a prose-only `patch` changeset. Nothing else changes.
The sentence is in the "Why" paragraph of
`packages/spec/src/data/object.zod.ts`, now `:3483`–`:3490`.
Before:
> Today only the security gate (`assertControlledByParentWrite`) closes
that shape, and commit 75b7c24 records that the declaration and the
enforcement disagree. This makes the unsafe shape impossible to NEWLY
declare:
After:
> Before commit 75b7c24 only the security gate
(`assertControlledByParentWrite`) closed that shape, by refusing the
insert at runtime, and that commit records that the declaration and the
enforcement disagreed. Since protocol 18 the parse refuses the
declaration itself: {@link refuseControlledByParentWithoutOneMaster}
refuses a `controlled_by_parent` object none of whose `master_detail`
fields declares `required: true`. The builder makes the unsafe shape
impossible to NEWLY declare:
- "This makes" became "The builder makes". The sentence before it is now
about the parse, so "This" would point at the wrong mechanism.
- `.changeset/24291-cbp-docblock-tense.md`: `'@objectstack/spec':
patch`, prose only, with `Clause-②: no`.
No key, schema, export, accepted value or message text changes. All 12
changed lines in `packages/spec` are ` *` comment lines.
## What each claim rests on
**The past half: before commit 75b7c24, only the security gate closed
that shape, by refusing the insert.**
- Commit `75b7c240a3eb` (read through the REST API) adds the builder
force and this docblock. Its changeset says: "#8772 measured that only
the security gate closed that shape while the declaration surface
accepted it." So the old "Today" meant the tree before that commit, and
"records" stays true.
- I read `packages/plugins/plugin-security/src/security-plugin.ts` at
`75b7c240a` through the contents API. That commit does not touch this
file, so its parent has the same content.
- `resolveCbpRelation` picks a required `master_detail`, then any
`master_detail` (`:4949`), then a required `lookup`.
- A non-required master resolves with `omissionRefusedByValidation`
false (`:4969`).
- The insert leg takes the master id from the body (`:5164`). A null id
throws `MasterReferenceMissingError` (`:5251`), because the stand-down
at `:5250` does not apply.
- The record validator never checks a field that is not `required` (PR
#24290's contract review, D7). So at runtime, the gate's insert refusal
was the only one.
**The present half: since protocol 18, the parse refuses the declaration
itself.**
- `refuseControlledByParentWithoutOneMaster` (`object.zod.ts:2213`) runs
inside `ObjectSchemaBase`'s `.superRefine` (`:3143`, called at `:3167`).
- It reports the verdict of `controlledByParentMasterRefusal`
(`src/data/controlled-by-parent-master.ts`). That function counts a
field as a master only when `type === 'master_detail'` and `required ===
true`, and refuses unless exactly one field counts.
- It is a protocol 18 change: the parse refusal is registered by
`src/migrations/entries/semantic/18.cbp-master-relation-tiers-retired.ts`.
- I ran a one-off probe against the built `dist/data/index.mjs`. No test
file was added or kept. Each object below declares `sharingModel:
'controlled_by_parent'`:
| `master_detail` fields | `ObjectSchema.safeParse` |
`ObjectSchema.create` |
|---|---|---|
| one, `required` omitted | refused at `["sharingModel"]`, "but no
required `master_detail` field" | passes (forced) |
| one, `required: false` | refused at `["sharingModel"]` | throws (the
force's refusal) |
| one, `required: true` | passes | passes |
| one required, plus one with `required` omitted | passes | throws at
`["fields"]` (two required after the force) |
The fourth row is why the new sentence states the exact predicate ("none
of whose `master_detail` fields declares `required: true`"). It does not
say "a `master_detail` without `required: true`", which would be wrong
for that row.
**Which door the sentence claims.** It claims only the `ObjectSchema`
parse. That means `.parse()`, `.safeParse()` and `create()`, which runs
the force (`:3677`) and then `ObjectSchemaBase.parse(withDefaults)`
(`:3687`).
The sentence says nothing about `defineStack`, `os validate`, the
metadata save door or stored rows. The refinement's own docblock says
stored rows still register, under a `[metadata_spec_invalid]`
diagnostic. I did not read those doors for this PR, so the sentence does
not name them.
The sentence also drops "only", and it does not say whether the gate
still closes the shape. I read what the gate does today: it derives a
master from exactly one required `master_detail`
(`security-plugin.ts:7923`), and on any write to an object without one
it throws `MasterDetailRelationMissingError` (`:8585`). That is not this
sentence's subject, so the sentence makes no claim about it.
**The symbol reference.** The new text cites the symbol as `{@link
refuseControlledByParentWithoutOneMaster}`. That is the form the same
docblock already uses one paragraph below the list. It is not a
`path#symbol` anchor, so `check-spec-docblock-symbol-anchors` does not
judge it. That gate counts `12 symbol (12 declaration, 0 literal)` on
this head, the same as on PR #24290. The name has exactly one
declaration in the file: `git grep -c "^function
refuseControlledByParentWithoutOneMaster("` gives 1.
## Changeset
`@objectstack/spec`'s `files` array ships `src/**/*.zod.ts`, so the
docblock goes into the tarball as written. It does not reach `dist`.
After the build, 0 `dist` files carry the docblock's unchanged phrase
"arms the worst measured failure shape", and 0 carry the new "closed
that shape, by refusing the". The function's name is in `dist`, which is
the positive control. So the published surface is the source file, and
the changeset is a `patch` with `Clause-②: no`. This follows PR #24290
and PR #24072.
## Verification
The final head is `5d9d774b1`, the merge of `main` at `21070a2f0`. The
two commits that merge brought in touch only `packages/lint`, `docs/qa`
and one changeset. Build and test ran through
`scripts/pm/os-verify-lock.sh`, and every exit code was captured before
any pipe.
- `pnpm --filter @objectstack/spec build && pnpm --filter
@objectstack/spec check:generated`: `VERDICT command-exit 0`, `All 14
generated artifacts are up to date`. Nothing was regenerated.
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: `Test Files 694 passed (694)`, `Tests 21040 passed | 1
todo (21041)`.
- `pnpm --filter @objectstack/spec typecheck`: `VERDICT command-exit 0`,
ending `check:test-typecheck: OK`.
- The build, the tests and the typecheck ran on the pre-merge head
`6abb5d815`. The merge brought no `packages/spec` change.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `5d9d774b1` derived 81 commands from 2
paths against merge base `21070a2f0`. 80 exit 0. The `--ran`
reconciliation reads `81 derived, 80 run, 1 NOT-MEASURED, 0 UNRUN`.
- `check-spec-docblock-symbol-anchors`: `4972 anchors across 2121 spec
sources resolve`.
- `check:nul-bytes`: OK.
- `check-adr-0087-registration`: `this PR adds no declared-breaking
changeset`.
- `check-empty-changeset`: green.
- `check:doc-formula-expressions` and `check:lean-entry-closure` read
built output. Both exit 0, after I built `@objectstack/formula`,
`@objectstack/lint` and `@objectstack/objectql`.
- **NOT MEASURED: `check:dual-build-cjs-loads`** (exit 3, `PREREQUISITE
NOT MET`). It reads every workspace package's `dist`, and 69 package
entries had none. I did not run a whole-repo build locally; that is a
declared narrowing. CI's `Lint & Repo Gates` runs this gate.
- Comment-only check: the `git diff -U0` of `packages/spec` against the
base has 12 changed lines, and each one starts with ` *`. Neither
changed file contains a control byte.
- `git grep "Today only the security gate" -- packages`: 0 hits on this
head, 1 on the base `ef6564684`.
- No ablation: the diff changes no tested behaviour.
- Left to CI: `test:repo` (`Test Core`), `pnpm lint`, the type-check
lanes, and the families outside the derived set.
## Acceptance notes
- **The same docblock, outside this card's file surface.** The paragraph
after the list (`:3498`–`:3507`) says "so a raw parse of a
`master_detail` without `required: true` is refused rather than forced".
The probe's fourth row reads otherwise: next to a required
`master_detail`, a second one without `required` parses green.
(`create()` refuses that object, because the force makes both required.)
Read as "the object's one `master_detail`", the sentence holds. This is
an observation, not a defect. Carrier: none.
- **`packages/lint/src/data-model-rules.ts:304`, a code comment.** It
says "the security gate's fallbacks stay, so metadata already at rest
keeps loading and is still guarded". Since protocol 18, the gate has no
fallback: `security-plugin.ts:7923`, and the gate's own docblock says
both tiers retired. This is the same family, a comment that names a
retired mechanism in the present tense. It is not published:
`@objectstack/lint`'s `files` is `dist`, `README.md` and `CHANGELOG.md`.
0 `dist` files carry the phrase, while the neighbouring code string
"readable by nobody" is in `dist`. Carrier: none.
## Deviations
- `main` was merged once before this PR opened (`5d9d774b1`), as
AGENTS.md section 10 asks.
- I read commit `75b7c240a` twice through the REST API: the commit
itself, and `security-plugin.ts` at that ref. Nothing was fetched into
the shared object store.
- Labels: none written.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_
Co-authored-by: Claude <noreply@anthropic.com>forceCbpMasterDetailRequired "only the security gate" sentence in the past tense (#24310)1 parent f5e98a3 commit 2ce2e36
2 files changed
Lines changed: 21 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3480 | 3480 | | |
3481 | 3481 | | |
3482 | 3482 | | |
3483 | | - | |
3484 | | - | |
3485 | | - | |
3486 | | - | |
| 3483 | + | |
| 3484 | + | |
| 3485 | + | |
| 3486 | + | |
| 3487 | + | |
| 3488 | + | |
| 3489 | + | |
| 3490 | + | |
3487 | 3491 | | |
3488 | 3492 | | |
3489 | 3493 | | |
| |||
0 commit comments