Skip to content

Commit 17eeb5d

Browse files
committed
Merge origin/main into claude/action-body-write-lint-4271
Conflict in validate-hook-body-writes.ts, resolved in main's favour and then some. #4339 replaced this rule's hand-copied `SYSTEM_FIELDS` list with `IMPLICIT_FIELDS` — derived from the spec via the package-shared `system-fields.ts` (#4330) — which supersedes the `BODY_WRITE_SYSTEM_FIELDS` rename this branch had introduced for the same reason. So the branch drops its rename and exports `IMPLICIT_FIELDS` instead: the action rule shares the derived set rather than the hand-copied one, which is strictly the outcome both changes were after.
2 parents 4c64648 + 667b83e commit 17eeb5d

92 files changed

Lines changed: 2904 additions & 600 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/action-body-write-set-lint.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ every exclusion is proved to be about applicability rather than an
4848
unextractable shape: the shared extractor still sees it, and this rule still
4949
reports nothing for it.
5050

51-
One extractor, one field index, one system-column set, shared with the hook
52-
rule rather than copied — two copies would drift into two different answers
53-
about what a system column is.
51+
One extractor, one field index, one implicit-field set, shared with the hook
52+
rule rather than copied. The action rule is the same check on the other body
53+
surface, so a second copy of `IMPLICIT_FIELDS` would drift exactly the way the
54+
five hand-copied system-field lists #4330 collapsed did.
5455

5556
The lint stays off the kernel boot path, and lands one notch tighter than the
5657
hook side: the only applicable pattern is rooted at `ctx.api`, so an action
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/service-automation": minor
3+
"@objectstack/spec": minor
4+
---
5+
6+
feat(automation,spec): flow executors `parse()` their config, and undeclared config keys reject at registration (#4277)
7+
8+
The #4045 reconciliation left every flat builtin with a Zod config contract that
9+
nothing enforced, and #4059 left `registerFlow` warning about undeclared keys it
10+
could not yet safely reject. #4277 installs both halves of the enforcement:
11+
12+
**1. Executors parse their config (execute time).** The 12 contract-carrying
13+
builtins — `get_record` / `create_record` / `update_record` / `delete_record`,
14+
`screen`, `map`, `notify`, `http`, `loop` / `parallel` / `try_catch` — now run
15+
`node.config` through their Zod contract before executing
16+
(`service-automation/builtin/parse-config.ts`). A type or missing-`required`
17+
violation refuses the node as a **guard** (`errorClass: 'guard'`, not routable
18+
via `fault` edges — config is metadata; re-running changes nothing), naming
19+
every violated path. `{token}` templates stay legal: string-typed slots parse
20+
the raw template, and `http` — whose executor reads the interpolated config —
21+
parses POST-interpolation, where a whole-token template has already resolved to
22+
its value's real type. Exemption: a legacy flat-graph `loop` (no `config.body`)
23+
predates the ADR-0031 construct and is not parsed.
24+
25+
**2. Undeclared config keys are rejected at `registerFlow` (registration
26+
time).** The #4059 warning is now an error: a config key the node type's
27+
descriptor `configSchema` does not declare fails registration, with the exact
28+
path, the declared key set, a did-you-mean, and — for keys with documented
29+
history (`screen.visibleIf`, `create_record`/`update_record.fieldValues`) — a
30+
per-key tombstone (the `UNKNOWN_KEY_GUIDANCE` pattern). Unchanged exemptions:
31+
`assignment` is exempt wholesale (its top-level keys ARE the author's variable
32+
names), schemaless types (`decision`/`script`/`wait`/`subflow`/
33+
`connector_action`) declare nothing so nothing can be undeclared, and keyValue
34+
maps stop the walk (their keys are author data). Every `registerFlow` call site
35+
already try/catches per flow, so a bad stored flow is skipped loudly at boot,
36+
never a crashed kernel.
37+
38+
**Contract fix folded in:** `LoopConfigSchema.collection` is now
39+
`z.union([z.string().min(1), z.array(z.unknown())])` — the executor has always
40+
accepted an inline array (shared resolve logic with `map.collection`, which
41+
already declared the union), so the string-only declaration under-declared what
42+
it reads.
43+
44+
**Migration.** If a flow stops registering: the error names the undeclared key
45+
and its path — rename it to the declared key it meant (`visibleIf`
46+
`visibleWhen`, `fieldValues``fields`), or delete it (an undeclared key was
47+
never read, so removing it changes no behavior). If an executor of yours
48+
genuinely reads the key, declare it on the node type's descriptor
49+
`configSchema`. If a node starts refusing at run time: the refusal names each
50+
violated path against the contract — fix the value's type or supply the missing
51+
required key (e.g. `get_record` `limit` must be a number; `screen`
52+
`fields[].options` entries are `{ value, label }` objects; `notify` requires
53+
`recipients` + `title`). Retry-policy defaults now come from the contract: a
54+
`try_catch` `retry` block that omits `retryDelayMs` gets the documented 1000ms
55+
base delay where the executor historically used 0.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): the seven system-field exemption lists derive from the spec's declarations (#4330)
6+
7+
Five rules in `@objectstack/lint` each carried their own hand-copy of
8+
"registry-injected columns present on almost every object but absent from
9+
authored `fields`" — and they had already drifted from one another (two more
10+
copies had appeared by the time the fix landed). This is the shape #3786
11+
removed from the audit-provenance family, rebuilt one package over: the same
12+
list, maintained in parallel, each under a comment asking to be kept in sync
13+
with one of the others.
14+
15+
The package now has one module, `system-fields.ts`, whose `SYSTEM_FIELDS` is
16+
DERIVED from the spec's two declarations — `FIELD_GROUP_SYSTEM_FIELDS`
17+
(`@objectstack/spec/data`) and `SystemFieldName` (`@objectstack/spec/system`)
18+
— and all seven field-resolving rules consume it. A pin test holds the
19+
boundary in both directions: the set contains exactly the two declarations'
20+
union, and none of the rule-local exemptions.
21+
22+
Two deliberate behavior consequences, both in the permissive direction the
23+
rules' own comments argue for (over-inclusion costs at worst a missed
24+
warning; under-inclusion costs a false one):
25+
26+
- `widget-bindings`, `page-field-bindings` and `react-page-props` now also
27+
exempt `is_deleted`;
28+
- `flow-template-paths` now also exempts `user_id`.
29+
30+
Names that are NOT system columns in the spec's sense (`name`, `owner`,
31+
`record_type`, and the legacy physical spellings `_id` / `space`) stay
32+
rule-local next to the reason each rule exempts them, instead of widening
33+
every rule: `name` in particular is an ordinary authored field on most
34+
objects, and exempting it package-wide would stop the field-existence rules
35+
from catching a reference to a field the object genuinely does not have.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): `<ListView searchableFields>` on a react page is checked against
6+
the bound object's fields (#4329)
7+
8+
#4328's `searchable-field-unknown` gates a stale `searchableFields` entry on
9+
the metadata surfaces — an object's own ADR-0061 declaration, its built-in
10+
named list views, and a `defineView` aggregate's default `list` / named
11+
`listViews`. It did not cover the react page surface: `ListView` declares
12+
`searchableFields` as a dataProp, so a `kind:'react'` page could write
13+
`<ListView searchableFields={['renamed_field']}>` and nothing resolved the
14+
name. The failure is the one #4328 documents — the engine's
15+
`resolveSearchFields` silently filters the stale name out, so the search scans
16+
a narrower set than the page asked for, or (once every entry is stale) falls
17+
through to the auto-default and scans a wider one; and once the REST read path
18+
validates the `$searchFields` override (#4254), the prop objectui echoes
19+
verbatim becomes a `400 INVALID_FIELD` on that list.
20+
21+
The check lives in `validate-react-page-props` — the gate that already parses
22+
the page's real JSX — and runs on `<ListView>` usages whose `objectName` and
23+
`searchableFields` are static literals, under the same rule id and severity
24+
(`searchable-field-unknown`, `error`) as the metadata surfaces. It is not a
25+
re-implementation: `validate-searchable-fields` now exports its core
26+
(`indexObjectSearchTargets` + `checkSearchableFieldList`), and the react gate
27+
runs that, so the two surfaces agree on what counts as a field by construction
28+
— same three skips (an object this stack does not define, an object with no
29+
authored field map, registry-injected system columns derived from the spec's
30+
own declarations), same dotted-path strictness (search matches the field map
31+
by exact string, so `owner_id.name` is flagged, not exempted).
32+
33+
JSX-specific seams follow the gate's existing rules: a value that comes from a
34+
variable, a call, or a spread is not knowable at build time and is skipped
35+
silently — an unresolvable binding is not a wrong one (ADR-0072 D1).
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
'@objectstack/spec': major
3+
---
4+
5+
**BREAKING**: the legacy `_dev: true` service marker is retired. `readServiceSelfInfo()`
6+
now reads exactly one marker — the standard `__serviceInfo` descriptor — and the
7+
`SERVICE_DEV_MARKER_KEY` export is removed.
8+
9+
FROM → TO, for any service that self-identifies as not-fully-real:
10+
11+
```ts
12+
// FROM — normalized to { status: 'stub', handlerReady: false }
13+
const svc = { _dev: true, chat };
14+
15+
// TO — say which kind of unreal it is
16+
const svc = {
17+
__serviceInfo: {
18+
status: 'stub', // 'stub' = fabricates answers | 'degraded' = really serves, reduced capability
19+
message: 'Development stub — register <PluginName> for a real implementation',
20+
},
21+
chat,
22+
};
23+
```
24+
25+
`handlerReady` defaults to `false` for `stub` and `true` for `degraded`; set it
26+
explicitly when the slot has no HTTP surface at all (`cache` / `queue` / `job`).
27+
28+
**Why it matters if you skip the migration:** a service still carrying `_dev: true`
29+
reads as *unmarked* — i.e. as fully real — so discovery will report it
30+
`status: 'available', handlerReady: true`, and dispatcher domains will call it
31+
instead of refusing it. That is the "fake reported as real" failure ADR-0076 D12
32+
exists to prevent, so migrate rather than leave the marker in place.
33+
34+
Removing rather than aliasing is deliberate: a boolean cannot express the
35+
`stub` / `degraded` split every consumer gates on (a stub's domain refuses it, a
36+
degraded implementation's domain keeps serving it). No producers remained in this
37+
repo when the reader was deleted — plugin-dev's stub table was retired in
38+
ADR-0115, and the kernel's in-memory fallbacks moved onto the descriptor in the
39+
same lineage.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/runtime": major
3+
"@objectstack/cli": patch
4+
---
5+
6+
feat(runtime)!: retire the inert `DriverPluginOptions``DriverPlugin` takes `(driver, driverName?)` (#4320)
7+
8+
`new DriverPlugin(driver, { datasourceName, registerAsDefault })` never did
9+
what it promised: both options configured a datasource-registration block in
10+
`start()` gated on `metadata.addDatasource`, a method **no metadata service
11+
implements** — so the block early-returned on every boot since inception and
12+
the options were dead weight (found while typing service lookups for #4251).
13+
14+
**Migration** — delete the options argument; nothing changes at runtime
15+
because nothing ever happened:
16+
17+
- FROM `new DriverPlugin(driver, { datasourceName: 'x', registerAsDefault: false })`
18+
TO `new DriverPlugin(driver)`
19+
- FROM `new DriverPlugin(driver, 'name', options)` TO `new DriverPlugin(driver, 'name')`
20+
- The string second argument (`new DriverPlugin(driver, 'memory')`) is unchanged.
21+
22+
If you passed `datasourceName` expecting routing to a named auxiliary driver:
23+
that routing never came from the option. It keys off the **driver name**
24+
`DriverPlugin.init()` registers `driver.<name>`, ObjectQL's discovery loop
25+
adopts it, and the engine's lifecycle/datasource resolution looks the name up
26+
(see the telemetry provision in `os serve` for the pattern: stamp
27+
`driver.name`, register the plugin, done). For Setup → Datasources visibility,
28+
declare the datasource through `DatasourceConnectionService` /
29+
`registerInMemory('datasource', …)` (ADR-0062).
30+
31+
The `DriverPluginOptions` interface was module-local (never exported from the
32+
package root), so the only public break is the constructor's second/third
33+
argument shape.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/service-automation": patch
4+
---
5+
6+
feat(spec,automation): publish executor-derived config contracts for the schemaless flow nodes (#4278)
7+
8+
The five descriptor-schemaless builtins (`decision` / `script` / `subflow` /
9+
`wait` / `connector_action`) deliberately publish no `configSchema`, so their
10+
Studio form lives only in objectui's hand-written `FLOW_NODE_CONFIG` table —
11+
and nothing reconciled that table against the executors. `script` had drifted:
12+
the form offered an `outputVariables` key nothing reads, two `actionType`
13+
options (`sms` / `notification`) that fail every run, a no-op default (`code`),
14+
and could not author the `function` / `inputs` / `outputVariable` path that
15+
works.
16+
17+
New in `@objectstack/spec/automation` — contract exports only. Unlike their
18+
`builtin-node-config.zod.ts` siblings, which #4277 wired into execute-time
19+
parsing, no engine path `parse()`s node config with these: `script`'s legal key
20+
set depends on `actionType` and `decision` may branch purely on edge
21+
predicates, so a flat parse would either reject valid shapes or check nothing.
22+
Their enforcement is the objectui reconciliation test.
23+
24+
- `ScriptConfigSchema` / `SubflowConfigSchema` / `DecisionConfigSchema` (+
25+
`DecisionConditionSchema`) — written from the executors in
26+
`service-automation`, the machine-readable half of the cross-repo
27+
reconciliation objectui's `flow-node-config` test now performs. `wait` and
28+
`connector_action` need no new schema — their contracts are the existing
29+
`FlowNodeSchema` sibling blocks (`waitEventConfig` / `connectorConfig`).
30+
- `SCRIPT_BUILTIN_ACTION_TYPES` (`['email', 'slack']`) and
31+
`SCRIPT_INVOKE_FUNCTION_ACTION_TYPE` (`'invoke_function'`) — the `script`
32+
executor now builds its dispatch set from the published constant, so the
33+
designer's options, the dispatch set, and the "not a built-in action"
34+
failure message can no longer disagree.
35+
36+
Undeclared-alias graduation in the same change (Prime Directive #12, the
37+
`map.flow` path): the `subflow` executor's bare `cfg.flowName ?? cfg.flow`
38+
fallback is deleted, replaced by the ADR-0087 D2 conversion
39+
`flow-node-subflow-flow-alias` — a stored `subflow` node authored with
40+
`config.flow` is rewritten to the canonical `config.flowName` at load
41+
(including the `AutomationEngine.registerFlow` rehydration seam). FROM
42+
`config.flow` TO `config.flowName`; one-line fix for hand-maintained sources:
43+
rename the key.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/runtime": patch
4+
---
5+
6+
fix(spec,runtime): the service-lookup `any` guard now sees the type-argument form, and its scope stops at nothing under `packages/` (#4251)
7+
8+
The #4127/#4214 rule banned `: any` and `as any` on a service-lookup result but
9+
not `getService<any>('data')` — the form the codebase actually used (80 sites,
10+
zero matches), erasing the slot contract identically. And the rule's `files`
11+
covered only `packages/runtime`, leaving the composition roots (rest,
12+
plugins/*, services/*) that hold most lookups unlinted. Both gaps closed: a
13+
third AST selector catches the type-argument form, the scope is now all of
14+
`packages/`, and the 40 not-yet-swept files are grandfathered in a visible,
15+
shrinking ratchet list (`SLOT_LOOKUP_UNSWEPT`) — enumerated at 180 sites by
16+
running the widened rule with the list emptied. `http.server` joins
17+
`UNCONTRACTED_SLOTS` (three providers, no written contract).
18+
19+
Typing the three in-scope runtime sites surfaced its first yield: both
20+
`addDatasource` datasource-registration branches (DefaultDatasourcePlugin,
21+
DriverPlugin) probed a method **no metadata service implements**, so they had
22+
never run on any boot — deleted rather than typed against a phantom shape. The
23+
inert `DriverPluginOptions` they configured are tracked in #4320.
24+
`registerInMemory('datasource', …)` is the actual visibility path (#3827).
25+
26+
Contract members declared from evidence, both optional: `IDataEngine` gains
27+
`getDefaultDriverName?()` / `getDriverByName?()` (ObjectQL's driver registry —
28+
the surface `os migrate` and serve's storage detection reach through
29+
`driver.<name>` services), `IMetadataService` gains `registerInMemory?()`
30+
(MetadataManager's boot-time seeding primitive). Callers that supplied `<any>`
31+
to these lookups should pass the slot's contract type instead — or nothing:
32+
an unmapped slot deliberately resolves to `unknown`, not `any`.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
ci/test: #4250 stall forensics (SIGUSR2 stack harvest in run-with-stall-guard) + objectql kernel tests stop intercepting worker SIGTERM. Releases nothing.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): read decorations stop round-tripping into persisted metadata bodies (#4326)
6+
7+
`getMetaItem` / `getMetaItems` decorate every served document with
8+
`_diagnostics` (and `_draft` on preview reads), while the write path persists
9+
the request body **verbatim** by design (ADR-0005 §Validation — `parsed.data`
10+
would strip Studio-only auxiliary fields). Nothing stripped the decorations in
11+
between, so the standard designer round-trip — GET the served document, edit a
12+
field, PUT the whole body back — baked a stale read-time verdict into
13+
`sys_metadata.metadata`, into its checksum, and into every history diff.
14+
15+
It was never user-visible: reads recompute `_diagnostics` and the fresh verdict
16+
shadows the persisted one. What it corrupted was the stored bytes — a
17+
decoration-only re-save moved the content checksum, and history diffs carried
18+
diagnostic noise no author wrote.
19+
20+
`saveMetaItem` now strips `_diagnostics` and `_draft` from the body before the
21+
destructive-change diff, the schema gate, the authoring gate, and persistence
22+
(new `stripReadDecorations`, exported for tests). A **silent** strip, unlike the
23+
neighbouring layered-envelope rejection: those keys are our own decoration
24+
riding on a document that is otherwise exactly what the author edited, so
25+
rejecting the round-trip would be hostile. The ADR-0010 protection envelope
26+
(`_lock`, `_lockReason`, `_provenance`) and `_packageId` are deliberately left
27+
alone — envelope state the write path legitimately carries, not read decoration.
28+
29+
Also documents the #3903 conversion boundary on `SysMetadataRepository.get`:
30+
its body stays verbatim because every caller wants the bytes a hash was
31+
computed over (parent-version lineage, existence probes) or is diffing against
32+
equally-verbatim history rows — conversion belongs one layer up, at the
33+
protocol's serving seams.

0 commit comments

Comments
 (0)