Skip to content

Commit 3c7b84d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14477-inert-deadline-key-retirement
2 parents 3273a34 + ed9d876 commit 3c7b84d

26 files changed

Lines changed: 1798 additions & 121 deletions
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/core": patch
3+
---
4+
5+
fix(core): narrow the operation-private-keys pin's scanner to `.ts`, so it judges exactly the population turbo re-runs it for (#15090)
6+
7+
`packages/core/src/security/operation-private-keys.pin.test.ts` filtered its
8+
candidate set with `/\.tsx?$/``.ts` **and** `.tsx` — while this package's
9+
declared radius in the cross-package declaration table is a `packages/**`
10+
subtree glob ending in `.ts`. So the pin judged a population **strictly wider**
11+
than the one either scoping layer of `check:cross-package-test-inputs` knows
12+
about: Layer A never unions this package into the test shard when a `.tsx` file
13+
changes, and Layer B never moves the `test` task's cache hash for one. A `.tsx`
14+
file under `packages/` declaring its own `OPERATION_PRIVATE_KEY_PREFIX` or
15+
`withoutOperationPrivateKeys` was therefore scanned by the pin and invisible to
16+
CI's scoping — landing on `main` with every PR green and then reddening whichever
17+
unrelated PR next touched a `.ts` file. That is the #7802 shape the declaration
18+
table exists to close, one extension wide.
19+
20+
Repaired by narrowing the **scanner**, not by widening the **glob** — and that
21+
asymmetry is measured rather than assumed. On `b548e438d`, adding a `.tsx` glob
22+
to this package's roster entry and re-deriving `check:cross-package-test-inputs`'
23+
watch hints flips the dispatch-gates self-test case *"nor a .tsx test file inside
24+
it"* from true to false, with the added glob itself as the covering hint. That
25+
case is a live specimen for "a test class the hint route cannot reach", so the
26+
red is real and re-pointing it is a decision in another lane, not a fixup.
27+
28+
What the boundary costs, measured on the pin's own surface (tracked **plus**
29+
untracked, ignored paths excluded) at `b548e438d`: **5408** `.ts` files scanned,
30+
8 of them mentioning a guarded symbol; **8** `.tsx` files excluded, **0** of them
31+
mentioning either symbol. The loss is empty today — and that reading is no longer
32+
transcribed and trusted. A new case re-measures it on every run: it asserts the
33+
excluded `.tsx` population is non-empty (so the boundary is an exclusion and not
34+
an empty tree describing itself), that the filter really drops those files, and
35+
that none of them declares either symbol. Ablation, with the restore proven by
36+
blob hash rather than by exit code: re-widening the scanner reddens it while the
37+
offender assertion stays green — which is precisely the failure mode, since a
38+
wider scanner reads as coverage CI never runs — and planting a `.tsx`
39+
redeclaration reddens it with a message that says the choice is a second-gate
40+
trade, not a one-line widening.
41+
42+
The correspondence between scanner and glob is now stated at **both** ends: the
43+
pin's header and the declaration table's entry for this package. No published
44+
surface moves — the only source file edited is a test.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/driver-memory': minor
3+
---
4+
5+
fix(driver-memory): `find()`, `findOne()` and `create()` publish their declared types (#14435)
6+
7+
**BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, the same shape #13878 landed on `update()` / `upsert()` one door over, shipped as `minor` under the launch-window convention (`major` is refused by `check-changeset-no-major`, so the BREAKING banner and the ADR-0087 disposition are the carriers, not the level).
8+
9+
`IDataDriver` has always declared `Promise<Record<string, unknown>[]>`, `Promise<Record<string, unknown> | null>` and `Promise<Record<string, unknown>>` on these three doors. The emitted `.d.ts` published `Promise<any[]>`, `Promise<any>` and `Promise<Record<string, any>>`: the return types of `find` and `findOne` were INFERRED through the backing store's `any[]` rows (`private db: Record<string, any[]>` to `getTable()`), and `create` carried an explicit annotation that itself spelled `Record<string, any>`. They are now declared as the contract declares them.
10+
11+
What this asks of a consumer holding a concrete `InMemoryDriver`: a caller that reads fields off a `findOne()` result narrows the `null` arm first — the arm the driver has always been able to answer with (`results[0] || null`) and that no caller was ever asked to handle; and a caller that leaned on `any` to read a member off a `find()` row or a `create()` result now types it, since the rows are `Record<string, unknown>`. A consumer whose receiver is typed as `IDataDriver` sees no change at all — that declaration already said this.
12+
13+
The parameters are deliberately untouched: `create(data: Record<string, any>)` stays as it is, because narrowing an INPUT would be a second, unrelated break, and method parameters compare bivariantly against the contract's `Record<string, unknown>`. No runtime behaviour changes; the store keeps its `any[]` rows, which the card measured to cascade if re-typed.
14+
15+
<!-- adr-0087: not-required (no-migration-prescription) A published return type moves off `any` onto the contract's own shape: no metadata key is removed, renamed or re-shaped, no spec schema changes (this diff touches `packages/drivers/driver-memory/**` only), and nothing exists for `objectstack migrate meta` to rewrite. The obligation is a TypeScript narrowing at the consumer's call site, delivered by the compiler. -->
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@objectstack/lint": minor
3+
"@objectstack/metadata-protocol": minor
4+
---
5+
6+
A publish now refuses an object whose `highlightFields` names a field that does not exist on it — the same gate that refuses a code-authored stack.
7+
8+
`list-view-field-unknown` inspects `view.columns`, and Studio's app builder mints no `view` items at all, so the reference-integrity family had nothing to inspect on the only artifacts the click path authors. What it authors is the **object**, and an object-level field-name list was covered by nothing that could refuse: measured on `origin/main`, `runtimeAuthoringRulesFor('object')` dispatched seven rules with no reference-integrity rule among them, while the object-level existence check that did exist (`semantic-role-field-unknown`) is `warning`, advisory-tier and CLI-only. So `os validate` exited 0 on a dangling reference and the runtime publish door — the only door a Studio, REST `/meta` or MCP author has — said nothing at all.
9+
10+
The reproduction is the natural click order, not a contrived one: click-create a field (Studio mints it as `field_10`), add it to `highlightFields`, then give it a label — the API name auto-derives to `health_score` and `highlightFields` keeps `field_10`. Anyone who names a field after placing it produces this.
11+
12+
- **New rule `object-field-ref-unknown` (`error`)**, in `@objectstack/lint`, over the object-level field-name **lists** that no rule owned: `highlightFields` (ADR-0085) and `publicSharing.redactFields`. It resolves through the same `object-graph` seam as the rest of the family, so the three shared skips hold — an object outside the stack, an object with no readable field map (ADR-0015 `external`), and a registry-injected system column resolved **per object** (`highlightFields: ['owner_id']` is a live pointer on an owned object and a real miss under `ownership: 'none'`).
13+
- **It runs on the runtime publish door.** The reference-integrity suite entry's `runtimeTypes` gains `object`, and the suite's per-member declaration keeps the crossing narrow: this is the only member that judges an object snapshot; every other member keeps `['flow', 'view']` or the frozen `['flow']` default.
14+
- **`validateSemanticRoles` keeps the provenance question** at the same position (`semantic-role-field-unprovisioned`, still `warning`) and no longer restates existence — one finding per path, at one tier.
15+
- **`probes.checked` gained an `objects` counter.** Its absence was the tell: a receipt reading `{seeds: 0, views: 0, widgets: 0}` was accurate while the objects the package published were probed by nothing.
16+
17+
## Migration
18+
19+
**A publish that used to succeed can now be refused (HTTP 422, `INVALID_METADATA`).** The receipt names the rule id `object-field-ref-unknown` and the offending path, name-keyed on the wire — for example `objects.proj_task.highlightFields[1]` — plus the string that was written and the fields the object actually has.
20+
21+
To fix a dangling reference, do one of:
22+
23+
- rewrite the entry to the field's current API name (after a Studio label edit the derived name is the one to use — `field_10` becomes `health_score`); or
24+
- drop the entry from the list.
25+
26+
`os validate` / `os build` / `os lint` report the same finding at `error`, so a stack can be repaired before it reaches a publish. If an object legitimately points at a platform-injected system column, no change is needed — the rule resolves those per object and stays silent where the platform really provisions them.

content/docs/data-modeling/field-types.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ These properties are available on **all** field types:
653653
| `description` | `string` | — | Field description / help text |
654654
| `type` | `FieldType` | **required** | One of the supported field types |
655655
| `required` | `boolean` | `false` | Whether the field is required |
656-
| `unique` | `boolean \| 'global' \| 'organization'` | `false` | Unique constraint **and its scope** (ADR-0120). `'organization'` = one holder per organization; `true` = that same per-organization scope (positional synonym — prefer the explicit spelling in new code); `'global'` = one holder across the whole installation; `false` = no constraint. `'tenant'`/`'org'` are rejected — the word is `'organization'`. Full rule in the `unique` doc block in [`FieldSchema`](/docs/references/data/field) |
656+
| `unique` | `boolean \| 'global' \| 'organization'` | `false` (`'organization'` on `autonumber`) | Unique constraint **and its scope** (ADR-0120). `'organization'` = one holder per organization; `true` = that same per-organization scope (positional synonym — prefer the explicit spelling in new code); `'global'` = one holder across the whole installation; `false` = no constraint. Omitted ⇒ `false`, except on an `autonumber` field, where omitted ⇒ `'organization'` — an auto-number is a business identifier, so the platform makes it unique per organization by default; write `unique: false` explicitly to opt out. `'tenant'`/`'org'` are rejected — the word is `'organization'`. Full rule in the `unique` doc block in [`FieldSchema`](/docs/references/data/field) |
657657
| `multiple` | `boolean` | `false` | Allow array of values (multi-record lookup, multi-select, multi-file). An emptied multi-value lookup reads back as `[]`, never `null` — the `multiple` doc block in [`FieldSchema`](/docs/references/data/field) |
658658
| `searchable` | `boolean` | `false` | Include in search index |
659659
| `sortable` | `boolean` | `true` | Allow sorting by this field |

0 commit comments

Comments
 (0)