Skip to content

Commit 8671754

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-19061-step3-retire-clause2-label
2 parents d656dbb + 342808c commit 8671754

14 files changed

Lines changed: 699 additions & 22 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
**BREAKING (published artifact narrows)** — `packages/spec/json-schema/**` now states the `$`-prefix key ban a normalized field condition enforces, so a validator reading the published files stops answering PASS on `{"$and":[{"$bogus":{"$eq":1}}]}` at `data/NormalizedFilter` — a document the runtime refuses by name (#18670 item 2, the fifth arm).
6+
7+
Clause-②: yes (narrowing)
8+
9+
One named pattern joins the closed list, and only one:
10+
11+
- **`banned-key-pattern` — "no document may carry a key matching this pattern"**, emitted as `propertyNames` with a `not` over a `pattern`. `NormalizedFilter`'s `$and` / `$or` members and its `$not` operand each admit a field condition whose keys are field names (`amount`, `account.name`) and never `$`-prefixed operators. The published file now says so at all three nodes.
12+
13+
**Scoped, and the scope is mechanical.** The ban is over an OPEN set of names, which is why the existing `banned-keys` arm cannot express it — a finite list that merely sampled the set would be wider than the rule. The pattern arm that can express it is bounded by a second closed list: `BannedKeyPattern` is a union of the pattern strings this package publishes, exactly one today (`^\$`), so a call site cannot invent a regex because there is no `string` to pass, and widening it is the same reviewed decision that adding an arm is. That is what answers the standing objection to a regex-shaped declaration — its over-reach cannot be read off the declaration the way a key list's can, so the bound is on how few declarations exist rather than on trusting the next caller.
14+
15+
**The rows retired, by name.** `packages/spec/dropped-refinements.baseline.json`, entry `data/NormalizedFilter`:
16+
17+
| row | before | after |
18+
|:---|:---|:---|
19+
| `lazy.$and.element.options[0]` | dropped | **deleted** — reads `projected`, arm `banned-key-pattern` |
20+
| `lazy.$or.element.options[0]` | dropped | **deleted** — reads `projected`, arm `banned-key-pattern` |
21+
| `lazy.$not.options[0]` | dropped | **deleted** — reads `projected`, arm `banned-key-pattern` |
22+
23+
**⛔ Not a behaviour change, and no document the runtime accepts becomes refused.** The arm is EXACT rather than approximate. A JSON object's properties are exactly its own enumerable string-keyed ones and `propertyNames` judges exactly those names; JSON Schema specifies `pattern` as an ECMA-262 regular expression evaluated as a SEARCH, which is `RegExp.prototype.test` and nothing else — so the same source text decides the same set of names on both sides. It is presence and never value: a matching key present with a `null` value is present to both. Measured with ajv 8 (draft 2020-12) on the generated file, the verdict vector moves in one direction only: the three `$`-prefixed specimens go `true` → `false`, and every document the runtime accepts — `{}`, the empty combinators `{"$and":[]}` / `{"$or":[{}]}` / `{"$not":{}}`, a nested group, an ordinary field condition — is accepted before and after. Across the published tree, **1530 of 1535 files are byte-identical**; one file changes what it accepts, two change annotation only, and the remaining two are the bundle and the build-input hash.
24+
25+
**The predicate and the keyword are ONE string.** `bannedKeyPattern` compiles its `RegExp` from the declared pattern, so the keyword the file publishes and the rule the runtime enforces cannot come to mean different things — the construction `requiredOneOf`, `dependentRequired` and `bannedKeys` already use, and the reason this arm needs no drift pin either. The `RegExp` carries no flags, which is part of the equality rather than a style choice: a JSON Schema `pattern` has none to carry, and `g` would make `test` stateful through `lastIndex` so a key's verdict would depend on which keys were judged before it.
26+
27+
**A ratchet repair ships with it, and it is what made the rows exist to delete.** The detector decided `dropped` vs `projected` on a two-rung projection ladder while the generator publishes on a three-rung one — a node whose every io direction refuses over an unrepresentable member still reaches its file when that member sits in a union position, because the emit loop drops the branch and publishes the rest. Nine PUBLISHED sites therefore read `undecidable`, the one verdict the ledger does not count: they held no row, carried no `x-dropped-refinements`, and no repair of them could ever have deleted a row. The three nodes this arm closes were three of the nine. The detector now carries the generator's third rung and reports which rung answered, so a differential can never compare a pruned projection with an unpruned one; and a published site that still cannot be adjudicated fails the build by name, so the blind spot cannot reopen in silence.
28+
29+
⚠️ **The ledger therefore GREW before it shrank, and the growth is the point.** Six sites became visible that were previously uncounted — `data/FieldOperators` and `data/RangeOperator` gained `$between.items[0]` / `[1]`, `data/NormalizedFilter` gained the same pair under `$not`, and `data/RangeOperator` entered the ledger as a published schema that had been holding no entry at all — then this arm deleted three. Net across the change: **204 entries / 560 sites → 205 / 566**, with the census at **566 dropped / 205 published schemas / 360 projected** (224 `non-blank-string`, 129 `required-one-of`, 3 `banned-key-pattern`, 2 `dependent-required`, 2 `banned-keys`) and **0 undecidable**, down from 9. Those two files gain annotation only: `x-` keywords are ignored by every validator, so the set of documents they accept is unchanged.
30+
31+
⭐ **Superseding a sibling entry in this same release.** `18670-project-banned-keys.md` records that the `$`-prefix sites 「stay unprojected … they carry NO annotation and hold NO ledger row: published yet unratcheted」. That was a correct reading of its own tree and is no longer true of this one: the sites are projected, the blind spot is closed, and the population it described is empty. The earlier entry is left as the record of what it landed.
32+
33+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author can write is removed, renamed or re-spelled: no spec key, no export and no config field changes, and the accepted set of metadata documents is byte-for-byte what it was. What changed is a machine-readable DECLARATION catching up with the runtime it always described, so there is nothing for `objectstack migrate meta` to rewrite and no stored representation to convert. -->
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`liveness/README.md` — the `authorWarn` section's pointer to the lint that emits author warnings named a path that does not exist; it now names the module's real home, `packages/lint/src/lint-liveness-properties.ts` (#19269).
6+
7+
The ledgers and this README ship inside this package (`files[]` includes `liveness`), so the pointer an upgrading reader follows is this one. It read `packages/cli/src/utils/lint-liveness-properties.ts`, measured at zero in a full tree listing, while the module it describes — the one that reads these ledgers, emits the advisory warning and never fails the build — sits in `packages/lint/src/`. Nothing else moves: no schema, no export, no verdict, no ledger entry, no runtime behaviour.
8+
9+
- **This grid has no mechanical reader, which is why it rotted quietly.** `check:liveness` resolves the `evidence` paths inside ledger *entries*; a path written in README prose is checked by nobody, so the pointer stayed wrong through the move with every gate green. The lit control is the rule registration itself: `packages/lint/src/authoring-rules.ts` carries `source: 'packages/lint/src/lint-liveness-properties.ts'` as data, and that is the path this sentence now agrees with.
10+
- **The second pointer in this file is deliberately left alone.** The closing paragraph of the type table says "see lint-liveness-properties.ts" — a bare filename with no directory. It is not stale (the basename resolves uniquely in the tree), and a bare filename carries no directory to rot; giving it one would newly expose it to exactly the failure this change repairs. The full path is stated once, here, where a reader who needs the directory gets it.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the published `effae80` changelog entry states ONE `undoable` capture set — the one that shipped (#19301)
6+
7+
Clause-②: no
8+
9+
`packages/spec` ships `CHANGELOG.md` inside its npm tarball (it is named in
10+
`files[]`), so an entry there is a published surface — the text an upgrading
11+
agent greps. The single entry for `effae80: feat(spec): a row action gets the
12+
declarative single-record field write` stated BOTH capture sets, four lines
13+
apart:
14+
15+
> `undoable` now has its anchor — the patch names exactly the fields whose prior values are captured.
16+
17+
> `undoable` captures the prior values of exactly the fields written.
18+
19+
**The second is the one that shipped.** `declarativeUpdateWrite` builds the
20+
write bag as `{ ...patch, ...params }`, and `executeDeclarativeUpdateAction`
21+
keys `undoData` off `Object.keys(data)` over that bag — so on any
22+
params-carrying action the capture is strictly wider than `patch` names. The
23+
same entry's own `patch` bullet already says the static values are "merged
24+
UNDER the values `params` collects (a param of the same name wins)". The first
25+
sentence was wrong when it was written; it is not a record of behaviour that
26+
later changed.
27+
28+
The first sentence now names the merged write bag, in the wording the settled
29+
`ActionSchema.undoable` description uses. Nothing else in the entry moves and
30+
no other entry is touched: the correction is an **amendment in place**, not an
31+
erratum in a later entry — a reader who greps the old promise lands on this
32+
entry and nowhere else.
33+
34+
- **Prose only.** No key, export, accept set, refusal, tombstone or generated
35+
artifact moves; the same author input parses the same way before and after.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
The install door's doc block no longer denies that the in-process protocol primitive reads `enableOnInstall` (#19339).
6+
7+
`PackageInstallRequestSchema.enableOnInstall` (`api/package-api.zod.ts`) carries the map to the other two declarations of this key, and its entry for the kernel copy read: "its own implementation does not read it, and this door does not forward it down that seam". That was true when it was written and stopped being true when `MetadataProtocol.installPackage` started honouring the key (`482d584121`). Nothing went red — no gate compares a sentence against an implementation — and the text ships: `src/**/*.zod.ts` is in this package's `files[]`, and the comment survives into `dist/api/index.js` and `dist/browser/api/index.mjs`.
8+
9+
Clause-②: no
10+
11+
**Only one half of the sentence was false.** It is a compound claim about two layers, and they were re-derived separately from the source rather than rewritten together:
12+
13+
- `MetadataProtocol.installPackage` (`packages/metadata-protocol/src/protocol.ts`, the `requestedEnabled` arms) now reads the key: `true` enables, `false` disables, an absent key makes no lifecycle call at all. That half is corrected, and scoped — the primitive moves the **registry row**, for the life of the process.
14+
- "this door does not forward it down that seam" is **still true** on `main` and is kept: `handlePackages` (`packages/runtime/src/domains/packages.ts`) calls `installPackage({ manifest, settings })` and performs the enable/disable flip itself, then writes the durable record from the row it returned. Correcting that clause would have swapped one false sentence for another.
15+
16+
The scope words are load-bearing: the durable disabled-package record is keyed by environment (`setPackageDisabled(environmentId, …)`), which an `InstallPackageRequest` does not carry, so `POST /api/v1/packages` still owns the half that survives a restart.
17+
18+
**What does not move.** No key is added, removed, renamed or retyped, and no default changes: the accept set is byte-for-byte what it was, `check:api-surface` and `check:authorable-surface` are green with no diff, and no generated reference page changes — this text is a TSDoc block, not a `.describe()`, so `check:generated` reports all 15 artifacts up to date without a regeneration. The declaration is `no` on both limbs: nothing is widened and nothing is retired.

‎packages/spec/CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3915,7 +3915,7 @@
39153915

39163916
- `operation: 'update'` — the declarative single-record field write. One member by ruling; `'delete'` and `'custom'` are refused with the reason (a row delete is the object's own affordance; `'custom'` means "dispatch the action this def names", and on a row the action is already the action).
39173917
- `patch: Record<string, unknown>` — static field values written to the current record, merged UNDER the values `params` collects (a param of the same name wins). Passed through verbatim.
3918-
- The action's existing `params`, `visible`, `confirmText` and `undoable` keys are reused, nothing duplicated; `undoable` now has its anchor — the patch names exactly the fields whose prior values are captured.
3918+
- The action's existing `params`, `visible`, `confirmText` and `undoable` keys are reused, nothing duplicated; `undoable` now has its anchor — the merged write bag, `patch` UNDER the collected `params`, names exactly the fields whose prior values are captured.
39193919

39203920
Key shape, pinned for contract review: `operation` is a parallel key beside `type`, not a new `ActionType` member. `type` stays at its default `'script'` — the platform action route, which is where the write is performed — and answers WHERE the action dispatches; `operation` answers WHAT the platform does there. Every contradiction is refused at its own path with a prescription: any other explicit `type`, `target`, `body`, `method`, `bodyExtra`, `bodyShape`, `recordIdParam`/`recordIdField`, `onSuccess`, `opensInNewTab`/`newTabUrl` beside `operation: 'update'`; `patch` without it; `operation: 'update'` with neither `patch` nor `params`; a `list_toolbar` location (no current record). `defineStack` refuses a standalone `operation: 'update'` action that names no `objectName` (an object-embedded one is bound by the object it is written on). An inline page-element action cannot carry the keys at all.
39213921

‎packages/spec/dropped-refinements.baseline.json‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
"description": "Shrink-only ledger of every PUBLISHED JSON Schema that is STILL WIDER than the Zod type it was generated from, because a rule written as `.refine()` reaches the runtime and not the file (#18670). `z.toJSONSchema()` has no arm for a `custom` check: a plain record, the same record with a `.refine()`, and the same record with an ABORTING `.refine()` all project byte-identically (measured on zod 4.4.3, the version packages/spec resolves). So a document one of these files ACCEPTS can still be refused at parse time, and an author -- or an AI -- validating against packages/spec/json-schema/** finds out a release later. Each `sites` path is a position under that schema at which a refinement is dropped; the same paths are written onto the artifact itself as `x-dropped-refinements`. Item 2 closed the first patterns: a refinement DECLARED through the closed list in src/shared/refinement-projection.ts is emitted into the published file, reads `projected` rather than `dropped`, and its row LEAVES this ledger in the same PR -- which is why the ledger shrinks and never grows on a repair. Every refinement outside that closed list stays here, and adding an arm to the list is a public-contract decision, not a refactor. Hand-edited on purpose and with no `gen:` script: a generator would let a new gap be admitted by running a command instead of by a decision, which is the silence this ledger exists to end. Adding, removing or moving a site fails packages/spec/scripts/build-schemas.ts until the line moves with it, and the failure prints the corrected entry in full. ⛔ Do not delete or weaken a refinement to shorten this file -- the runtime rule is correct; it is the projection that is silent, and the remedy is to teach the closed list a NAMED pattern, never to drop the rule.",
33
"measured": {
44
"zod": "4.4.3",
5-
"publishedSchemasWithDroppedRefinements": 205,
6-
"droppedRefinementSites": 565,
7-
"refinementSitesThatDidProject": 366,
8-
"refinementSitesWithNoJsonFormToCompare": 9
5+
"publishedSchemasWithDroppedRefinements": 206,
6+
"droppedRefinementSites": 571,
7+
"refinementSitesThatDidProject": 369,
8+
"refinementSitesWithNoJsonFormToCompare": 0
99
},
1010
"entries": {
1111
"ai/BlueprintField": {
@@ -614,6 +614,8 @@
614614
},
615615
"data/FieldOperators": {
616616
"sites": [
617+
"$between.items[0]",
618+
"$between.items[1]",
617619
"$in",
618620
"$nin"
619621
]
@@ -677,6 +679,8 @@
677679
},
678680
"data/NormalizedFilter": {
679681
"sites": [
682+
"lazy.$not.options[0].valueType.$between.items[0]",
683+
"lazy.$not.options[0].valueType.$between.items[1]",
680684
"lazy.$not.options[0].valueType.$in",
681685
"lazy.$not.options[0].valueType.$nin"
682686
]
@@ -745,6 +749,12 @@
745749
"in.where.options[0].lazy.options[2].items[0]"
746750
]
747751
},
752+
"data/RangeOperator": {
753+
"sites": [
754+
"$between.items[0]",
755+
"$between.items[1]"
756+
]
757+
},
748758
"data/ReferenceIdValue": {
749759
"sites": [
750760
""

‎packages/spec/liveness/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ deleting or renaming a proof re-runs this check and the dangling reference is ca
545545
## Author warnings — closing the loop (`authorWarn`)
546546

547547
Classification is also fed back to the *author* at build time. The CLI `compile`
548-
lint (`packages/cli/src/utils/lint-liveness-properties.ts`) reads these ledgers and
548+
lint (`packages/lint/src/lint-liveness-properties.ts`) reads these ledgers and
549549
emits an advisory **warning** when an authored object/field sets a property that is
550550
misleading — "you set this expecting it to do something; at runtime it does nothing /
551551
isn't enforced" — with a corrective hint. Never fails the build.

0 commit comments

Comments
 (0)