You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 39ba6e6
Browse filesBrowse the repository at this point in the historyBrowse files
`seed-name-lookup.ts` — the batched seed existence read's OWN failure now reaches the author when no logger was injected, through the one delivery derivation the package already owns (#18570).
6
+
7
+
The oracle every declared-metadata seeder consults hoists one `$in` read out of its loop and degrades to the per-item read when that read cannot answer — an outage, or a page proven to be a prefix of the answer. It reported that degradation through a doubly-optional `logger?.warn?.(…)`, which evaluates to NOTHING when the caller injected no sink: the read failed, the pass silently switched to the slow path, and no human was told.
8
+
9
+
Measured differentially rather than read off the code, in this package's own `bootstrap-declared-capabilities` control harness: an unreadable-database pass with **no logger** printed exactly **one** author-visible line — the seeder's own end-of-pass summary — while the batched read that failed *first* said nothing. With this change the same pass prints **two**, and that assertion is now the pin (`toHaveLength(1)` → `toHaveLength(2)`, both lines selected by content).
10
+
11
+
-**Delivery only.** The wording, the structured meta (`object`, `names`, `rowBudget`, `organization`) and the two named causes — `unreadable` and `truncated` — are axis-specific and stay at the call site, which is the split `seed-refusal-sink.ts` documents. ⛔ No sixth hand-written copy of the rule, and ⛔ no generic refusal sentence.
12
+
-**A read that ANSWERED stays silent on every channel**, with or without a sink — the discriminating control that keeps a healthy boot quiet.
13
+
-**It also stops a throw.**`logger?.warn?.(…)` guards `null`/`undefined`, never a non-callable `warn`: a host that declared one and shipped something else raised `TypeError: logger?.warn is not a function`*inside* the degradation path, turning a slower read into a failed boot. The site now asks `typeof` — the same question `reportThroughSink` asks — so such a host takes the console arm instead.
14
+
-**No exported surface moves.**`seed-name-lookup.ts` is package-private (`src/index.ts` re-exports nothing from it) and `SeedLookupLogger` is unchanged, both members still optional.
`/discovery` advertises `capabilities.transactionalBatch` from the predicate the atomic-batch refusal already trusts, so the advertisement and the 501 stop disagreeing (#18997).
6
+
7
+
`getDiscovery()` derived the bit from the ENGINE alone — `typeof this.engine?.transaction === 'function'` — while `runAtomicBatch` refuses `batchData({ atomic: true })` with `501 NOT_IMPLEMENTED` on `engineCanRollBack(engine)`, which asks the DEFAULT DRIVER as well. `engine.transaction` is a function on every real engine, so the advertisement answered `true` for compositions that then 501 — and the 501's own remedy text sends the caller to that very bit ("probe `capabilities.transactionalBatch` on /discovery first"). The prescribed remedy routed the caller to a signal that was wrong in exactly the case the remedy exists for.
8
+
9
+
**What a consumer sees.** Two compositions, measured separately, stop advertising `true` and now advertise `false`:
10
+
11
+
-**(a) a default driver with no `beginTransaction` at all** — pre-existing, not introduced by #18063;
12
+
-**(b) a default driver that inherits `beginTransaction` and declares `supports.transactionsUnsupported`** — the population #18063 added; the shipped example is `TursoDriver` on its remote transport.
13
+
14
+
Both already answered `501 NOT_IMPLEMENTED` to an atomic batch, so nothing that was accepted becomes refused. A client that read `true` and proceeded was taking the 501; it now reads `false` and takes its non-atomic fallback ahead of the failure — which is what probing the capability was for. A client that hard-asserts `transactionalBatch === true` at startup against such a composition fails at startup instead of at the first atomic batch.
15
+
16
+
Unchanged in the other direction, and pinned so that "honest" cannot decay into "always `false`": a composition whose default driver **can** roll back still advertises `true`, and so does a host whose driver registry is not inspectable (test doubles, metadata-only hosts), where the engine-level probe is all there is. Measured over all 16 compositions of the four inputs the two predicates read: 0 go `false` → `true`, 3 go `true` → `false`.
The dataset publish door now judges the dataset. A runtime-created `dataset` reached ZERO author-time rules; it now dispatches the existence rules that were already written for it (#19143).
7
+
8
+
`dataset` is a registered metadata type declaring `allowRuntimeCreate: true`, so Studio's designer, REST `/meta` item CRUD and an MCP/AI author may all mint one — and at that door nothing judged it. Measured on `origin/main`: no rule declared `dataset` in `runtimeTypes` (zero, against a lit control returning every other declared type), and `TYPE_TO_STACK_KEY` in `runtime-gate.ts` carried no `dataset` row. The two absences were **consistent rather than contradictory** — the gate filters by `runtimeTypes` before it consults the table — so nothing was mis-wired and CI was green, correctly. What they summed to is that a dataset write built no per-write snapshot and dispatched no rule at all, while the rules that judge a dataset state their own failure mode as a surface that *"renders successfully with empty or wrong numbers"*. An author working only through Studio or MCP has no `os lint` step to fall back on, so for them that door is the only one there is.
9
+
10
+
ADR-0049's 「声明即强制」 admits two resolutions and the card chose neither; this takes the first because the measurement says so. The author-time rules for `dataset`**exist**: `validateDatasetReferences` (#14105, `packages/lint/src/validate-dataset-references.ts`), `validateDatasetMeasureAggregates` (#16354) and `validateObjectReferences`' `datasets[].object` rung. The declaration is honoured rather than retired.
11
+
12
+
-**`TYPE_TO_STACK_KEY` gains `dataset: 'datasets'`**, and the rules that READ that collection are declared in the same commit — never a mapping ahead of its rules, which is the inert state the table's own `seed: 'data'` note records paying for. Every crossed rule has a door control that fires it through the real gate.
13
+
-**`validateDatasetMeasureAggregates` crosses to `CLI_AND_RUNTIME` with `runtimeTypes: ['dataset']`.** Its previous `surfaceReason` named this exact gap as what held it off the door.
14
+
-**The reference-integrity suite entry gains `dataset`**, and its per-member axis admits exactly two members — `validateDatasetReferences` and `validateObjectReferences`. Both resolve only against `stack.objects` and `stack.datasets`, the two collections a per-write snapshot carries, so neither opens a missing-collection false-positive channel. They cross together on #7220's reading: an author refused for a dangling dimension field and waved through for a dangling base object cannot predict the door.
15
+
-**No new rule and no new finding class.** The rule ids (`dataset-field-unknown`, `dataset-field-not-included`, `dataset-filter-field-unknown`, `dataset-include-unknown`, `measure-aggregate-field-type-refused`, `object-reference-unknown`) and their severities are unchanged — they now reach the door where the author actually is.
16
+
-**Findings from a dataset write are name-keyed on the wire** (#10064): `datasets.<name>.dimensions[0].field`, never the gate's private snapshot index. `datasets` entered the derived name-keyed set by derivation, with no second edit to remember.
17
+
-**Measured before crossing**, at the door's own snapshot shape and differential, over every dataset shipped in this monorepo — **11 datasets** (`platform-objects` 5 over `sys_*`, showcase 4, crm 1, todo 1) judged against 52 platform objects plus each app's own (showcase 22, crm 6, todo 1): **0 findings, 0 advisories, `rulesRun` 2 on every one** — so the zero is a fact about the corpus and not about a door that ran nothing. The same harness's synthetic probe IS refused, with both ids and both name-keyed paths.
18
+
19
+
## Migration
20
+
21
+
**A dataset publish that used to succeed can now be refused (HTTP 422, `INVALID_METADATA`).** The receipt names the rule id and the offending path, name-keyed on the wire — for example `datasets.invoice_metrics.dimensions[0].field` or `datasets.invoice_metrics.measures[1].aggregate` — plus the string that was written.
22
+
23
+
To clear a refusal, do one of:
24
+
25
+
- point the `dimensions[].field` / `measures[].field` path at a column the base object actually declares (after a Studio label edit the derived API name is the one to use); or
26
+
- add the relationship the path traverses to the dataset's `include[]`, for `dataset-field-not-included`; or
27
+
- correct the filter KEY, for `dataset-filter-field-unknown`; or
28
+
- for `measure-aggregate-field-type-refused`, either aggregate a field of an accepted type or choose an aggregate the field's type accepts (`count` / `count_distinct` accept every type) — the compile leg already refuses that same pair with `400 DATASET_INVALID` once a query is built, so this is the same fix made earlier; or
29
+
- for `object-reference-unknown`, point `object` at an object this stack defines, or at a platform object by its full name.
30
+
31
+
`os validate` / `os build` / `os lint` already reported every one of these findings at the same severity, so a code-authored stack can be repaired before it ever reaches a publish. A dataset over an object this stack does not define, one that declares no readable field map, and a registry-injected system column are all skipped exactly as they were on the CLI — the door adds no verdict the commands did not already make. A stored dataset already in violation is never charged to an unrelated publish, and republishing a dataset under its own name with the defect removed is clean (#4463 D4).
The activity-timeline summary resolves a reference field's target through `referenceTargetOf` instead of the materialized `reference` carrier, so a `trackHistory`'d `{ type: 'user' }` field authored without one is planned, read and rendered as a name instead of silently showing the raw id (#19264).
6
+
7
+
`audit-writers.ts` admitted `user` as a reference type and then required an EXPLICIT `reference` on it. The spec declares exactly the opposite for that type: `IMPLICIT_REFERENCE_TARGETS` (`@objectstack/spec/data`) says a `user` field's target is "a CONSTANT OF THE TYPE, so `reference` on a `user` field materializes that constant; it does not supply it. Metadata authored without it (hand-written JSON, an AI author, a Studio form) is **fully specified, not under-specified**." So the one spelling the contract calls complete was the one the reader refused — and it refused it **silently**: the field was simply absent from the read plan, and the timeline rendered `usr_1` where every other reference field showed a name.
8
+
9
+
-**Four sites, not two.** The target is the key of the `id → title` map, so it has two ends: the two read planners (`planTrackedLookupReads`, `planMilestoneTokenReads`) build the plan under it and the two renderers (`renderTrackedChangeSummary`, `renderMilestoneSummary`) look the resolved titles back up under it. All four now ask one helper, so repairing the plan alone cannot pay for a read whose result the renderer then fails to find.
10
+
-**Nothing else widens.** The admitted types are unchanged (`lookup`, `master_detail`, `user`), so a `lookup` / `master_detail` whose author-chosen target is absent still names nothing, is still left out, and still issues no read — `tree` is deliberately not added.
11
+
-**A padded carrier can no longer split the key.** The planners used to `trim()` and the renderers did not, so `reference: ' crm_account '` produced two keys and no title; one helper trims once for both ends.
12
+
-**The unreadable-carrier behaviour is unchanged.**`referenceTargetOf` reads the carrier through `referenceCarrierOf`, which throws for an object- or array-valued `reference`; that throw is caught at the helper because this code runs inside `writeAudit`'s summary composition, which is not inside the `try` that guards the audit row write — an escaping `TypeError` would turn a display-enrichment miss into a failure on the audited write's own path. Such a carrier is left out exactly as it was before.
13
+
-**No authoring change.** Metadata that already spells `reference: 'sys_user'` resolves to the same target it always did; nobody has to restate the constant.
0 commit comments