Commit 63b33e6
* fix(cli,lint): run validateFormLayout, and close the rule registry from the other side (#4449)
`validateFormLayout` was implemented, unit-tested, exported and given published
rule ids — and no command ever called it. A whole-repo search found the
implementation, the barrel export line and its own unit test, and nothing else:
the rule ran on zero stacks for as long as it existed.
Two changes:
* register it in `AUTHORING_RULES` as `advisory` on all three commands. It
walks structured metadata only (no lazy dependency), so `os validate`,
`os build` and `os lint` pay nothing measurable for it.
* add the reverse closure to the wiring guard. Every invariant #4409 shipped
starts FROM a registry and looks at the commands, which cannot see a rule
that never entered a registry — the same blind spot as #4402's name list,
one layer up. The guard now subtracts both registries from the `validate*`
/ `lint*` symbols on `@objectstack/lint`'s public barrel; the difference
must be empty or ledgered with a reason in `UNWIRED_RULE_LEDGER`, which
ships empty because today's difference was exactly this one rule.
The new tests fail without the registry entry: the closure reports
`validateFormLayout` as unwired, and the liveness test asserts the entry's own
`run` adapter returns both findings for a stack that earns them — membership
alone is not evidence a rule produces output.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
* fix(spec): a stored reference holding an embedded record is not a valid id (#4455)
`os migrate value-shapes` is the evidence half of the ADR-0104 D1
per-deployment gate, and the scan's own header names the case it exists for: "a
`location` stored as `{latitude, longitude}` or a `lookup` holding an expanded
record object". The second case was never detected. `ReferenceIdValueSchema`
was `z.string().min(1)`, and in a SQL deployment a legacy embedded reference
reaches storage as JSON TEXT — a non-empty string. So a deployment carrying
exactly the values the gate exists to find ran the scan, was told it was clean,
and closed the gate with `--apply`; and because the scan deliberately imports
the write-path predicate, the write path was equally blind, so the value also
survived future writes.
`ReferenceIdValueSchema` now rejects a value whose first non-space character is
`{` or `[`, in the expanded form too — `$expand` produces an object, never its
serialization.
Deliberately narrower than an id charset. `FileReferenceIdValueSchema` can
bound its alphabet because a `sys_file` id is minted by the platform and
nothing else; a reference id is whatever the target object's key holds,
including an external key an ADR-0015 federated datasource supplies. So this
rejects the shape that is provably not an id and leaves the alphabet to the
object that owns it — `CB0-2026-0001`, `SFDC:001xx…` and `ops/eu-west/tenant-7`
stay valid, and the tests pin that.
Regression coverage is at the GATE, not just the schema: the scan test plants
the serialized embedded record, asserts it is counted, and asserts
`valueShapeScanPassed()` is false — the deployment may not record the flag —
then asserts the same value is a write rejection under strict, so the scan and
the validator still answer with one predicate.
Reaches authors through the ADR-0104 warn-first path until a deployment opts
into strict, so nothing starts rejecting writes on upgrade.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
* fix(metadata-protocol): one canonical type key at the /meta boundary (#4432)
#3985 taught the per-type gates to accept both spellings of the `/meta` type
segment. It did not FOLD them, so `/meta/actions/x` and `/meta/action/x`
addressed two namespaces and the layers below disagreed about which one an item
lived in — `SysMetadataRepository` folded to singular on its own, while the
authorization tier above it (`isOverlayAllowed`, `isArtifactBacked`), the
registry heal below it (`restoreArtifactRegistryView`) and the list hydration
all read the caller's spelling.
The damaging half was the hydration. `getMetaItems` registered overlay rows back
into the SchemaRegistry under `request.type`, so one plural-spelled read minted a
PLURAL registry entry; from the next read on `listItems('actions')` was no longer
empty, the singular fallback that had been supplying every code-authored action
stopped running, and one overlay row hid the entire code-authored listing — on a
spelling no DELETE addresses, so it outlived the delete that was meant to lift
it and left listing and dispatch disagreeing about a removed item.
`saveMetaItem`, `getMetaItem`, `getMetaItems`, `getMetaItemLayered`,
`getMetaItemCached` and `deleteMetaItem` now fold the type to its canonical
singular as their first act. Reads of data AT REST keep the other-spelling
fallback: rows written under a plural `type` before this fix are real and
nothing rewrites them on upgrade. What changed is that nothing WRITES or
REGISTERS a non-canonical key any more.
Regression tests fail without the fold: a plural-spelled read mints a phantom
`actions` registry entry and the second read drops the code-authored actions,
and `getMetaItem` echoes back the caller's spelling so a client can round-trip
it into a second namespace.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
* fix(objectql,service-datasource,runtime): a datasourceMapping rule is routing, not a hint (#4462)
Measured on `main` during the v17 verification: map an object to a Postgres
datasource with a bad URL and the boot SUCCEEDS, `/ready` answers 200, the
datasource name appears in ZERO log lines, the write returns 201 — and the row
is physically in the DEFAULT store. The operator finds out by opening the
database they declared and finding it empty.
Two causes, one per layer, and each is what makes fixing the other correct:
* `ObjectQLEngine.getDriver` step 2 read `mapped && drivers.has(mapped)`, so a
MATCHED mapping rule naming a datasource with no live driver fell silently
through to the default driver. It now throws — `DatasourceUnavailableError`
when the connect layer recorded a verdict (#3828), otherwise an error naming
the object, the datasource and the two remedies. `default` still resolves
onward: the default driver keeps its natural name (#3826), so
`drivers.has('default')` is false by construction and step 5 IS how routing
to it works.
* ADR-0062 D2's phase-1 note deliberately excluded "mapped" from the
auto-connect gate, to keep `examples/app-crm` byte-for-byte unchanged. That
note priced only one side. Gate (d) now fires when a mapping rule routes at
least one object to a datasource, and a `declared-auto` failure is FATAL with
an operator-readable reason — the same call gate (b) already makes for an
explicit `object.datasource` binding, correct for (d) now that routing no
longer supplies a fallback. `OS_ALLOW_DRIVER_CONNECT_FAILURE` still degrades.
The mapped-object list comes from the engine's own matcher
(`ObjectQLEngine.resolveMappedDatasource`, newly public) via
`connectDeclared({ mappedObjects })`. The connection service never re-derives
rule matching: two matchers drifting by one clause would connect a datasource
routing never uses, or route to one nothing connects — the defect again.
`examples/app-crm`'s mapping is DELETED, and that is what keeps the example
unchanged rather than what breaks it. Its `namespace: 'crm'` rule never matched
(`namespace` is deprecated; no object sets it) and its `default: true →
crm_primary` rule routed everything to an unconnected `:memory:` datasource,
i.e. to the default store by fall-through. Honouring it would move the whole
app — platform objects included — onto a database empty on every boot.
Verified against a real boot on a private port, not only in unit tests:
* unchanged CRM example boots healthy; crm_primary/crm_analytics stay
`unvalidated` (metadata-only) exactly as before;
* with a mapping to `postgres://…@127.0.0.1:1/nonexistent_db`, boot exits 1
with "1 object(s) are routed to it by a datasourceMapping rule (crm_account)
and have no fallback datasource — their reads/writes would otherwise land in
a DIFFERENT database than the one they declare ⇒ fail-fast per ADR-0062 D5";
* under OS_ALLOW_DRIVER_CONNECT_FAILURE=1 the degraded-boot banner carries the
same sentence and the mapped object's seeds fail instead of silently
populating the default store.
ADR-0062 D2 carries the amendment; the docs page and the data skill now state
that a mapping rule is routing and fails the boot when it cannot be honoured.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEHJN2NFpS9VMeURvakgD
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent fd3013a commit 63b33e6
20 files changed
Lines changed: 977 additions & 53 deletions
File tree
- .changeset
- content/docs/data-modeling
- docs/adr
- examples/app-crm
- packages
- cli/src
- commands
- lint
- metadata-protocol/src
- objectql/src
- validation
- runtime/src
- services/service-datasource/src
- __tests__
- spec/src/data
- skills/objectstack-data/rules
| 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 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 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 | + | |
| 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 | + | |
| 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 | + | |
| 36 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
111 | | - | |
| 111 | + | |
| 112 | + | |
112 | 113 | | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
117 | 127 | | |
118 | 128 | | |
119 | 129 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
80 | 89 | | |
81 | 90 | | |
82 | 91 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
59 | 72 | | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | 73 | | |
65 | 74 | | |
66 | 75 | | |
| |||
0 commit comments