@objectstack/lint 17.0.0-rc.0 → 17.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3627 -0
- package/dist/index.cjs +1836 -863
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +297 -61
- package/dist/index.d.ts +297 -61
- package/dist/index.js +1791 -840
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,3627 @@
|
|
|
1
|
+
# @objectstack/lint
|
|
2
|
+
|
|
3
|
+
## 17.0.0-rc.1
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6a67d7a: feat(lint): L2 action-body writes to undeclared fields warn at author time (#4271)
|
|
8
|
+
|
|
9
|
+
The write-set lint that #4305 gave L2 hook bodies now covers the other surface
|
|
10
|
+
that carries one. An action body is the same artefact: the same
|
|
11
|
+
`HookBodySchema` union, parsed by the same `HookBodySchema.safeParse` in
|
|
12
|
+
`actionBodyRunnerFactory`, run in the same QuickJS sandbox. So it fails the
|
|
13
|
+
same way — `ctx.api.object('crm_deal').update({ stag: 'won' })` inside an
|
|
14
|
+
action reaches the driver unfiltered, and the outcome splits by driver: on SQL
|
|
15
|
+
the stray column fails the whole call with a driver-level error far from the
|
|
16
|
+
authoring site, and on a schemaless driver the stray key is persisted. Half
|
|
17
|
+
the surface was still blind.
|
|
18
|
+
|
|
19
|
+
**New rule — `action-body-write-unknown-field` (advisory).** Wired into
|
|
20
|
+
`REFERENCE_INTEGRITY_RULES`, so `os validate`, `os lint` and `os compile` all
|
|
21
|
+
report it; it never blocks a build. Both places the runtime reads actions from
|
|
22
|
+
are walked — top-level `actions` and `objects[].actions` — and a
|
|
23
|
+
`defineStack`-merged action, which lives in both, is reported once at its
|
|
24
|
+
authored path. That dedupe is by VALUE (bound object + name + body source), not
|
|
25
|
+
by object identity the way `collectBundleActions` can afford: the suite runs on
|
|
26
|
+
the schema-PARSED stack, and parsing rebuilds every node, so the two copies
|
|
27
|
+
arrive as distinct objects that are merely equal. An identity check passes a
|
|
28
|
+
shared-reference unit fixture and then reports the showcase app's one warning
|
|
29
|
+
twice — which is exactly what it did before the end-to-end run caught it.
|
|
30
|
+
|
|
31
|
+
**Only the `ctx.api` write family carries over, and that is the point.** An
|
|
32
|
+
action's `ctx.input` is its PARAMS bag (`input: unwrapProxyToPlain(actionCtx
|
|
33
|
+
?.params)`), not a record, so resolving those names against object fields would
|
|
34
|
+
flag every correctly-named parameter — a pure false-positive machine, and a
|
|
35
|
+
false positive kills an advisory lint. `ctx.record` is not a write surface
|
|
36
|
+
either: the runner hands the body a plain snapshot and never writes it back, so
|
|
37
|
+
`ctx.record.x = …` is discarded for _declared_ and undeclared fields alike —
|
|
38
|
+
a different defect from "the unknown column vanishes", and flagging only its
|
|
39
|
+
undeclared half would imply the declared half persists.
|
|
40
|
+
|
|
41
|
+
So the rule ships a declared **partition** of the shared
|
|
42
|
+
`HOOK_BODY_WRITE_PATTERNS` rather than a second ledger:
|
|
43
|
+
`ACTION_BODY_WRITE_PATTERN_IDS` (today: `api-crud-literal`) and
|
|
44
|
+
`ACTION_BODY_WRITE_EXCLUSIONS` (`input-property-assign`,
|
|
45
|
+
`input-object-assign`), each exclusion carrying its reason. The two halves are
|
|
46
|
+
tested to cover the shared ledger exactly, so a fourth pattern landing on the
|
|
47
|
+
hook side fails this rule's test until someone classifies it — silence is not a
|
|
48
|
+
decision. Every applicable pattern is additionally proved end-to-end through
|
|
49
|
+
the full validator (prefilter, pattern filter and field check included), and
|
|
50
|
+
every exclusion is proved to be about applicability rather than an
|
|
51
|
+
unextractable shape: the shared extractor still sees it, and this rule still
|
|
52
|
+
reports nothing for it.
|
|
53
|
+
|
|
54
|
+
One extractor, one field index, one implicit-field set, shared with the hook
|
|
55
|
+
rule rather than copied. The action rule is the same check on the other body
|
|
56
|
+
surface, so a second copy of `IMPLICIT_FIELDS` would drift exactly the way the
|
|
57
|
+
five hand-copied system-field lists #4330 collapsed did.
|
|
58
|
+
|
|
59
|
+
The lint stays off the kernel boot path, and lands one notch tighter than the
|
|
60
|
+
hook side: the only applicable pattern is rooted at `ctx.api`, so an action
|
|
61
|
+
body that never mentions it does not even parse, let alone load the ~9 MB
|
|
62
|
+
TypeScript compiler. Guarded by `lazy-deps.test.ts`.
|
|
63
|
+
|
|
64
|
+
`@objectstack/spec`: `ScriptBodySchema` and `ActionSchema.body` now point at
|
|
65
|
+
the action-side rule and spell out that `ctx.input` (params) and `ctx.record`
|
|
66
|
+
(a discarded snapshot) are not record-write surfaces — doc comments only, no
|
|
67
|
+
schema or generated-artifact change.
|
|
68
|
+
|
|
69
|
+
- 0ecc656: feat(lint): an action body's discarded `ctx.record` write warns at author time (#4345)
|
|
70
|
+
|
|
71
|
+
`#4344` deliberately left `ctx.record` alone, and said why: an action's
|
|
72
|
+
`ctx.record` is a plain snapshot (`unwrapProxyToPlain(actionCtx?.record)`) that
|
|
73
|
+
`boundActionHandler` never writes back — the hook path's
|
|
74
|
+
`applyMutationsToInput` has no action-side counterpart — so `ctx.record.x = …`
|
|
75
|
+
is discarded for **declared and undeclared fields alike**. Reporting that
|
|
76
|
+
through the unknown-field rule would have been actively wrong: flagging only
|
|
77
|
+
the undeclared half implies the declared half persists, which is the false
|
|
78
|
+
completion this rule family exists to stop manufacturing. It needed its own
|
|
79
|
+
finding, and now has one.
|
|
80
|
+
|
|
81
|
+
**New rule — `action-record-write-discarded` (advisory).**
|
|
82
|
+
|
|
83
|
+
**It is not "flag every `ctx.record.<field>` assignment"** — that would be a
|
|
84
|
+
false-positive machine, because mutating the snapshot to build a payload is a
|
|
85
|
+
legitimate idiom:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
ctx.record.stage = "won";
|
|
89
|
+
await ctx.api.object("crm_deal").update(ctx.record); // the write is LIVE
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
So the finding requires the write to be **provably dead**: reported only when
|
|
93
|
+
`ctx.record` never escapes the body as a value. Property reads
|
|
94
|
+
(`ctx.record.id`) do not rescue a write and do not suppress the finding;
|
|
95
|
+
handing the object to anything — an argument, an assignment RHS, a spread, a
|
|
96
|
+
return — does. Aliasing (`const r = ctx.record`) reads as an escape, which is
|
|
97
|
+
the safe direction: it costs a missed finding, never a false one.
|
|
98
|
+
|
|
99
|
+
Truthiness and type tests are **not** escapes, and that distinction is what
|
|
100
|
+
makes the rule fire on real code rather than almost never. Running it against
|
|
101
|
+
the showcase app is what surfaced it: `mark_done` opens with
|
|
102
|
+
`ctx.recordId || (ctx.record && ctx.record.id)`, the defensive idiom action
|
|
103
|
+
bodies are actually written with, and counting that guard as an escape silenced
|
|
104
|
+
the finding on the one body in the repo that had a record write. A test reads
|
|
105
|
+
the reference and yields a boolean — or, for `&&`/`||`/`??`, yields the left
|
|
106
|
+
operand only when it is falsy, which is null or undefined and persists nothing.
|
|
107
|
+
Only the LEFT operand is a test: `x || ctx.record` really does evaluate to the
|
|
108
|
+
object, and still escapes.
|
|
109
|
+
|
|
110
|
+
**One suite member, two rule ids.** Both findings fall out of one parse of one
|
|
111
|
+
source on one surface, so `validateActionBodyWrites` reports both rather than
|
|
112
|
+
`REFERENCE_INTEGRITY_RULES` growing a second member that would parse every
|
|
113
|
+
action body again to say two things about the same walk. The alternative —
|
|
114
|
+
hand-wiring it into the three CLI commands — is the drift that suite exists to
|
|
115
|
+
end, and `validateReadonlyFlowWrites` is the standing proof: wired into
|
|
116
|
+
`validate` and `compile`, never into `lint`. The trade-off is written down at
|
|
117
|
+
both ends rather than left to be rediscovered.
|
|
118
|
+
|
|
119
|
+
**The ledger ratchet fired, as designed.** `record-property-assign` joins the
|
|
120
|
+
shared `HOOK_BODY_WRITE_PATTERNS` — the extractor's shape inventory, not any
|
|
121
|
+
one rule's — and both existing consumers had to classify it before it could
|
|
122
|
+
land. That was not cosmetic on the hook side: a `record-property-assign` write
|
|
123
|
+
carries no `object`, and `validateHookBodyWrites` branched on exactly that to
|
|
124
|
+
mean "a `ctx.input` write", so the new shape would have been reported as _"the
|
|
125
|
+
hook writes 'stage' to its input"_. The hook rule now declares its own
|
|
126
|
+
consumed subset (`HOOK_BODY_WRITE_PATTERN_IDS`) and its exclusion with a
|
|
127
|
+
reason — a hook sandbox context has no `ctx.record` at all
|
|
128
|
+
(`buildSandboxContext` never sets it), so the expression throws at run time
|
|
129
|
+
rather than silently no-op'ing, and a loud failure is not an advisory rule's
|
|
130
|
+
business.
|
|
131
|
+
|
|
132
|
+
`extractHookBodyWriteSet` is the new one-parse entry point, returning the
|
|
133
|
+
writes plus the `ctxRecordEscapes` signal; `extractHookBodyWrites` stays as a
|
|
134
|
+
thin projection of it.
|
|
135
|
+
|
|
136
|
+
**Boot path.** The action gate's prefilter widens from `api` to `api`-or-
|
|
137
|
+
`record`, so a body reaching neither still never loads the ~9 MB TypeScript
|
|
138
|
+
compiler. `lazy-deps.test.ts` pins it — and its header and two case names,
|
|
139
|
+
which still claimed every lazy dep waited on "a react page", now say which
|
|
140
|
+
trigger each one pins (typescript has also been loaded by the hook-body gate
|
|
141
|
+
since #4271).
|
|
142
|
+
|
|
143
|
+
`@objectstack/spec` / `@objectstack/runtime`: `ScriptBodySchema`,
|
|
144
|
+
`ActionSchema.body` and `ScriptContext.record` now state that
|
|
145
|
+
`ctx.api.object(...)` is the only path that persists anything, and that
|
|
146
|
+
`ctx.record` is read-only in effect. Doc comments only — no schema or
|
|
147
|
+
generated-artifact change. Whether the runtime should instead refuse or honour
|
|
148
|
+
a record write stays open on #4345.
|
|
149
|
+
|
|
150
|
+
- e4c61a7: Validate the expression slots a flow node's `configSchema` declares (#4027).
|
|
151
|
+
|
|
152
|
+
A node type's designer `configSchema` and the keys its validators traverse were
|
|
153
|
+
two unreconciled lists. Both the engine's `registerFlow` pass and the author-time
|
|
154
|
+
`objectstack validate` pass hardcoded `config.condition` / `edge.condition` and
|
|
155
|
+
assumed every other node string was a `{var}` template — so a declared expression
|
|
156
|
+
property outside that hardcoded set was validated by nobody.
|
|
157
|
+
|
|
158
|
+
That is how #3528 shipped. `screen.fields[].visibleWhen` has been on the `screen`
|
|
159
|
+
descriptor since #3304, typed `xExpression: 'expression'` (bare CEL) and offered
|
|
160
|
+
to authors in Studio, but no validator traversed it. An app authored the
|
|
161
|
+
predicate in the _other_ dialect — `'{createOpportunity} == true'` — and it passed
|
|
162
|
+
`tsc`, `objectstack validate` and registration in silence. Because `required` _is_
|
|
163
|
+
enforced, a field the author had made conditional rendered unconditionally and
|
|
164
|
+
blocked Submit on an input the user was never shown: the run paused forever and no
|
|
165
|
+
resume was ever issued.
|
|
166
|
+
|
|
167
|
+
Now:
|
|
168
|
+
|
|
169
|
+
- **`FLOW_NODE_EXPRESSION_PATHS`** (`@objectstack/spec`) is the declared ledger of
|
|
170
|
+
expression-bearing node config paths, each recording the dialect it takes.
|
|
171
|
+
- **Both validators read it.** A malformed `visibleWhen` is a located, quoted
|
|
172
|
+
error at `registerFlow` _and_ at `objectstack validate` — `node 'screen_1'
|
|
173
|
+
(screen) screen field visibleWhen at config.fields[1].visibleWhen`.
|
|
174
|
+
- **A reconciliation ratchet** derives the expression properties from the live
|
|
175
|
+
descriptors and fails CI in both directions: a new `xExpression` property with
|
|
176
|
+
no ledger entry, or a stale entry no descriptor declares. It walks every
|
|
177
|
+
registered builtin, not just `screen`.
|
|
178
|
+
|
|
179
|
+
Dialects are recorded rather than assumed because there are three, and two of them
|
|
180
|
+
disagree about braces: bare CEL (`{…}` is the #1491 brace-trap), single-brace
|
|
181
|
+
`{var}` flow interpolation (`{…}` is correct), and the ADR-0032 §3 double-brace
|
|
182
|
+
text template. Only bare-CEL slots are checked — `loop.collection` and
|
|
183
|
+
`map.collection` are recorded as `flow-template` and deliberately left alone,
|
|
184
|
+
since no validator implements their dialect and checking them under either of the
|
|
185
|
+
other two would reject every currently-valid flow.
|
|
186
|
+
|
|
187
|
+
`ActionDescriptor.configSchema`'s TSDoc no longer claims `registerFlow()`
|
|
188
|
+
validates `config` against it. It never did: `FlowNodeSchema.config` is
|
|
189
|
+
`z.record(z.unknown())`, so types, `required`, `enum` and unknown keys are still
|
|
190
|
+
unenforced. The doc now states exactly what is checked and what is designer-facing
|
|
191
|
+
only, so nothing relies on a guard that does not exist.
|
|
192
|
+
|
|
193
|
+
- cc60165: feat(lint): a flow `update_record` node writing an undeclared field gates the build (#4271)
|
|
194
|
+
|
|
195
|
+
The write-set family #4305 (hooks) and #4344 (actions) opened had a third
|
|
196
|
+
surface, and it was the one the docs had spent the longest recommending as the
|
|
197
|
+
safe alternative to the other two. A flow `update_record` node whose
|
|
198
|
+
`config.fields` names a field the target object never declares was caught by
|
|
199
|
+
**nothing**: `validate-readonly-flow-writes.ts` walks that exact map and
|
|
200
|
+
explicitly stepped over the unknown key (`if (!meta) continue; // a
|
|
201
|
+
form/field-layout lint concern` — a referral to a rule that does not check
|
|
202
|
+
writes), and `validate-flow-template-paths.ts` checks the `{record.<path>}`
|
|
203
|
+
READ tokens interpolated into node config, never the write-side key. So the
|
|
204
|
+
surface `hook-bodies.mdx` pointed authors at — "prefer a flow `update_record`
|
|
205
|
+
node, whose structural `fields` config is checked" — was the least checked of
|
|
206
|
+
the three.
|
|
207
|
+
|
|
208
|
+
**New rule — `flow-node-write-unknown-field`, and it is an `error`.** Wired into
|
|
209
|
+
`REFERENCE_INTEGRITY_RULES`, so `os validate`, `os lint` and `os compile` report
|
|
210
|
+
it at once (one more place than the hand-wired readonly rule next door reaches).
|
|
211
|
+
|
|
212
|
+
**Why it gates where its two siblings advise.** The hook and action rules are
|
|
213
|
+
advisory because they PARSE JavaScript: the finding is only as good as the
|
|
214
|
+
extractor, and a false positive kills an advisory lint. Nothing here is parsed —
|
|
215
|
+
`config.fields` is a literal map next to a literal `objectName`, the same
|
|
216
|
+
certainty `flow-update-readonly-field` already gates on one config key over. A
|
|
217
|
+
rule that errors on a write the engine _strips_ while only warning on a write
|
|
218
|
+
that names no column at all would be incoherent in the same `fields` map.
|
|
219
|
+
|
|
220
|
+
And the runtime consequence is not the benign "consumer skips the unknown name
|
|
221
|
+
and renders the rest" that keeps `page-field-unknown` / `form-field-unknown`
|
|
222
|
+
advisory. Both halves were measured, not inferred:
|
|
223
|
+
|
|
224
|
+
- Through the engine, an undeclared key reaches `driver.update` verbatim — the
|
|
225
|
+
flow executor calls the data engine directly, the UPDATE path strips only
|
|
226
|
+
readonly/readonlyWhen, and the SQL driver's `formatInput` /
|
|
227
|
+
`applyWriteColumnMap` pass an unrecognized key straight through (`m[k] ?? k`).
|
|
228
|
+
- On SQLite/knex it becomes `update "deal" set "name" = 'n2', "stagee" = 'won' …
|
|
229
|
+
→ no such column: stagee`. The statement is rejected **whole**: `name` —
|
|
230
|
+
spelled correctly, in the same payload — does not land either, and the step
|
|
231
|
+
fails with a driver error naming a column, far from the authoring mistake.
|
|
232
|
+
- On a schemaless datasource nothing rejects it, so the stray key is persisted
|
|
233
|
+
into a column the object never declares, where no schema-driven read returns
|
|
234
|
+
it.
|
|
235
|
+
|
|
236
|
+
That is the call `validate-searchable-fields` makes for a stale entry and
|
|
237
|
+
`validate-flow-template-paths` makes for a filter-position token: gate when the
|
|
238
|
+
miss breaks or corrupts the operation, advise when it merely narrows the output.
|
|
239
|
+
|
|
240
|
+
**One field index and one implicit-field set across all three surfaces.**
|
|
241
|
+
`indexObjectFields` and `IMPLICIT_FIELDS` are imported from the hook rule rather
|
|
242
|
+
than copied, so the three rules cannot drift on what is writable without being
|
|
243
|
+
authored — the shape #4330 collapsed one package over.
|
|
244
|
+
|
|
245
|
+
Every skip exists so the gate only ever fires on a certainty, and each is
|
|
246
|
+
silent: a templated `objectName`, a non-literal `fields` map, an object this
|
|
247
|
+
stack does not define, an object that declares no fields at all (external /
|
|
248
|
+
datasource-introspected schemas, the same skip `validate-searchable-fields`
|
|
249
|
+
takes), and dotted keys (a nested-path write, not a top-level column). `runAs`
|
|
250
|
+
is deliberately NOT consulted, unlike the readonly rule that skips
|
|
251
|
+
`runAs:'system'` — an elevated identity bypasses the readonly strip, but no run
|
|
252
|
+
identity conjures a column.
|
|
253
|
+
|
|
254
|
+
**Scope is declared as data, not left as silence.** `FLOW_WRITE_NODE_TYPES`
|
|
255
|
+
(today `update_record`) and `FLOW_WRITE_NODE_TYPES_DEFERRED` (`create_record`,
|
|
256
|
+
with its reason) are partition-tested against the CRUD node types that carry a
|
|
257
|
+
`fields` write map — derived behaviourally from the spec's executor-written
|
|
258
|
+
config schemas, not restated — so a node type that grows one later fails that
|
|
259
|
+
test until someone classifies it.
|
|
260
|
+
|
|
261
|
+
`@objectstack/spec`: `ScriptBodySchema`'s "prefer a flow `update_record` node,
|
|
262
|
+
whose structural `fields` config is error-checked" note now names the rule that
|
|
263
|
+
makes it true. Doc comment only — no schema or generated-artifact change.
|
|
264
|
+
|
|
265
|
+
Docs: #4355 had just rewritten `automation/hook-bodies.mdx` to record this gap
|
|
266
|
+
honestly — "**Prefer a flow `update_record` node when the write set is fixed —
|
|
267
|
+
but not for _this_ check** … writing a field the object never declares is
|
|
268
|
+
currently reported by nothing at all. On that one axis an L2 body is now the
|
|
269
|
+
better-checked surface." That bullet, and the matching note in
|
|
270
|
+
`automation/hooks.mdx`, are the two sentences this change makes false. Both now
|
|
271
|
+
say the axis has flipped back — and why the flow side lands a level _stronger_
|
|
272
|
+
than the body side rather than merely level with it.
|
|
273
|
+
|
|
274
|
+
- c1d44f7: feat(lint): L2 hook-body writes to undeclared fields warn at author time (#4271)
|
|
275
|
+
|
|
276
|
+
An L2 (`language:'js'`) hook body that writes a field the target object never
|
|
277
|
+
declares — `ctx.input.amout = 0`, `ctx.api.object('deal').update({ stag: … })`
|
|
278
|
+
— runs clean in the QuickJS sandbox and reaches the driver **unfiltered**:
|
|
279
|
+
`applyMutationsToInput` is a plain `Object.assign`, and the write-path
|
|
280
|
+
validator walks declared fields on insert and skips a key it has no field def
|
|
281
|
+
for on update. What happens next depends on the driver, and neither half is
|
|
282
|
+
acceptable:
|
|
283
|
+
|
|
284
|
+
- **SQL** — the stray column enters the statement and the **whole write fails**
|
|
285
|
+
with a driver-level error (`table deal has no column named stagee`). The
|
|
286
|
+
write is lost, and the error surfaces far from the mistake that caused it.
|
|
287
|
+
- **Schemaless** (memory, MongoDB) — the driver spreads the payload, so the
|
|
288
|
+
stray key **is** persisted: an undeclared column nothing downstream reads.
|
|
289
|
+
|
|
290
|
+
No diagnostic anywhere, and nothing at the authoring site either way — the
|
|
291
|
+
#4001 "the mistake is invisible where it is made" family. The read side
|
|
292
|
+
(`hook.condition`) and the capability surface were already statically checked;
|
|
293
|
+
the write side was the one blind face, and `hook-body.zod.ts` carried it as an
|
|
294
|
+
**accepted gap**.
|
|
295
|
+
|
|
296
|
+
**New rule — `hook-body-write-unknown-field` (advisory).** `@objectstack/lint`
|
|
297
|
+
now parses each L2 body (TypeScript parser; parsed, never executed, never
|
|
298
|
+
type-checked) and resolves its literal writes against the target object's
|
|
299
|
+
declared + system fields. An unknown field warns with a did-you-mean. Wired
|
|
300
|
+
into `REFERENCE_INTEGRITY_RULES`, so `os validate`, `os lint` and `os compile`
|
|
301
|
+
all report it; it never blocks a build.
|
|
302
|
+
|
|
303
|
+
The recognized write shapes are declared as data — `HOOK_BODY_WRITE_PATTERNS`,
|
|
304
|
+
each entry carrying a canonical example that a reconciliation test round-trips
|
|
305
|
+
through the real extractor, so a pattern cannot be declared-but-unverified
|
|
306
|
+
(#3528's death). v1 ships three:
|
|
307
|
+
|
|
308
|
+
- `ctx.input.<field> = …` / `ctx.input['<field>'] ⟨op⟩= …` → the hook's own
|
|
309
|
+
target object(s); flat-input envelope keys (`id`/`options`/`ast`/`data`) are
|
|
310
|
+
never treated as record fields.
|
|
311
|
+
- `Object.assign(ctx.input, { <field>: … })` → same target.
|
|
312
|
+
- `ctx.api.object('<object>').insert|create|update({…})` / `.updateById(id, {…})`
|
|
313
|
+
→ the named object, at the **real** `ObjectRepository` payload positions
|
|
314
|
+
(`update(data)` — the payload is argument 0, not `update(id, data)`).
|
|
315
|
+
|
|
316
|
+
Everything statically unknowable is skipped silently, favouring missed findings
|
|
317
|
+
over false ones: computed keys, spreads, non-literal payloads, dynamic object
|
|
318
|
+
names, wildcard-target (`object:'*'`) input writes, cross-package targets,
|
|
319
|
+
aliased input (`const doc = ctx.input`), and multi-target hooks where the field
|
|
320
|
+
exists on _some_ target (the body may branch per object — only an
|
|
321
|
+
everywhere-miss warns).
|
|
322
|
+
|
|
323
|
+
The lint stays off the kernel boot path: the TypeScript compiler loads lazily,
|
|
324
|
+
only when a hook actually carries a JS body (same contract as the react-page
|
|
325
|
+
gates, guarded by `lazy-deps.test.ts`).
|
|
326
|
+
|
|
327
|
+
`@objectstack/spec`: the `ScriptBodySchema` header's "write-set opacity —
|
|
328
|
+
accepted static-analysis gap" note now points at the lint instead, and spells
|
|
329
|
+
out what remains opaque so the warning's absence is not read as proof of
|
|
330
|
+
correctness.
|
|
331
|
+
|
|
332
|
+
- 3eb1b2b: feat(lint): every field-bearing prop on a React page block resolves against the
|
|
333
|
+
object it names
|
|
334
|
+
|
|
335
|
+
#4329 closed ONE of them — `<ListView searchableFields>` — by running the
|
|
336
|
+
metadata rule's core from the gate that owns React block props. That prop was an
|
|
337
|
+
instance, not the class: every other prop a `kind:'react'` page binds BY FIELD
|
|
338
|
+
NAME shipped exactly as typed, the same silent drift `page-field-unknown`
|
|
339
|
+
already closes for the page-component `properties` bag one surface over.
|
|
340
|
+
|
|
341
|
+
`validate-react-page-props` now resolves all of them:
|
|
342
|
+
|
|
343
|
+
- `<ListView>` `fields` / `columns` / `sort` / `grouping` / `userFilters` /
|
|
344
|
+
`hiddenFields` / `fieldOrder` / `filterableFields`
|
|
345
|
+
- `<ObjectForm>` `fields`, `initialValues` KEYS, `sections[].fields[]`
|
|
346
|
+
- `<RecordHighlights>` / `<RecordDetails>` / `<RecordPath>` /
|
|
347
|
+
`<RecordRelatedList>` — via the SAME `COMPONENT_FIELD_SPECS` table the
|
|
348
|
+
metadata surface uses, keyed by the block's `schemaType`, so the two surfaces
|
|
349
|
+
agree by construction rather than by two lists that happen to match
|
|
350
|
+
- `<Block type="…">` — the escape hatch reaches the same table by the type the
|
|
351
|
+
author writes, so it is checked instead of being a hole
|
|
352
|
+
|
|
353
|
+
Findings carry the metadata rule's id (`page-field-unknown`) at its advisory
|
|
354
|
+
severity, because the consumer behaves the same way: an unknown name is skipped
|
|
355
|
+
and the rest renders.
|
|
356
|
+
|
|
357
|
+
**A FILTER position gates instead.** `<ListView filters>` / `<ObjectChart
|
|
358
|
+
filter>` name fields in a QUERY, and an unknown column there is not a skipped
|
|
359
|
+
column: the predicate can never match, `SqlDriver` swallows the driver's
|
|
360
|
+
"no such column" and returns `[]`, and the surface renders an empty list that
|
|
361
|
+
looks exactly like "there is no data" — the silent zero `filter-token-unknown`
|
|
362
|
+
and `validate-flow-template-paths`' filter-position call both gate on. Those
|
|
363
|
+
are reported as `error`.
|
|
364
|
+
|
|
365
|
+
Filter positions are also resolved INDEPENDENTLY of each other, unlike every
|
|
366
|
+
other value this gate reads. `filters={['status', '=', stage]}` — a static field
|
|
367
|
+
beside a React-state value — is the shape a react page actually writes, and the
|
|
368
|
+
all-or-nothing static reader skipped the whole array, including the one position
|
|
369
|
+
that was knowable.
|
|
370
|
+
|
|
371
|
+
Everything else is unchanged: a value from a variable, a call, or behind a
|
|
372
|
+
spread is unresolvable rather than wrong and is skipped silently (ADR-0072 D1),
|
|
373
|
+
as are cross-package objects, objects with no authored field map, dotted
|
|
374
|
+
relationship paths, and registry-injected system columns.
|
|
375
|
+
|
|
376
|
+
### Breaking: `<RecordRelatedList objectName>` is the RELATED object, as the spec always said
|
|
377
|
+
|
|
378
|
+
`RecordRelatedListProps.objectName` is the related (child) object — that is what
|
|
379
|
+
`record:related_list` means on every metadata surface, what
|
|
380
|
+
`validate-page-field-bindings` resolves its `columns` against, and what the one
|
|
381
|
+
registry component behind both surfaces consumes. The React overlay declared
|
|
382
|
+
`objectName` a SECOND time and glossed it "The parent object", and the generated
|
|
383
|
+
contract publishes the overlay's description in place of the schema's — so the
|
|
384
|
+
react surface both contradicted the spec and lost any way to name the object it
|
|
385
|
+
renders.
|
|
386
|
+
|
|
387
|
+
FROM → TO for a page authored against the old gloss:
|
|
388
|
+
|
|
389
|
+
```diff
|
|
390
|
+
- <RecordRelatedList objectName="account" recordId={id} relationshipField="account_id" columns={['name','total']} />
|
|
391
|
+
+ <RecordRelatedList objectName="invoice" recordId={id} relationshipField="account_id" columns={['name','total']} />
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`objectName` names the CHILD object being listed; the parent record stays bound
|
|
395
|
+
by `recordId`, and `relationshipField` is the child's field pointing back at it.
|
|
396
|
+
The lint above reports the old spelling (the child's columns and its FK do not
|
|
397
|
+
resolve against the parent). `objectName` is now also published as required, as
|
|
398
|
+
the schema declares it.
|
|
399
|
+
|
|
400
|
+
The class is closed as well as the instance: `REACT_OVERLAY_SHADOWS` in
|
|
401
|
+
`@objectstack/spec/ui` ledgers every overlay prop that restates a spec-schema
|
|
402
|
+
prop, and a test asserts the ledger equals the real collision set — so the next
|
|
403
|
+
overlay entry that silently redefines a schema prop fails a test instead of
|
|
404
|
+
shipping a second dialect.
|
|
405
|
+
|
|
406
|
+
- 9555b07: feat(lint): `<ListView searchableFields>` on a react page is checked against
|
|
407
|
+
the bound object's fields (#4329)
|
|
408
|
+
|
|
409
|
+
#4328's `searchable-field-unknown` gates a stale `searchableFields` entry on
|
|
410
|
+
the metadata surfaces — an object's own ADR-0061 declaration, its built-in
|
|
411
|
+
named list views, and a `defineView` aggregate's default `list` / named
|
|
412
|
+
`listViews`. It did not cover the react page surface: `ListView` declares
|
|
413
|
+
`searchableFields` as a dataProp, so a `kind:'react'` page could write
|
|
414
|
+
`<ListView searchableFields={['renamed_field']}>` and nothing resolved the
|
|
415
|
+
name. The failure is the one #4328 documents — the engine's
|
|
416
|
+
`resolveSearchFields` silently filters the stale name out, so the search scans
|
|
417
|
+
a narrower set than the page asked for, or (once every entry is stale) falls
|
|
418
|
+
through to the auto-default and scans a wider one; and once the REST read path
|
|
419
|
+
validates the `$searchFields` override (#4254), the prop objectui echoes
|
|
420
|
+
verbatim becomes a `400 INVALID_FIELD` on that list.
|
|
421
|
+
|
|
422
|
+
The check lives in `validate-react-page-props` — the gate that already parses
|
|
423
|
+
the page's real JSX — and runs on `<ListView>` usages whose `objectName` and
|
|
424
|
+
`searchableFields` are static literals, under the same rule id and severity
|
|
425
|
+
(`searchable-field-unknown`, `error`) as the metadata surfaces. It is not a
|
|
426
|
+
re-implementation: `validate-searchable-fields` now exports its core
|
|
427
|
+
(`indexObjectSearchTargets` + `checkSearchableFieldList`), and the react gate
|
|
428
|
+
runs that, so the two surfaces agree on what counts as a field by construction
|
|
429
|
+
— same three skips (an object this stack does not define, an object with no
|
|
430
|
+
authored field map, registry-injected system columns derived from the spec's
|
|
431
|
+
own declarations), same dotted-path strictness (search matches the field map
|
|
432
|
+
by exact string, so `owner_id.name` is flagged, not exempted).
|
|
433
|
+
|
|
434
|
+
JSX-specific seams follow the gate's existing rules: a value that comes from a
|
|
435
|
+
variable, a call, or a spread is not knowable at build time and is skipped
|
|
436
|
+
silently — an unresolvable binding is not a wrong one (ADR-0072 D1).
|
|
437
|
+
|
|
438
|
+
- 7967133: feat(lint): a `searchableFields` entry naming no field is caught at authoring
|
|
439
|
+
time, not at request time
|
|
440
|
+
|
|
441
|
+
`searchableFields` is `z.array(z.string())` in both `object.zod.ts` and the
|
|
442
|
+
list-view schema, so nothing ever checked that an entry resolves to anything.
|
|
443
|
+
Rename a field and the old name stays behind — Zod-valid, shipped, pointing at
|
|
444
|
+
a column that no longer exists.
|
|
445
|
+
|
|
446
|
+
The engine tolerates it, which is exactly what kept the drift invisible:
|
|
447
|
+
`resolveSearchFields` filters the declaration down to fields that exist
|
|
448
|
+
(`searchableFields?.filter((f) => all[f])`) and says nothing. The tolerance
|
|
449
|
+
fails in the direction nobody expects:
|
|
450
|
+
|
|
451
|
+
- **some entries stale** → `$search` scans a NARROWER set than the object
|
|
452
|
+
declares. Records that should match do not, and the response is
|
|
453
|
+
indistinguishable from "no such record";
|
|
454
|
+
- **every entry stale** → the filtered set is empty, so resolution falls
|
|
455
|
+
through to the AUTO-DEFAULT (name/title + short-text fields). A declaration
|
|
456
|
+
whose whole purpose is to CHOOSE the searchable set ends up selecting one the
|
|
457
|
+
author never wrote — the "asked narrower, answered wider" inversion #4226
|
|
458
|
+
closed on the projection axis.
|
|
459
|
+
|
|
460
|
+
It also stops being quiet downstream. Clients echo the declaration verbatim as
|
|
461
|
+
the `$searchFields` override (objectui's list search sends
|
|
462
|
+
`schema.searchableFields`), so once the REST read path validates that override
|
|
463
|
+
against the object (#4254), a stale entry the engine had been silently skipping
|
|
464
|
+
becomes a `400 INVALID_FIELD` on every list search for that object — a
|
|
465
|
+
request-time break whose cause is an authoring typo made long before.
|
|
466
|
+
|
|
467
|
+
**New rule — `searchable-field-unknown` (gating).** Wired into
|
|
468
|
+
`REFERENCE_INTEGRITY_RULES`, so it runs on `os validate`, `os lint` and
|
|
469
|
+
`os compile` with no CLI edit. It covers the object's own ADR-0061 declaration
|
|
470
|
+
and the list views that narrow it (`objects[].listViews`, a `defineView`
|
|
471
|
+
default `list`, and named `listViews`), resolving each entry against the bound
|
|
472
|
+
object's declared fields.
|
|
473
|
+
|
|
474
|
+
`error`, not the advisory level the other field-existence rules use
|
|
475
|
+
(`page-field-unknown`, `form-field-unknown`, `semantic-role-field-unknown` are
|
|
476
|
+
all warnings). Those describe a consumer that SKIPS an unknown name and renders
|
|
477
|
+
the rest; this describes a declaration that either selects the wrong set or
|
|
478
|
+
refuses the request outright — the same call `validate-flow-template-paths`
|
|
479
|
+
makes for a filter-position token, where the miss widens the query instead of
|
|
480
|
+
shrinking the page.
|
|
481
|
+
|
|
482
|
+
Existence only: a field that exists but is an odd search target (a `json`
|
|
483
|
+
column) is NOT flagged — an explicit `searchableFields` is authoritative, so
|
|
484
|
+
declaring one is a choice, not drift. Three skips keep false positives near zero
|
|
485
|
+
(ADR-0072 D1): an object this stack does not define, an object with no authored
|
|
486
|
+
field map (external / datasource-introspected), and registry-injected system
|
|
487
|
+
columns — the last derived from the spec's own `FIELD_GROUP_SYSTEM_FIELDS` and
|
|
488
|
+
`SystemFieldName` rather than hand-copied, since this package already carries
|
|
489
|
+
five slightly-different copies of that list.
|
|
490
|
+
|
|
491
|
+
Dotted paths are the one place this rule is stricter than its siblings. They
|
|
492
|
+
skip `owner_id.name` because the query engine resolves the traversal; search
|
|
493
|
+
does not — `resolveSearchFields` matches the field map by exact string, so a
|
|
494
|
+
dotted entry is dropped exactly like a typo, and it is the spelling most likely
|
|
495
|
+
borrowed from `select`/`sort`. It is flagged, with its own fix hint.
|
|
496
|
+
|
|
497
|
+
### Patch Changes
|
|
498
|
+
|
|
499
|
+
- 78caf51: fix(lint): the write-set diagnostics describe what the runtime actually does (#4271)
|
|
500
|
+
|
|
501
|
+
`hook-body-write-unknown-field` and `action-body-write-unknown-field` told
|
|
502
|
+
authors the undeclared column "silently never lands in the stored record".
|
|
503
|
+
Measured on `main`, that is wrong in **both** directions. Nothing between the
|
|
504
|
+
body and the driver filters the key — `applyMutationsToInput` is a plain
|
|
505
|
+
`Object.assign`, and `validateRecord` walks declared fields on insert and
|
|
506
|
+
`continue`s past a key with no field def on update — so the driver decides:
|
|
507
|
+
|
|
508
|
+
- **SQL** — the stray column enters the statement and the **whole write
|
|
509
|
+
fails** with a driver-level error (`table deal has no column named stagee`).
|
|
510
|
+
Nothing is stored, so the correctly-spelled fields of that row are lost too,
|
|
511
|
+
and the error names a column far from the body that wrote it.
|
|
512
|
+
- **Schemaless** (memory, MongoDB — both spread the payload without consulting
|
|
513
|
+
the declared field set) — the stray key **is** persisted, as an undeclared
|
|
514
|
+
column nothing downstream reads.
|
|
515
|
+
|
|
516
|
+
A lint that misdescribes the failure it is warning about teaches the wrong
|
|
517
|
+
debugging instinct: an author told the value silently vanishes will not connect
|
|
518
|
+
the driver error they actually see to the typo that caused it, and on a
|
|
519
|
+
schemaless driver will not go looking for the stray key that is really there.
|
|
520
|
+
All three messages now state the split, matching the "What still happens at
|
|
521
|
+
runtime" description #4355 gave `content/docs/automation/hook-bodies.mdx`.
|
|
522
|
+
|
|
523
|
+
Both outcomes are pinned by a new integration test —
|
|
524
|
+
`runtime/src/sandbox/undeclared-field-write-driver-split.integration.test.ts`.
|
|
525
|
+
Its insert cases run the full chain (real QuickJS sandbox, real hook body, real
|
|
526
|
+
engine, real driver against a real SQLite table), so "reaches the driver
|
|
527
|
+
unfiltered" is proved rather than asserted: if anything on that path ever
|
|
528
|
+
learns to filter, the SQL half stops throwing and the test goes red. The rule
|
|
529
|
+
headers, the `ScriptBodySchema` / `ActionSchema.body` notes and the two
|
|
530
|
+
still-unreleased #4271 changesets are corrected to match. #4355 fixed the
|
|
531
|
+
prose docs; this is the same correction on the surfaces that ship in the
|
|
532
|
+
packages — the diagnostic an author actually reads, and a test that pins it.
|
|
533
|
+
|
|
534
|
+
`@objectstack/spec`: doc comments only — no schema or generated-artifact change.
|
|
535
|
+
|
|
536
|
+
- 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
|
|
537
|
+
|
|
538
|
+
The AGENTS.md post-task checklist requires breaking changesets to carry their
|
|
539
|
+
FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
|
|
540
|
+
inside the npm package and is what an upgrading agent greps after the tombstone
|
|
541
|
+
error." That delivery path was severed for 68 of the 69 publishable packages:
|
|
542
|
+
npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
|
|
543
|
+
older npm versions — not `CHANGELOG.md`, and the canonical
|
|
544
|
+
`"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
|
|
545
|
+
10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
|
|
546
|
+
70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
|
|
547
|
+
explicitly.
|
|
548
|
+
|
|
549
|
+
The tombstone-error scenario is precisely the one where the repo is out of
|
|
550
|
+
reach — the upgrading agent has `node_modules` and nothing else — so the
|
|
551
|
+
migration text has to ride in the tarball. Every publishable package now
|
|
552
|
+
declares `CHANGELOG.md` in `files`, and the canonical whitelist is
|
|
553
|
+
`["dist", "README.md", "CHANGELOG.md"]`.
|
|
554
|
+
|
|
555
|
+
The other half is the gate: `check:published-files` gains a fifth invariant,
|
|
556
|
+
COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
|
|
557
|
+
always-required lint job, so the next package cannot silently sever the path
|
|
558
|
+
again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
|
|
559
|
+
into the canonical set.
|
|
560
|
+
|
|
561
|
+
Consumer-visible change: one more file per install (the package's changelog,
|
|
562
|
+
e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
|
|
563
|
+
node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
|
|
564
|
+
promised.
|
|
565
|
+
|
|
566
|
+
- 38182ff: feat(lint): `flow-node-write-unknown-field` covers `create_record` too (#4271)
|
|
567
|
+
|
|
568
|
+
#4369 shipped the flow write-set gate on `update_record` alone and parked
|
|
569
|
+
`create_record` in `FLOW_WRITE_NODE_TYPES_DEFERRED` with its reason — a gating
|
|
570
|
+
rule earning its severity one measured surface at a time, recorded as data
|
|
571
|
+
rather than left as silence. This measures the other half and moves it across.
|
|
572
|
+
|
|
573
|
+
**The INSERT path fails the same way, one notch harder.** Same literal
|
|
574
|
+
`config.fields` map, same `objectName` binding, same journey to the driver — the
|
|
575
|
+
engine hands an undeclared key to `driver.create` verbatim, alongside the audit
|
|
576
|
+
stamps. On SQLite/knex it becomes `table deal has no column named stagee` and
|
|
577
|
+
the statement is rejected whole, so the correctly named fields in the same
|
|
578
|
+
payload never land either. The extra harm is what does _not_ exist afterwards:
|
|
579
|
+
the row is never created, so every later node reading `{<node>.id}` from that
|
|
580
|
+
node's `outputVariable` is working from a record that was never written. An
|
|
581
|
+
`update_record` failure at least leaves the record intact.
|
|
582
|
+
|
|
583
|
+
So the message now names that consequence on `create_record` and only there —
|
|
584
|
+
"…and the record is never created at all" — instead of one sentence blurred to
|
|
585
|
+
fit both.
|
|
586
|
+
|
|
587
|
+
Nothing else moves: same rule id, same `error` severity, the same silent bails
|
|
588
|
+
(templated `objectName`, non-literal `fields`, cross-package objects, objects
|
|
589
|
+
declaring no fields, dotted keys), and `runAs` is still not consulted. Each skip
|
|
590
|
+
is now pinned on the create surface as well as the update one, so the two node
|
|
591
|
+
types cannot drift into different behaviour.
|
|
592
|
+
|
|
593
|
+
**`FLOW_WRITE_NODE_TYPES_DEFERRED` is now empty and deliberately kept.** The
|
|
594
|
+
partition test derives the full `fields`-write-map set behaviourally from the
|
|
595
|
+
spec's executor-written config schemas, so a node type that grows one later
|
|
596
|
+
belongs to neither list and fails that test until someone classifies it.
|
|
597
|
+
Deleting the empty array would turn that forced decision back into a default.
|
|
598
|
+
|
|
599
|
+
Two non-members are now excluded on the shape of their failure rather than by
|
|
600
|
+
omission, both stated in the module header and one pinned by a test:
|
|
601
|
+
`get_record.fields` is a projection (`z.array(z.string())`) — a READ, where an
|
|
602
|
+
unknown entry narrows the selection instead of breaking the statement — and
|
|
603
|
+
`screen.defaults` is forwarded into the `ScreenSpec` the client renders, so an
|
|
604
|
+
unknown key is a prefill the renderer ignores. That inert "skips it and renders
|
|
605
|
+
the rest" case is exactly what this rule's `error` severity is defined against.
|
|
606
|
+
|
|
607
|
+
Verified against the repo's own apps: app-crm, app-todo and app-showcase all
|
|
608
|
+
still validate clean with `create_record` covered — including crm's
|
|
609
|
+
convert-lead flow, which creates an account and an opportunity before updating
|
|
610
|
+
the lead.
|
|
611
|
+
|
|
612
|
+
- af5b96b: fix(lint): flow rules see into try_catch / loop / parallel regions (#4380)
|
|
613
|
+
|
|
614
|
+
Every lint rule that inspects flow nodes had hand-written the same one-liner —
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
const nodes = Array.isArray(flow.nodes) ? (flow.nodes as AnyRec[]) : [];
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
— and every one of them was therefore blind to the same thing.
|
|
621
|
+
`FlowRegionSchema` holds a full `nodes: z.array(FlowNodeSchema)`, and four
|
|
622
|
+
config slots carry one: `try_catch.config.try` / `.catch`, `loop.config.body`,
|
|
623
|
+
and `parallel.config.branches[].nodes`. Regions nest arbitrarily. Move a node
|
|
624
|
+
into any of them and the checking stayed behind.
|
|
625
|
+
|
|
626
|
+
Measured before the fix, the same bad nodes at the top level vs inside a
|
|
627
|
+
`try_catch`:
|
|
628
|
+
|
|
629
|
+
| rule | severity | flat | nested |
|
|
630
|
+
| :---------------------------------------------- | :------------ | :--- | :------------------ |
|
|
631
|
+
| `flow-node-write-unknown-field` | error | 1 | **0** |
|
|
632
|
+
| `flow-update-readonly-field` | error | 1 | **0** |
|
|
633
|
+
| `approval-approver-*` | error/warning | 1 | **0** |
|
|
634
|
+
| `flow-template-unknown-field` (filter position) | error | 1 | **1, as a warning** |
|
|
635
|
+
|
|
636
|
+
**The last row is the one a reader would not predict.**
|
|
637
|
+
`validate-flow-template-paths` scans a node's whole `config` for string leaves,
|
|
638
|
+
so it still _saw_ tokens inside a region — but its `filter`-position split only
|
|
639
|
+
looks at the top level of the node it was handed. A nested filter token lost its
|
|
640
|
+
position, so the #3810 finding ("this node cannot run — an erased condition
|
|
641
|
+
WIDENS the query") silently degraded to an advisory warning, reported against
|
|
642
|
+
the wrapping `try_catch` instead of the `get_record` that is broken:
|
|
643
|
+
|
|
644
|
+
```
|
|
645
|
+
FLAT error flow "f" node "get_record" flows[0].nodes[1]
|
|
646
|
+
NESTED warning flow "f" node "try_catch" flows[0].nodes[1]
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
Being visible is not the same as being judged correctly. That is worse than a
|
|
650
|
+
clean miss: a yellow line reads as "checked and merely advisory".
|
|
651
|
+
|
|
652
|
+
**One shared walk, not five.** `flow-walk.ts` — the flow-side counterpart of the
|
|
653
|
+
existing `page-walk.ts`, and here for the same stated reason: getting the
|
|
654
|
+
traversal right is subtle enough that duplicating it has already produced dead
|
|
655
|
+
rules. `walkFlowNodes(flow, flowPath)` yields every node with its real config
|
|
656
|
+
path (`flows[0].nodes[1].config.catch.nodes[0]`), a region breadcrumb for
|
|
657
|
+
diagnostics (`try_catch "Guard" › catch`), and depth. Four rules now route
|
|
658
|
+
through it: the two flow write rules, the template-path rule, and the approval
|
|
659
|
+
rule.
|
|
660
|
+
|
|
661
|
+
Findings now land on the node that is actually wrong, which is the point — a
|
|
662
|
+
path pointing at the container is not actionable in a flow with several regions.
|
|
663
|
+
|
|
664
|
+
**The double-count trap is handled, not left to each caller.** A container node
|
|
665
|
+
is walked too (it has its own config worth checking — a `loop`'s `collection`, a
|
|
666
|
+
`try_catch`'s `retry`), but its `config` physically contains every descendant,
|
|
667
|
+
so a rule that scans config recursively would report each nested finding twice.
|
|
668
|
+
`WalkedFlowNode.localConfig` is the container's config with region slots
|
|
669
|
+
removed; the recursive scanner uses it, and a test pins that a nested token is
|
|
670
|
+
reported once while the container's own `collection` token still is.
|
|
671
|
+
|
|
672
|
+
`REGION_SLOTS` is declared as data and pinned against the spec's own
|
|
673
|
+
region-bearing config schemas — derived behaviourally (a slot is one that
|
|
674
|
+
accepts `{nodes: […]}`), not restated — so a fifth construct fails that test
|
|
675
|
+
instead of becoming a fifth silent blind spot. A `MAX_REGION_DEPTH` cap keeps a
|
|
676
|
+
hand-authored (pre-parse) stack from hanging a lint.
|
|
677
|
+
|
|
678
|
+
Verified end to end: nested now matches flat on every rule, including the
|
|
679
|
+
restored `error` severity. app-showcase ships an `update_record` inside a
|
|
680
|
+
`catch` branch (`showcase_resilient_sync`) that had never been checked by
|
|
681
|
+
anything — it is correct, so validation stays clean, and breaking its field name
|
|
682
|
+
on purpose now fails `os validate` with
|
|
683
|
+
`flows[24].nodes[1].config.catch.nodes[0].config.fields.sync_statuss` and the
|
|
684
|
+
region trail `try_catch "Push with retry" › catch › node "Flag Sync Failure"`.
|
|
685
|
+
|
|
686
|
+
- 7d80695: fix(lint): an object declaring no fields is unjudgeable, not "has no such field" (#4383)
|
|
687
|
+
|
|
688
|
+
`hook-body-write-unknown-field` and `action-body-write-unknown-field` reported
|
|
689
|
+
**every** field write to an object that declares no `fields` — an external
|
|
690
|
+
object, or a datasource-introspected schema whose columns are resolved at
|
|
691
|
+
runtime. Measured before the fix:
|
|
692
|
+
|
|
693
|
+
```
|
|
694
|
+
hook : ["hook-body-write-unknown-field / warning"] ← false
|
|
695
|
+
action: ["action-body-write-unknown-field / warning"] ← false
|
|
696
|
+
flow : [] ← correct
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
`indexObjectFields` returns an **empty Set** for such an object rather than
|
|
700
|
+
`undefined`, and both rules only asked "is this object in the stack?" —
|
|
701
|
+
`targetSets.every((s) => s !== undefined)` and `if (!known) continue`. An empty
|
|
702
|
+
Set is neither undefined nor falsy, so it became the answer to `has(field)`,
|
|
703
|
+
and the answer is always `false`.
|
|
704
|
+
|
|
705
|
+
That field map is not empty, it is **unknown**. The distinction already existed
|
|
706
|
+
in two other rules of the same family, each with its reason written down —
|
|
707
|
+
`validate-searchable-fields` skip #2 and `validate-flow-node-writes` (#4369,
|
|
708
|
+
which added the guard because it gates). Two of four had it; the drift shape
|
|
709
|
+
#3583 and #4330 exist to remove.
|
|
710
|
+
|
|
711
|
+
**Fixed once, not twice.** The guard now lives in a shared
|
|
712
|
+
`judgeableFieldsOf(index, objectName)` that returns the declared names only when
|
|
713
|
+
they are a sound basis for a "resolves to nothing" judgement, and `undefined`
|
|
714
|
+
for both unjudgeable cases — cross-package objects and fields-less ones. All
|
|
715
|
+
three write-set rules route their lookups through it, so a fourth cannot repeat
|
|
716
|
+
the omission. It is internal to the family (not re-exported from the package
|
|
717
|
+
barrel), same as `indexObjectFields` and `IMPLICIT_FIELDS`.
|
|
718
|
+
|
|
719
|
+
One semantic call worth naming: a **multi-target** hook where only _some_
|
|
720
|
+
targets are judgeable is now skipped entirely. The `ctx.input` finding fires
|
|
721
|
+
only when a field is missing from EVERY target, and an unjudgeable target is one
|
|
722
|
+
the field might well exist on — so judging the remainder would assert "missing
|
|
723
|
+
everywhere" on evidence that does not cover everywhere. Consistent with the
|
|
724
|
+
rule's stated asymmetry: prefer a missed finding to a false one.
|
|
725
|
+
|
|
726
|
+
No behaviour change for objects that declare fields: an unknown field on a
|
|
727
|
+
normal object still warns exactly as before, pinned by a test placed next to
|
|
728
|
+
each new skip so the guard cannot swallow the real finding.
|
|
729
|
+
|
|
730
|
+
- ade7be4: fix(lint): the seven system-field exemption lists derive from the spec's declarations (#4330)
|
|
731
|
+
|
|
732
|
+
Five rules in `@objectstack/lint` each carried their own hand-copy of
|
|
733
|
+
"registry-injected columns present on almost every object but absent from
|
|
734
|
+
authored `fields`" — and they had already drifted from one another (two more
|
|
735
|
+
copies had appeared by the time the fix landed). This is the shape #3786
|
|
736
|
+
removed from the audit-provenance family, rebuilt one package over: the same
|
|
737
|
+
list, maintained in parallel, each under a comment asking to be kept in sync
|
|
738
|
+
with one of the others.
|
|
739
|
+
|
|
740
|
+
The package now has one module, `system-fields.ts`, whose `SYSTEM_FIELDS` is
|
|
741
|
+
DERIVED from the spec's two declarations — `FIELD_GROUP_SYSTEM_FIELDS`
|
|
742
|
+
(`@objectstack/spec/data`) and `SystemFieldName` (`@objectstack/spec/system`)
|
|
743
|
+
— and all seven field-resolving rules consume it. A pin test holds the
|
|
744
|
+
boundary in both directions: the set contains exactly the two declarations'
|
|
745
|
+
union, and none of the rule-local exemptions.
|
|
746
|
+
|
|
747
|
+
Two deliberate behavior consequences, both in the permissive direction the
|
|
748
|
+
rules' own comments argue for (over-inclusion costs at worst a missed
|
|
749
|
+
warning; under-inclusion costs a false one):
|
|
750
|
+
|
|
751
|
+
- `widget-bindings`, `page-field-bindings` and `react-page-props` now also
|
|
752
|
+
exempt `is_deleted`;
|
|
753
|
+
- `flow-template-paths` now also exempts `user_id`.
|
|
754
|
+
|
|
755
|
+
Names that are NOT system columns in the spec's sense (`name`, `owner`,
|
|
756
|
+
`record_type`, and the legacy physical spellings `_id` / `space`) stay
|
|
757
|
+
rule-local next to the reason each rule exempts them, instead of widening
|
|
758
|
+
every rule: `name` in particular is an ordinary authored field on most
|
|
759
|
+
objects, and exempting it package-wide would stop the field-existence rules
|
|
760
|
+
from catching a reference to a field the object genuinely does not have.
|
|
761
|
+
|
|
762
|
+
- 8db4587: fix(lint,cli): `os lint` / `os compile` 不再放行一个 `os validate` 会拒绝的 react 页面
|
|
763
|
+
|
|
764
|
+
`validateReactPageProps` 只手工接在 `os validate` 上,另外两个命令从来没跑过它。
|
|
765
|
+
在 showcase 的 react 页面上植入一处 gating 违规(`<ListView filters={['no_such_col','=',stage]}>`
|
|
766
|
+
—— 谓词命中不了任何行,列表回空,和「本来就没数据」无法区分)实测:
|
|
767
|
+
|
|
768
|
+
```
|
|
769
|
+
os lint os compile os validate
|
|
770
|
+
修复前 exit 0 放行 exit 0 放行 exit 1 拒绝
|
|
771
|
+
修复后 exit 1 拒绝 exit 1 拒绝 exit 1 拒绝
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
这条规则在 #4340 之后已经是**整个 react 页面表面唯一**的字段解析闸门:
|
|
775
|
+
`<ListView>` 的 columns/fields/sort/grouping/userFilters、`<ObjectForm>` 的
|
|
776
|
+
fields/initialValues/sections/subforms、`record:*` 一族(与元数据表面共用同一张
|
|
777
|
+
`COMPONENT_FIELD_SPECS`)、`<ObjectChart>` 的 aggregate/axes、以及 `searchableFields`。
|
|
778
|
+
漏接不是少几条警告 —— 而是这些绑定在 build 路径上**完全没人看**,包括其中会 gate 的那些。
|
|
779
|
+
|
|
780
|
+
现接入 `REFERENCE_INTEGRITY_RULES`,`os validate` 里那处手工接线随之删除,三个命令的
|
|
781
|
+
答案由构造保证一致。这正是 suite 设立要终结的漂移(#3583 §5 D5),也是
|
|
782
|
+
`validateReadonlyFlowWrites` 在 #4394 里刚走过的同一条路 —— 那次的教训是
|
|
783
|
+
「一张 map、两个检查、两套命令集合」,这次是「一次 JSX parse、七个 rule id、
|
|
784
|
+
一套命令集合」。
|
|
785
|
+
|
|
786
|
+
规则行为零变化:id、严重级、文案都不动;喂进去的输入也不变(`os validate` 原本就
|
|
787
|
+
传 `result.data`,suite 拿到的是同一个)。`#4402` 的接线守卫会在下一次有人想再手工
|
|
788
|
+
接一条规则时直接报错。
|
|
789
|
+
|
|
790
|
+
`validateReactPageProps` 沿用 `validateHookBodyWrites` / `validateActionBodyWrites`
|
|
791
|
+
的惰性约定:只有真的存在 `kind:'react'` 页面时才加载 TypeScript 编译器。
|
|
792
|
+
|
|
793
|
+
- 7fec5d6: fix(lint,cli): `os lint` no longer passes a flow the other two commands refuse
|
|
794
|
+
|
|
795
|
+
`validateReadonlyFlowWrites` was hand-wired into `os validate` and `os compile`
|
|
796
|
+
and never into `os lint`. Measured on the showcase app with one planted
|
|
797
|
+
violation — a `runAs:'user'` `update_record` writing a static-`readonly` field:
|
|
798
|
+
|
|
799
|
+
| | `os lint` | `os validate` |
|
|
800
|
+
| ------ | ------------------- | ---------------- |
|
|
801
|
+
| before | **exit 0 — passed** | exit 1 — refused |
|
|
802
|
+
| after | exit 1 — refused | exit 1 — refused |
|
|
803
|
+
|
|
804
|
+
That rule **gates** (a static `readonly` + literal field is a certain no-op:
|
|
805
|
+
the engine strips it from the UPDATE payload while the step still reports
|
|
806
|
+
success, #2948/#3425), so the divergence was not a missing warning — `os lint`
|
|
807
|
+
green-lit a build `os validate` stops.
|
|
808
|
+
|
|
809
|
+
It now joins `REFERENCE_INTEGRITY_RULES`, and both hand-wired call sites are
|
|
810
|
+
deleted with it, so the three commands share one answer by construction rather
|
|
811
|
+
than by three people remembering. This is the drift the suite was created to end
|
|
812
|
+
(#3583 §5 D5) and which its own header cited this rule as the standing proof of.
|
|
813
|
+
|
|
814
|
+
Two things made the wiring indefensible rather than merely untidy:
|
|
815
|
+
|
|
816
|
+
- `validateFlowNodeWrites` (#4369) walks the **same** `config.fields` map to ask
|
|
817
|
+
the other half of the question — "does this field exist?" against "is it
|
|
818
|
+
writable?" — and is already a suite member. One map, two checks, two different
|
|
819
|
+
command sets.
|
|
820
|
+
- The two hand-wired sites did not even agree with each other on their input:
|
|
821
|
+
`validate` passed the PRE-parse `normalized` stack, `compile` the POST-parse
|
|
822
|
+
`result.data`. Verified equivalent for this rule before collapsing them onto
|
|
823
|
+
the suite's post-parse input, so no finding is lost.
|
|
824
|
+
|
|
825
|
+
No rule behaviour changes: same ids, same severities, same messages.
|
|
826
|
+
|
|
827
|
+
- 31e0be9: Flow metadata is canonicalized inside structured regions, not just at the top level (#4347).
|
|
828
|
+
|
|
829
|
+
`registerFlow` canonicalizes a stored flow through three passes — the ADR-0087 conversion
|
|
830
|
+
table, `FlowSchema.parse`, and the ADR-0032 predicate validation — and every one of them
|
|
831
|
+
walked `flow.nodes` / `flow.edges` only. An ADR-0031 container keeps a whole sub-graph in
|
|
832
|
+
its open `config` (`loop.config.body`, `parallel.config.branches[]`,
|
|
833
|
+
`try_catch.config.try`/`.catch`), so all three stopped at the container and metadata came
|
|
834
|
+
out **position-dependent**: the same node converted at the top level and did not one level
|
|
835
|
+
in, and the same predicate was stored as a `{ dialect: 'cel', source }` envelope on a
|
|
836
|
+
top-level edge and left a bare string on a loop-body edge.
|
|
837
|
+
|
|
838
|
+
The reporting app shipped three sweeps whose gates never opened. Each run reported
|
|
839
|
+
`success: true`, queried correctly, selected exactly the right records, and then did
|
|
840
|
+
nothing — which is indistinguishable from "this sweep had no work to do" unless you assert
|
|
841
|
+
on records written.
|
|
842
|
+
|
|
843
|
+
- **`mapFlowNodes` recurses into regions**, to any depth. Every conversion in the table now
|
|
844
|
+
reaches a nested node, which matters most for the two that change behaviour rather than
|
|
845
|
+
spelling: a `webhook` / `http_request` callout inside a loop body kept a type no executor
|
|
846
|
+
owns (the run failed), and a `delete_record` kept `config.filters`, leaving the canonical
|
|
847
|
+
`filter` the executor reads absent — the erased-condition hazard
|
|
848
|
+
`flow-node-crud-filter-alias` exists to prevent. Notice paths carry the region
|
|
849
|
+
(`flows[0].nodes[3].config.body.nodes[1].config.filter`), so the warning points at the
|
|
850
|
+
node to edit.
|
|
851
|
+
- **New `normalizeControlFlowRegions`**, called at the load seam after
|
|
852
|
+
`validateControlFlow`: each region is parsed through its own schema (recursively — regions
|
|
853
|
+
nest), so nested edges and nodes carry the same canonical shapes as top-level ones. A
|
|
854
|
+
region that does not parse is left untouched; rejecting one stays `validateControlFlow`'s
|
|
855
|
+
job, so which flows register is unchanged.
|
|
856
|
+
- **New `collectFlowGraphs`** yields a flow's own graph plus every nested region, each with
|
|
857
|
+
a scope label. Both predicate validators iterate it instead of `flow.nodes` — the engine's
|
|
858
|
+
`validateFlowExpressions` and `@objectstack/lint`'s author-time
|
|
859
|
+
`validateStackExpressions` — so the `{record.x}` brace-trap they exist to catch is now
|
|
860
|
+
caught inside a loop body too, naming the region (`loop 'sweep' body · edge 'b1' …`). It
|
|
861
|
+
used to pass `objectstack validate`, pass registration, and fail at run time with the
|
|
862
|
+
diagnostic suppressed.
|
|
863
|
+
|
|
864
|
+
The container executors already parse their own config at run time (`parseNodeConfig`,
|
|
865
|
+
#4277), so a nested predicate did evaluate correctly on current `main` — what was still
|
|
866
|
+
wrong is everything that reads a region _without_ re-parsing it (the Studio designer,
|
|
867
|
+
`getFlow`, the version history), and every conversion, none of which the executors replay.
|
|
868
|
+
|
|
869
|
+
Also hardened, per the issue's secondary finding: `evaluateCondition`'s legacy `{var}`
|
|
870
|
+
template path **refuses an unresolved dotted reference** instead of comparing it as a
|
|
871
|
+
string. `'oppRecord.amount > 500000'` was compared `'oppRecord.amount' > '500000'` — `'o'`
|
|
872
|
+
against `'5'` — so it was constantly true regardless of the amount: silently wrong in the
|
|
873
|
+
_true_ direction, a gate that reports success while never gating. It now throws with the
|
|
874
|
+
source and the fix (a CEL envelope, or brace the reference if the `{var}` dialect was
|
|
875
|
+
meant), the same "never swallow a broken predicate" rule ADR-0032 §1c set for the CEL path.
|
|
876
|
+
The `try { … } catch { return false }` around that block went with it: nothing in it throws,
|
|
877
|
+
so it guarded nothing and would have swallowed the new refusal straight back into the silent
|
|
878
|
+
wrong answer. Bare-word comparisons (`'{status} == active'`) and `{var}` templates are
|
|
879
|
+
unchanged — only dotted references, which substitution can never leave behind, are refused.
|
|
880
|
+
|
|
881
|
+
- 4bfd455: One declaration of where ADR-0031 regions live (#4401).
|
|
882
|
+
|
|
883
|
+
A region is a sub-graph inside `FlowNodeSchema.config`, an open `z.record`. Nothing in the
|
|
884
|
+
type system says which key on which node type holds one, so every pass that needs to reach
|
|
885
|
+
a region node has to be told — and within one week three of them were told separately, by
|
|
886
|
+
two changes that were each correct on their own:
|
|
887
|
+
|
|
888
|
+
| pass | package | table it carried |
|
|
889
|
+
| --------------------------------------------------------------------------- | ------- | ------------------- |
|
|
890
|
+
| `mapFlowNodes` (ADR-0087 conversions) | `spec` | `FLOW_REGION_SLOTS` |
|
|
891
|
+
| `validateControlFlow` / `normalizeControlFlowRegions` / `collectFlowGraphs` | `spec` | `regionSlotsOf` |
|
|
892
|
+
| `walkFlowNodes` (lint flow rules) | `lint` | `REGION_SLOTS` |
|
|
893
|
+
|
|
894
|
+
Each pinned its own copy with its own reconciliation test. So every copy was protected from
|
|
895
|
+
drifting away from the schemas, and **nothing would have failed if the copies drifted from
|
|
896
|
+
each other** — while adding a fourth construct meant editing three places, and missing one
|
|
897
|
+
reproduces exactly the silent blind spot #4347 and #4380 were both filed about.
|
|
898
|
+
|
|
899
|
+
- New `@objectstack/spec/automation` export `FLOW_REGION_SLOTS` (plus the
|
|
900
|
+
`FLOW_REGION_SLOTS_BY_TYPE` / `FLOW_REGION_CONFIG_KEYS` views) is now the only statement
|
|
901
|
+
of the fact. It lives in an **import-free** module so `spec/conversions/walk.ts` can read
|
|
902
|
+
it and stay the pure shape walker it was written as; mapping a slot onto the Zod schema
|
|
903
|
+
its value parses as stays in `control-flow.zod.ts`, which is schema business.
|
|
904
|
+
- The three reconciliation tests collapse into one, `region-slots.test.ts`, keeping the
|
|
905
|
+
strongest of them: it derives each construct's region keys **behaviourally**, by asking
|
|
906
|
+
the config schema what it actually accepts in a region shape, rather than reading names
|
|
907
|
+
off `.shape`. It also probes every other exported `*ConfigSchema`, so a new
|
|
908
|
+
region-bearing construct cannot be added without either declaring its slots or failing
|
|
909
|
+
here.
|
|
910
|
+
|
|
911
|
+
The three **walks** are deliberately left separate. They take different inputs (parsed
|
|
912
|
+
`FlowNodeParsed` vs raw authored records), yield different units (a graph, a node, a
|
|
913
|
+
copy-on-write rewritten tree), and the lint one formats human diagnostic trails from node
|
|
914
|
+
labels — consumer logic, not protocol (Prime Directive #2). Merging them would trade a
|
|
915
|
+
duplicated four-line table for a walker that serves nobody well. Only the fact they all
|
|
916
|
+
need is shared.
|
|
917
|
+
|
|
918
|
+
No behaviour change: every existing test passes unchanged, which is the point of the
|
|
919
|
+
exercise.
|
|
920
|
+
|
|
921
|
+
- 1bd2795: feat(spec,lint): the `ui` vocabularies admit what the renderers implement, and derive instead of restating (objectui#2945)
|
|
922
|
+
|
|
923
|
+
Additions-only follow-up to the vocabulary audit
|
|
924
|
+
(objectstack-ai/objectui#2901, #2945). Nothing here narrows a vocabulary, so no
|
|
925
|
+
already-stored metadata changes meaning — three of the four `ui/` enums that had
|
|
926
|
+
drifted from what is actually implemented, plus the fork that drift had made
|
|
927
|
+
invisible.
|
|
928
|
+
|
|
929
|
+
**`ChartTypeSchema` admits `combo`.** The taxonomy could not name the one chart
|
|
930
|
+
family the rest of `chart.zod.ts` is written for: `ChartSeriesSchema.type`
|
|
931
|
+
exists to override a series' type — its doc comment literally says _"combo
|
|
932
|
+
charts"_ — and `ChartSeriesSchema.yAxis` binds a series to the left or right
|
|
933
|
+
axis, which is only meaningful for mixed marks. objectui's renderer draws it
|
|
934
|
+
distinctly (mixed bar/line/area on dual axes, per-series type) and had to carry
|
|
935
|
+
`combo` in a local fork of this list, whose own comment claimed to mirror it.
|
|
936
|
+
|
|
937
|
+
**`WidgetActionTypeSchema` is `ActionType`.** The two disagreed by one member,
|
|
938
|
+
`form`, and the disagreement was backwards: a dashboard header or widget action
|
|
939
|
+
button dispatches through the same `ActionRunner` that implements `form` —
|
|
940
|
+
objectui's `DashboardRenderer` deliberately routes everything except a raw `url`
|
|
941
|
+
into it, so a `flow` header action works (#3528). The narrower enum therefore
|
|
942
|
+
rejected at validation exactly what the shared dispatcher then executes.
|
|
943
|
+
Derived, so the next type the runner implements needs one edit, not two.
|
|
944
|
+
|
|
945
|
+
**`ListChartConfigSchema.chartType` is `ChartTypeSchema.extract([...])`.** Same
|
|
946
|
+
five members as before — a de-duplication, not a widening. A member renamed in
|
|
947
|
+
the taxonomy now fails at build time instead of leaving a second list quietly
|
|
948
|
+
disagreeing.
|
|
949
|
+
|
|
950
|
+
**`@objectstack/lint`'s chart-family set is derived from the taxonomy.**
|
|
951
|
+
`validate-widget-bindings` decides which widgets need a `chartConfig` measure
|
|
952
|
+
mapping from a hand-written list of families, and its omissions fail in the
|
|
953
|
+
worst direction: an unlisted family reads as _"not a chart"_, so a widget
|
|
954
|
+
missing its mapping **passes** validation. `combo` was exactly that case —
|
|
955
|
+
verified by pinning the old list back, where a `combo` widget with no
|
|
956
|
+
`chartConfig` produced zero findings. The set is now the taxonomy minus an
|
|
957
|
+
explicit `MEASURE_EXEMPT_CHART_TYPES` (single-value and tabular families), so a
|
|
958
|
+
family added to the spec is covered without editing the rule.
|
|
959
|
+
|
|
960
|
+
Guards: `packages/spec/src/ui/vocabulary-derivation.test.ts` asserts both
|
|
961
|
+
derivations still hold (a restated list fails silently — it keeps validating,
|
|
962
|
+
just not what the other list says), and the lint suite now walks every
|
|
963
|
+
multi-series family in the taxonomy rather than a list of its own.
|
|
964
|
+
|
|
965
|
+
A third ratchet already existed and did its job: `app-showcase`'s coverage test
|
|
966
|
+
requires a gallery widget for every distinctly-renderable `ChartType`, and it
|
|
967
|
+
failed the moment `combo` was admitted. The Chart Gallery dashboard now
|
|
968
|
+
demonstrates it — a task count as bars on the left axis, an average as a line on
|
|
969
|
+
the right, which is the configuration `series[].type` / `series[].yAxis` exist
|
|
970
|
+
for.
|
|
971
|
+
|
|
972
|
+
`ActionType` deliberately does **not** gain `navigation`, which the audit
|
|
973
|
+
suggested. `ActionRunner.executeNavigation` is a strictly weaker
|
|
974
|
+
`executeUrl` — no `${param.X}` interpolation, no `apiBase` promotion, no
|
|
975
|
+
`openIn` — differing only by a `replace` option, and its one live producer is
|
|
976
|
+
the SDUI `element:button` `action` prop, which `ElementButtonPropsSchema` does
|
|
977
|
+
not model at all. Promoting the name would add a second spelling of _navigate_
|
|
978
|
+
to a closed authorable vocabulary (members cannot be removed later) without
|
|
979
|
+
closing the gap that actually exists. Tracked separately.
|
|
980
|
+
|
|
981
|
+
Verified: `@objectstack/spec` **6944 tests / 267 files**, `@objectstack/lint`
|
|
982
|
+
**544 tests / 37 files**, both green; `tsc --noEmit` clean on both.
|
|
983
|
+
|
|
984
|
+
- Updated dependencies [6a67d7a]
|
|
985
|
+
- Updated dependencies [0ecc656]
|
|
986
|
+
- Updated dependencies [06772eb]
|
|
987
|
+
- Updated dependencies [270650f]
|
|
988
|
+
- Updated dependencies [3aef718]
|
|
989
|
+
- Updated dependencies [1ea6bce]
|
|
990
|
+
- Updated dependencies [c1dcacd]
|
|
991
|
+
- Updated dependencies [ad303ed]
|
|
992
|
+
- Updated dependencies [32ccb23]
|
|
993
|
+
- Updated dependencies [f5a4ef0]
|
|
994
|
+
- Updated dependencies [2d3e255]
|
|
995
|
+
- Updated dependencies [7d7521f]
|
|
996
|
+
- Updated dependencies [5dc4d02]
|
|
997
|
+
- Updated dependencies [05154a1]
|
|
998
|
+
- Updated dependencies [9b6fe7c]
|
|
999
|
+
- Updated dependencies [8c711fb]
|
|
1000
|
+
- Updated dependencies [09e4547]
|
|
1001
|
+
- Updated dependencies [91f4c78]
|
|
1002
|
+
- Updated dependencies [820eff9]
|
|
1003
|
+
- Updated dependencies [8d895ff]
|
|
1004
|
+
- Updated dependencies [f6472d7]
|
|
1005
|
+
- Updated dependencies [78caf51]
|
|
1006
|
+
- Updated dependencies [62a789b]
|
|
1007
|
+
- Updated dependencies [789ad63]
|
|
1008
|
+
- Updated dependencies [2af1988]
|
|
1009
|
+
- Updated dependencies [2e836de]
|
|
1010
|
+
- Updated dependencies [12a19a8]
|
|
1011
|
+
- Updated dependencies [41dcda3]
|
|
1012
|
+
- Updated dependencies [c8124e5]
|
|
1013
|
+
- Updated dependencies [a1a4140]
|
|
1014
|
+
- Updated dependencies [217e2e6]
|
|
1015
|
+
- Updated dependencies [86a71d1]
|
|
1016
|
+
- Updated dependencies [d5c75e2]
|
|
1017
|
+
- Updated dependencies [03d26f7]
|
|
1018
|
+
- Updated dependencies [4384921]
|
|
1019
|
+
- Updated dependencies [3c628ce]
|
|
1020
|
+
- Updated dependencies [7cb922e]
|
|
1021
|
+
- Updated dependencies [1d22114]
|
|
1022
|
+
- Updated dependencies [b5f9397]
|
|
1023
|
+
- Updated dependencies [ed77493]
|
|
1024
|
+
- Updated dependencies [58a03d2]
|
|
1025
|
+
- Updated dependencies [dc530b4]
|
|
1026
|
+
- Updated dependencies [e59786e]
|
|
1027
|
+
- Updated dependencies [bcf1112]
|
|
1028
|
+
- Updated dependencies [9774b78]
|
|
1029
|
+
- Updated dependencies [b07d829]
|
|
1030
|
+
- Updated dependencies [a648e96]
|
|
1031
|
+
- Updated dependencies [a47ac06]
|
|
1032
|
+
- Updated dependencies [e4c61a7]
|
|
1033
|
+
- Updated dependencies [cc60165]
|
|
1034
|
+
- Updated dependencies [081aa6f]
|
|
1035
|
+
- Updated dependencies [91f4c78]
|
|
1036
|
+
- Updated dependencies [e8d0c21]
|
|
1037
|
+
- Updated dependencies [c1d44f7]
|
|
1038
|
+
- Updated dependencies [ab9fb5c]
|
|
1039
|
+
- Updated dependencies [f985b3f]
|
|
1040
|
+
- Updated dependencies [9a4932a]
|
|
1041
|
+
- Updated dependencies [f9fc874]
|
|
1042
|
+
- Updated dependencies [011b386]
|
|
1043
|
+
- Updated dependencies [7777e8f]
|
|
1044
|
+
- Updated dependencies [507b92a]
|
|
1045
|
+
- Updated dependencies [7309c81]
|
|
1046
|
+
- Updated dependencies [20bc1ec]
|
|
1047
|
+
- Updated dependencies [90c2b15]
|
|
1048
|
+
- Updated dependencies [42eeb7d]
|
|
1049
|
+
- Updated dependencies [01e124d]
|
|
1050
|
+
- Updated dependencies [7ce02eb]
|
|
1051
|
+
- Updated dependencies [a13827e]
|
|
1052
|
+
- Updated dependencies [7733604]
|
|
1053
|
+
- Updated dependencies [40e420f]
|
|
1054
|
+
- Updated dependencies [d13004a]
|
|
1055
|
+
- Updated dependencies [cc2de0e]
|
|
1056
|
+
- Updated dependencies [5b47ab5]
|
|
1057
|
+
- Updated dependencies [b09d8d9]
|
|
1058
|
+
- Updated dependencies [b09d8d9]
|
|
1059
|
+
- Updated dependencies [8675db6]
|
|
1060
|
+
- Updated dependencies [b09d8d9]
|
|
1061
|
+
- Updated dependencies [3eb1b2b]
|
|
1062
|
+
- Updated dependencies [59b85c0]
|
|
1063
|
+
- Updated dependencies [6e357ed]
|
|
1064
|
+
- Updated dependencies [d6938bf]
|
|
1065
|
+
- Updated dependencies [31e0be9]
|
|
1066
|
+
- Updated dependencies [4bfd455]
|
|
1067
|
+
- Updated dependencies [ffd2ce2]
|
|
1068
|
+
- Updated dependencies [62f8017]
|
|
1069
|
+
- Updated dependencies [a831df1]
|
|
1070
|
+
- Updated dependencies [f752ee3]
|
|
1071
|
+
- Updated dependencies [a1b61e0]
|
|
1072
|
+
- Updated dependencies [cd6b9f2]
|
|
1073
|
+
- Updated dependencies [2cb6d3c]
|
|
1074
|
+
- Updated dependencies [af2a095]
|
|
1075
|
+
- Updated dependencies [ec796d5]
|
|
1076
|
+
- Updated dependencies [e87fea1]
|
|
1077
|
+
- Updated dependencies [c65e529]
|
|
1078
|
+
- Updated dependencies [3ca34c1]
|
|
1079
|
+
- Updated dependencies [239c3a3]
|
|
1080
|
+
- Updated dependencies [94a0bbc]
|
|
1081
|
+
- Updated dependencies [d6bfb3d]
|
|
1082
|
+
- Updated dependencies [a2266a6]
|
|
1083
|
+
- Updated dependencies [d25a0ec]
|
|
1084
|
+
- Updated dependencies [667b83e]
|
|
1085
|
+
- Updated dependencies [627b188]
|
|
1086
|
+
- Updated dependencies [8d4eae7]
|
|
1087
|
+
- Updated dependencies [65a3a84]
|
|
1088
|
+
- Updated dependencies [ccd9397]
|
|
1089
|
+
- Updated dependencies [bca935b]
|
|
1090
|
+
- Updated dependencies [c54c822]
|
|
1091
|
+
- Updated dependencies [8dcc0f5]
|
|
1092
|
+
- Updated dependencies [75b9e51]
|
|
1093
|
+
- Updated dependencies [0a2f233]
|
|
1094
|
+
- Updated dependencies [8621cdd]
|
|
1095
|
+
- Updated dependencies [6f23667]
|
|
1096
|
+
- Updated dependencies [5d21a48]
|
|
1097
|
+
- Updated dependencies [19365b7]
|
|
1098
|
+
- Updated dependencies [b7ed26d]
|
|
1099
|
+
- Updated dependencies [b3a3d83]
|
|
1100
|
+
- Updated dependencies [7a55913]
|
|
1101
|
+
- Updated dependencies [35accbf]
|
|
1102
|
+
- Updated dependencies [6038de7]
|
|
1103
|
+
- Updated dependencies [eb95d97]
|
|
1104
|
+
- Updated dependencies [e4c2dc8]
|
|
1105
|
+
- Updated dependencies [1bd2795]
|
|
1106
|
+
- Updated dependencies [8186a70]
|
|
1107
|
+
- Updated dependencies [a329cca]
|
|
1108
|
+
- Updated dependencies [6eec18c]
|
|
1109
|
+
- Updated dependencies [4d7bebf]
|
|
1110
|
+
- Updated dependencies [821ac7a]
|
|
1111
|
+
- Updated dependencies [8f81731]
|
|
1112
|
+
- Updated dependencies [4965bfa]
|
|
1113
|
+
- Updated dependencies [8b50cb3]
|
|
1114
|
+
- Updated dependencies [8c2db68]
|
|
1115
|
+
- Updated dependencies [22b5e54]
|
|
1116
|
+
- Updated dependencies [0166bd5]
|
|
1117
|
+
- Updated dependencies [9b702dc]
|
|
1118
|
+
- Updated dependencies [ab16331]
|
|
1119
|
+
- @objectstack/spec@17.0.0-rc.1
|
|
1120
|
+
- @objectstack/formula@17.0.0-rc.1
|
|
1121
|
+
- @objectstack/sdui-parser@17.0.0-rc.1
|
|
1122
|
+
|
|
1123
|
+
## 17.0.0-rc.0
|
|
1124
|
+
|
|
1125
|
+
### Minor Changes
|
|
1126
|
+
|
|
1127
|
+
- 14252d3: feat(approvals): cross-organization approver targeting — a plant document can
|
|
1128
|
+
require a group-side sign-off (ADR-0105 D9)
|
|
1129
|
+
|
|
1130
|
+
One organization id used to decide three different things at once in
|
|
1131
|
+
`openNodeRequest`: where the request row lives, where its inbox index rows
|
|
1132
|
+
live, and **where its approvers are looked up**. The first two are the
|
|
1133
|
+
request's own organization by definition. The third is not — a group CFO holds
|
|
1134
|
+
her `cfo` position in the GROUP organization while the purchase order she signs
|
|
1135
|
+
off lives in the PLANT organization. `expandPositionUsers('cfo', <plant>)`
|
|
1136
|
+
matched nobody, the slot fell back to the dead `position:cfo` literal, and a
|
|
1137
|
+
group escalation could not be expressed at all.
|
|
1138
|
+
|
|
1139
|
+
An approver may now declare which organization's directory resolves it:
|
|
1140
|
+
|
|
1141
|
+
```yaml
|
|
1142
|
+
approvers:
|
|
1143
|
+
- { type: position, value: plant_manager, group: plant }
|
|
1144
|
+
- { type: position, value: cfo, organization: $root, group: finance }
|
|
1145
|
+
behavior: per_group
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
- **`$root` / `$parent`** walk D6's `parent_organization_id` tree, so the two
|
|
1149
|
+
common intents need **no deployment knowledge** — flow metadata is portable
|
|
1150
|
+
across environments while organization ids are minted per deployment. A slug
|
|
1151
|
+
covers what the symbols cannot, notably a **sibling** organization (a
|
|
1152
|
+
shared-services centre approving payables for every plant).
|
|
1153
|
+
- Declared **per approver**, so one node can require a plant manager and a
|
|
1154
|
+
group CFO in parallel. A node-level form cannot express that without
|
|
1155
|
+
splitting into serial nodes, which changes the semantics.
|
|
1156
|
+
- **Bounded, not free:** the target must share a `parent_organization_id` root
|
|
1157
|
+
with the request's organization. The rule reads only the organization tree —
|
|
1158
|
+
never the submitter — so one flow routes identically for everyone.
|
|
1159
|
+
|
|
1160
|
+
Everything else fails loudly rather than quietly:
|
|
1161
|
+
|
|
1162
|
+
- a non-`group` posture **refuses** the declaration (a `group` → `isolated`
|
|
1163
|
+
migration must not silently reroute approvals);
|
|
1164
|
+
- an approver type with no org-scoped directory (`user` / `field` / `manager` /
|
|
1165
|
+
`team`) refuses it too, and a new `approval-approver-cross-org-unsupported`
|
|
1166
|
+
lint catches that at author time;
|
|
1167
|
+
- a targeted approver holding no membership in the request's organization is
|
|
1168
|
+
dropped with a warning naming them — D2's union wall would otherwise hide the
|
|
1169
|
+
request from someone already routed to, so the node's existing
|
|
1170
|
+
`onEmptyApprovers` policy takes over instead of leaving an unopenable task.
|
|
1171
|
+
|
|
1172
|
+
Nothing changes for an approver without `organization`: same resolution, same
|
|
1173
|
+
queries, no extra reads.
|
|
1174
|
+
|
|
1175
|
+
- 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a
|
|
1176
|
+
first-class authorization dimension.
|
|
1177
|
+
|
|
1178
|
+
> This release carries BREAKING spec removals (see "Enforce-or-remove" below)
|
|
1179
|
+
> but is recorded as `minor`: every publishable package is in the Changesets
|
|
1180
|
+
> lockstep group, so one `major` would promote the whole monorepo. Breaking
|
|
1181
|
+
> changes ship as `minor` during the launch window — the migration notes below
|
|
1182
|
+
> are what reach consumers in `CHANGELOG.md`.
|
|
1183
|
+
|
|
1184
|
+
## Tenancy is now a spectrum (D1)
|
|
1185
|
+
|
|
1186
|
+
`single | group | isolated`, resolved by the `tenancy` service and selected with
|
|
1187
|
+
the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged:
|
|
1188
|
+
`OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED`
|
|
1189
|
+
(`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot
|
|
1190
|
+
rather than silently landing in a posture with no organization wall.
|
|
1191
|
+
|
|
1192
|
+
- `single` — no wall (unchanged).
|
|
1193
|
+
- `group` — **new.** Organizations are membership boundaries over one shared
|
|
1194
|
+
dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC
|
|
1195
|
+
semantics). Enforced by the OPEN engine.
|
|
1196
|
+
- `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping`
|
|
1197
|
+
probe and degraded-boot handling all unchanged.
|
|
1198
|
+
|
|
1199
|
+
## Organization scope is a first-class context field (D2)
|
|
1200
|
+
|
|
1201
|
+
`ExecutionContext.accessible_org_ids` — every organization the caller holds a
|
|
1202
|
+
currently-valid membership in (ADR-0091 validity windows) — is resolved once by
|
|
1203
|
+
`resolveAuthzContext` and carried by every transport. The `group` wall reads it
|
|
1204
|
+
directly; RLS policies may reference it as
|
|
1205
|
+
`organization_id IN (current_user.accessible_org_ids)`. An empty or absent set
|
|
1206
|
+
fails the wall closed.
|
|
1207
|
+
|
|
1208
|
+
Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still
|
|
1209
|
+
computed independently of the RLS compiler, AND-composed outermost, and
|
|
1210
|
+
crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so
|
|
1211
|
+
ADR-0095's W1/W2 invariants hold in every posture.
|
|
1212
|
+
|
|
1213
|
+
## Two P0 correctness fixes (D3, D4) — behavior changes
|
|
1214
|
+
|
|
1215
|
+
**D3 — app-authored org-scoped RLS policies are no longer silently dropped**
|
|
1216
|
+
(finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose
|
|
1217
|
+
`using` contained the substring `current_user.organization_id` when isolation was
|
|
1218
|
+
inactive, which swallowed app-authored policies as well as the platform's own.
|
|
1219
|
+
Stripping is now decided by PROVENANCE (identity against the shipped
|
|
1220
|
+
declaration). **Upgrade impact:** in a deployment with no organization wall, an
|
|
1221
|
+
app-authored policy referencing the active organization is now RETAINED and
|
|
1222
|
+
fails closed (zero rows) with a one-time warning, where it previously vanished
|
|
1223
|
+
and the object read unscoped. `getReadFilter` shared the defect, so analytics and
|
|
1224
|
+
raw-SQL consumers were affected too. If a policy was only ever meant for
|
|
1225
|
+
multi-org, delete it or install `@objectstack/organizations`.
|
|
1226
|
+
|
|
1227
|
+
**D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization
|
|
1228
|
+
boundary** (finding F2, framework#3540). Under a wall-less posture nothing
|
|
1229
|
+
bounded the wildcard superuser bits `organization_admin` carries, so a
|
|
1230
|
+
deployment that accumulated organizations (personal orgs on signup) made every
|
|
1231
|
+
owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a
|
|
1232
|
+
de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and
|
|
1233
|
+
revokes the superseded variant whenever the posture changes. **Upgrade impact:**
|
|
1234
|
+
in `single` posture an org owner/admin keeps full CRUD but loses the blanket
|
|
1235
|
+
ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains
|
|
1236
|
+
available through `admin_full_access` or an explicitly authored permission set —
|
|
1237
|
+
it just stops being a side effect of a better-auth membership role.
|
|
1238
|
+
|
|
1239
|
+
## Engine-owned organization stamping (D5)
|
|
1240
|
+
|
|
1241
|
+
Under any wall-enforcing posture the engine stamps `organization_id` from the
|
|
1242
|
+
caller's active organization on an insert that omits it, and validates every
|
|
1243
|
+
supplied value against the wall. Idempotent with the enterprise auto-stamp
|
|
1244
|
+
(neither overwrites a supplied value). This also closes a real hole: the
|
|
1245
|
+
pre-existing post-image check required a non-array payload, so a BULK insert
|
|
1246
|
+
could carry a forged `organization_id` per row. One forged row now denies the
|
|
1247
|
+
whole write.
|
|
1248
|
+
|
|
1249
|
+
## Group structure, extension fields and red-line lints (D6, D7)
|
|
1250
|
+
|
|
1251
|
+
- `sys_organization` gains `parent_organization_id` and `sort_order` — a
|
|
1252
|
+
**reporting dimension only**.
|
|
1253
|
+
- New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`,
|
|
1254
|
+
`org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` /
|
|
1255
|
+
`os validate`: an RLS policy or sharing rule that walks the org tree is an
|
|
1256
|
+
error, as is a business-unit grant on a platform-global object.
|
|
1257
|
+
- Extension fields on better-auth-managed objects ride the existing ADR-0092
|
|
1258
|
+
whitelist. A new guard derives better-auth's real field surface from
|
|
1259
|
+
`getAuthTables()` at the pinned version and fails the build on any name
|
|
1260
|
+
collision, so a library upgrade cannot silently take ownership of a column.
|
|
1261
|
+
|
|
1262
|
+
## Enforce-or-remove (D11) — BREAKING
|
|
1263
|
+
|
|
1264
|
+
Both removals are of surface that had **zero runtime consumers**, so no
|
|
1265
|
+
behavior changes; authoring them is now a no-op instead of a lint warning.
|
|
1266
|
+
|
|
1267
|
+
- **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read
|
|
1268
|
+
it. FROM → TO: a set a policy needs as `field IN (current_user.<key>)` is now
|
|
1269
|
+
supplied by a registered membership resolver (below); a constant belongs in
|
|
1270
|
+
the policy itself as a literal (`status = 'published'`).
|
|
1271
|
+
- **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`)
|
|
1272
|
+
— REMOVED.** No runtime object, stack field or resolver existed. FROM → TO:
|
|
1273
|
+
matrix requirements are served by multi-position × business-unit anchoring; a
|
|
1274
|
+
generalized dimension-security module will arrive with its own ADR.
|
|
1275
|
+
- **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has
|
|
1276
|
+
merged since ADR-0056 finally has a producer: register an
|
|
1277
|
+
`IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the
|
|
1278
|
+
`rls-membership-resolver` service, declaring the keys it owns. Fail-closed by
|
|
1279
|
+
construction — an unresolved key makes its policies drop out. Kernel-owned
|
|
1280
|
+
keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be
|
|
1281
|
+
overwritten from this seam.
|
|
1282
|
+
|
|
1283
|
+
## Edition boundary (D12)
|
|
1284
|
+
|
|
1285
|
+
The `group` posture's enforcement primitives ship OPEN — the union wall,
|
|
1286
|
+
`accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness
|
|
1287
|
+
fixes and the D6 lints — because the correctness of a wall is never a paid
|
|
1288
|
+
feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing
|
|
1289
|
+
enterprise `org-scoping` probe, so the current commercial boundary for
|
|
1290
|
+
legal-entity isolation is unchanged by this release.
|
|
1291
|
+
|
|
1292
|
+
- e2616e0: feat(spec,lint)!: remove `agent.tools[]`, lint agent authoring, and resolve `action_<name>` only when it actually materialises (#3820, ADR-0109 accepted)
|
|
1293
|
+
|
|
1294
|
+
**Breaking — `agent.tools[]` is removed.** ADR-0064's central invariant is
|
|
1295
|
+
"an agent's tool set is the union of its surface-compatible skills' tools;
|
|
1296
|
+
nothing falls through to the global registry", and this legacy inline slot
|
|
1297
|
+
was the one seam that broke it: the runtime resolved `agent.tools[].name`
|
|
1298
|
+
against the **full** tool registry with no surface check, so an `ask`-surface
|
|
1299
|
+
agent could name an authoring tool and get it. Removing the field makes the
|
|
1300
|
+
invariant structural — there is no second slot to disagree with the skills —
|
|
1301
|
+
rather than a rule every reader has to remember (ADR-0049 "design+enforce or
|
|
1302
|
+
remove"). `AIToolSchema` / the `AITool` type go with it.
|
|
1303
|
+
|
|
1304
|
+
_Migration:_ attach capability through `skills`. An agent authoring `tools` is
|
|
1305
|
+
not a parse error — Zod strips the unknown key — so existing stacks keep
|
|
1306
|
+
parsing, but the slot no longer does anything.
|
|
1307
|
+
|
|
1308
|
+
**`validate-ai-tool-references` now models AI exposure.** The rule previously
|
|
1309
|
+
resolved `action_<name>` against every declared action. The runtime is far
|
|
1310
|
+
stricter (ADR-0011): it materialises a tool only when the action opts in with
|
|
1311
|
+
`ai.exposed: true` + `ai.description` **and** has a headless path (type
|
|
1312
|
+
`script`/`api`/`flow` with a target or body — `url`/`modal`/`form` are
|
|
1313
|
+
UI-only). Resolving against all actions therefore blessed references the agent
|
|
1314
|
+
could never call — the exact failure the rule exists to catch. Unresolved
|
|
1315
|
+
`action_*` references now get their own message and fix, since "the action
|
|
1316
|
+
isn't exposed" and "the name is fictional" need different answers.
|
|
1317
|
+
|
|
1318
|
+
**New rule `validate-ai-agent-authoring`** (`agent-authoring-withdrawn`,
|
|
1319
|
+
warning): flags a stack that declares `stack.agents`. Tenant/app-package
|
|
1320
|
+
agents were withdrawn in ADR-0063 §2 — the runtime filters them from the
|
|
1321
|
+
catalog and refuses to load them — but `defineStack` still accepted the array,
|
|
1322
|
+
so an app could ship agents that parse, validate, and never run. This is the
|
|
1323
|
+
authoring-time signal that was missing (ADR-0078: loud at the producer,
|
|
1324
|
+
tolerant at the consumer). Joins `REFERENCE_INTEGRITY_RULES`.
|
|
1325
|
+
|
|
1326
|
+
ADR-0109 is now **Accepted — implemented (Phase 1)**, and the AI docs teach
|
|
1327
|
+
the zero-tool-record default path, including the three conditions that decide
|
|
1328
|
+
whether `action_<name>` exists and why a `modal` action staying human-driven
|
|
1329
|
+
is a design answer rather than a gap.
|
|
1330
|
+
|
|
1331
|
+
- 33f5e23: feat(lint): `validate-ai-surface-affinity` — skill ↔ agent surface affinity is now linted (#3820)
|
|
1332
|
+
|
|
1333
|
+
An agent binds a product surface (`'ask'` | `'build'`, ADR-0063 §1) and a skill
|
|
1334
|
+
declares which surface it belongs to (`'ask'` | `'build'` | `'both'`, §3). The
|
|
1335
|
+
runtime refuses an incompatible binding with a **load error at chat time** —
|
|
1336
|
+
after parse, validate, and deploy all passed cleanly. The new rule reports that
|
|
1337
|
+
contradiction statically, and joins `REFERENCE_INTEGRITY_RULES`, so
|
|
1338
|
+
`objectstack validate`, `lint`, and `compile` all pick it up with no CLI
|
|
1339
|
+
changes.
|
|
1340
|
+
|
|
1341
|
+
Scope is deliberately narrow (zero false positives by construction): only
|
|
1342
|
+
bindings where **both** the agent and the skill are declared in the same stack
|
|
1343
|
+
are checked. `agent.skills[]` names that don't resolve in-stack (kernel skills
|
|
1344
|
+
are runtime-registered and statically invisible) are skipped — resolving those
|
|
1345
|
+
namespaces is #3820 D0/D2, decided by ADR-0109 (Proposed).
|
|
1346
|
+
|
|
1347
|
+
The spec side is doc-truth only, no schema shape changes:
|
|
1348
|
+
|
|
1349
|
+
- `stack.agents` is documented as **platform-internal** (ADR-0063 §2 — the
|
|
1350
|
+
kernel ships exactly two agents; third parties extend via skills), replacing
|
|
1351
|
+
prose that still described the withdrawn ADR-0040 per-app-copilot model.
|
|
1352
|
+
- `stack.tools` is documented as declaration-only pending the ADR-0109 tool
|
|
1353
|
+
authoring model.
|
|
1354
|
+
- `app.defaultAgent` is re-documented as a surface-binding knob (`'ask'`
|
|
1355
|
+
implicit / `'build'` for authoring surfaces), not a custom-agent slot.
|
|
1356
|
+
- `SkillSchema` now states that a per-skill `permissions` field deliberately
|
|
1357
|
+
does not exist (ADR-0049) — authoring one is silently stripped; access is
|
|
1358
|
+
gated by `agent.access` / `agent.permissions` and per-tool authz.
|
|
1359
|
+
|
|
1360
|
+
- 259af21: feat(spec,lint): ADR-0109 Phase 1 — platform tool-name registry + advisory `skill.tools[]` reference lint (#3820 R7)
|
|
1361
|
+
|
|
1362
|
+
ADR-0109 (revised) settles the AI tool authoring model: **the default
|
|
1363
|
+
third-party path needs no tool records at all.** A skill's `tools[]` names
|
|
1364
|
+
either a platform-registered tool or a tool the runtime materialises from the
|
|
1365
|
+
app's own declarative actions (`action_<name>`) — the executable, its authz,
|
|
1366
|
+
and its audit trail stay on the action/flow the app already ships. Tool
|
|
1367
|
+
records are demoted to an optional AI-presentation refinement layer (Phase 2,
|
|
1368
|
+
gated on acceptance).
|
|
1369
|
+
|
|
1370
|
+
Phase 1, shipped here:
|
|
1371
|
+
|
|
1372
|
+
- **`PLATFORM_PROVIDED_TOOL_NAMES`** (`@objectstack/spec/system`) — curated
|
|
1373
|
+
registry of every statically-named tool the cloud AI runtime registers,
|
|
1374
|
+
grouped by owning package, plus `PLATFORM_TOOL_FAMILY_PREFIXES` for the
|
|
1375
|
+
materialised `action_` family and `isPlatformProvidedToolName()`. The
|
|
1376
|
+
`PLATFORM_PROVIDED_OBJECT_NAMES` precedent, applied to tools; conformance
|
|
1377
|
+
tests live in the owning cloud packages.
|
|
1378
|
+
- **`validate-ai-tool-references`** (`@objectstack/lint`) — the #3820 R7
|
|
1379
|
+
`skill.tools` branch, wildcard-aware, resolving against declared
|
|
1380
|
+
`stack.tools` ∪ the registry ∪ the materialised action family. Severity
|
|
1381
|
+
**warning** (ADR-0078 advisory-first ratchet): the registry cannot see
|
|
1382
|
+
third-party runtime plugins. Joins `REFERENCE_INTEGRITY_RULES`, so
|
|
1383
|
+
`validate`, `lint`, and `compile` all pick it up. On the HotCRM corpus it
|
|
1384
|
+
reports exactly the 10 fictional tool references (0 false positives on the
|
|
1385
|
+
6 that resolve).
|
|
1386
|
+
- **`composeStacks` no longer drops `tools`** — the slot joins the
|
|
1387
|
+
concatenated array fields, so a declared record survives composition.
|
|
1388
|
+
- `stack.tools` / AI-slot docs updated to the ADR-0109 model.
|
|
1389
|
+
|
|
1390
|
+
- 474fe39: feat(approvals): declare approver value bindings; retire `queue` approver authoring (#3508)
|
|
1391
|
+
|
|
1392
|
+
- `@objectstack/spec` exports `APPROVER_VALUE_BINDINGS` — the single declaration of how a
|
|
1393
|
+
designer must source each approver row's `value`: `user`/`team`/`department`/`position`
|
|
1394
|
+
are DATA-record lookups on the system directory objects (`sys_user` / `sys_team` /
|
|
1395
|
+
`sys_business_unit` / `sys_position`; `position` commits the machine **name**, the
|
|
1396
|
+
others the row id), `org_membership_level` is a closed enum (`ORG_MEMBERSHIP_LEVELS`),
|
|
1397
|
+
`manager` is auto-resolved, `field` names a trigger-object field, and `queue` is
|
|
1398
|
+
unsupported. Also exports `NON_AUTHORABLE_APPROVER_TYPES`.
|
|
1399
|
+
- `queue` approver type is deprecated-for-authoring: it still parses (stored flows keep
|
|
1400
|
+
loading and rendering) but is published in `xEnumDeprecated`, so designers stop
|
|
1401
|
+
offering it — the runtime has no queue resolution and the slot routes to nobody. The
|
|
1402
|
+
approver `value` xRef now also maps `manager`, so designers can render its
|
|
1403
|
+
auto-resolved state. No authored key is removed; nothing to migrate. If a flow carries
|
|
1404
|
+
`{ type: 'queue' }`, replace it with `team` / `department` / `position` (or a concrete
|
|
1405
|
+
`user`) until a real ownership-queue implementation lands.
|
|
1406
|
+
- `@objectstack/plugin-approvals` now warns at resolution time when a stored `queue`
|
|
1407
|
+
approver is skipped.
|
|
1408
|
+
- `@objectstack/lint` adds `approval-approver-type-unsupported` (warning) for approver
|
|
1409
|
+
types that are declared but not implemented by the runtime.
|
|
1410
|
+
|
|
1411
|
+
- 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities:
|
|
1412
|
+
|
|
1413
|
+
**`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate.
|
|
1414
|
+
|
|
1415
|
+
**`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals.
|
|
1416
|
+
|
|
1417
|
+
**Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `<nodeId>.<key>` variables — a later approval node's expression can read `vars.<nodeId>.picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot).
|
|
1418
|
+
|
|
1419
|
+
Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time.
|
|
1420
|
+
|
|
1421
|
+
- b0e5a37: fix(lint,cli): a filter reference that cannot resolve fails the build, not the run (#3426, #3810)
|
|
1422
|
+
|
|
1423
|
+
`validateFlowTemplatePaths` reported every `{record.<path>}` miss as **advisory**,
|
|
1424
|
+
on the reasoning that an unresolved token renders a blank and the run still
|
|
1425
|
+
completes. Since #3810 that reasoning no longer holds in one position: inside a
|
|
1426
|
+
CRUD node's `filter`, an unresolved token does not blank a value, it **deletes
|
|
1427
|
+
the condition** — and a removed condition matches MORE rows, not fewer. Those
|
|
1428
|
+
nodes now refuse to execute rather than run a widened query.
|
|
1429
|
+
|
|
1430
|
+
So the rule was warning about metadata whose runtime is already decided: `os
|
|
1431
|
+
validate` printed a yellow line, exited 0, and shipped a flow that cannot run.
|
|
1432
|
+
Severity now follows the runtime consequence, by position:
|
|
1433
|
+
|
|
1434
|
+
- **`filter` of `get_record` / `update_record` / `delete_record` → `error`.**
|
|
1435
|
+
These are the three nodes whose filter `resolveNodeFilter` guards. The finding
|
|
1436
|
+
says what the runtime will do ("the node refuses to run at execution time")
|
|
1437
|
+
and why the build gates rather than warns (an absent condition _widens_ the
|
|
1438
|
+
query). `os validate` exits 1.
|
|
1439
|
+
- **Every other position → `warning`, unchanged.** A message body, an `http`
|
|
1440
|
+
url, an `update_record` write payload: the token still renders a blank, the
|
|
1441
|
+
run still completes, and the head object may legitimately come from another
|
|
1442
|
+
installed package. `create_record` is deliberately excluded from the gating
|
|
1443
|
+
set — it writes a payload and has no filter to widen.
|
|
1444
|
+
|
|
1445
|
+
Both rules split this way (`flow-template-unknown-field` and
|
|
1446
|
+
`flow-template-lookup-traversal`), so a typo and a lookup hop are gated wherever
|
|
1447
|
+
the runtime refuses them. A reference used in both positions on one node is
|
|
1448
|
+
reported **once, at error severity**.
|
|
1449
|
+
|
|
1450
|
+
**`os validate` now enforces it.** The command filtered this rule's findings for
|
|
1451
|
+
`severity === 'warning'` and dropped everything else on the floor, so an error
|
|
1452
|
+
from it would have been invisible. It now gates on errors first — printing rule
|
|
1453
|
+
id and config path, and emitting them under `errors` in `--json` — mirroring the
|
|
1454
|
+
`validateReadonlyFlowWrites` step directly below, which makes the same
|
|
1455
|
+
shift-left split (a certain runtime failure gates; a state-dependent one
|
|
1456
|
+
advises).
|
|
1457
|
+
|
|
1458
|
+
Verified against the shipped examples: 33 flows across app-todo, app-crm and
|
|
1459
|
+
app-showcase produce **no new errors**; the four pre-existing lookup-traversal
|
|
1460
|
+
warnings sit in `script` / `notify` / `subflow` / `parallel` positions and keep
|
|
1461
|
+
their advisory severity.
|
|
1462
|
+
|
|
1463
|
+
No authoring change is required for a correct filter. A filter that this rule
|
|
1464
|
+
now fails is one the runtime would have refused anyway — the difference is that
|
|
1465
|
+
you find out at `os validate` instead of at 3am.
|
|
1466
|
+
|
|
1467
|
+
- fd7cfde: fix(lint,cli): the flow-template-path rule reaches `os lint` and `os compile`, not just `os validate` (#3583, #3810)
|
|
1468
|
+
|
|
1469
|
+
`validateFlowTemplatePaths` was wired by hand into `os validate` and nowhere
|
|
1470
|
+
else. That is precisely the drift `REFERENCE_INTEGRITY_RULES` exists to end
|
|
1471
|
+
(#3583 §5 D5): the same stack, checked by a different rule subset depending on
|
|
1472
|
+
which command the author happened to run.
|
|
1473
|
+
|
|
1474
|
+
It mattered more after #3861 gave the rule a gating severity. A `{record.<path>}`
|
|
1475
|
+
token in a CRUD node's `filter` that names an unknown field — or hops through an
|
|
1476
|
+
un-expanded relation — makes the runtime **refuse the node** (#3810). `os
|
|
1477
|
+
validate` failed on it; `os lint` and `os compile` did not look, so a CI job
|
|
1478
|
+
running either one would build and ship a flow that cannot execute.
|
|
1479
|
+
|
|
1480
|
+
**The rule is now a suite member.** It belongs by the suite's own admission
|
|
1481
|
+
criterion: a `{record.<field>}` token is a name written in metadata, resolved
|
|
1482
|
+
against the bound object's declared fields. One line in
|
|
1483
|
+
`REFERENCE_INTEGRITY_RULES` reaches all three commands, and the hand-wiring in
|
|
1484
|
+
`validate.ts` is deleted rather than duplicated.
|
|
1485
|
+
|
|
1486
|
+
Before landing this, the rule was run against all three stack shapes the suite
|
|
1487
|
+
is handed — raw `config` (`os lint`), `normalizeStackInput` output, and
|
|
1488
|
+
schema-parsed `result.data` (`os validate` / `os compile`) — across `app-todo`,
|
|
1489
|
+
`app-crm` and `app-showcase`. All three agree finding-for-finding, so moving the
|
|
1490
|
+
call site does not change what is reported.
|
|
1491
|
+
|
|
1492
|
+
Verified end-to-end on `app-showcase`: all three commands pass unchanged on the
|
|
1493
|
+
real stack (the four pre-existing lookup-traversal warnings still print, still
|
|
1494
|
+
advisory), and with one filter token corrupted to `{record.idd}` **all three now
|
|
1495
|
+
exit 1** — where previously only `validate` did.
|
|
1496
|
+
|
|
1497
|
+
**Also fixed, in the same file.** On a clean run, `os validate --json` never
|
|
1498
|
+
reported the reference-integrity suite's warnings: `refWarnings` was assembled,
|
|
1499
|
+
printed to the console, and included in the _failure_ payload, but omitted from
|
|
1500
|
+
the success-path `warnings` array. Adding the rule to the suite would have
|
|
1501
|
+
silently dropped its warnings from `--json` for JSON consumers, so `refWarnings`
|
|
1502
|
+
now appears there — which also surfaces the other five rules' warnings that were
|
|
1503
|
+
being discarded. Same shape of bug as the dropped errors #3861 fixed: computed,
|
|
1504
|
+
then thrown away.
|
|
1505
|
+
|
|
1506
|
+
- 9bf4588: feat(lint): flag never-firing record trigger tokens at authoring time (#3427)
|
|
1507
|
+
|
|
1508
|
+
New `flow-trigger-unknown-event` rule in `validateFlowTriggerReadiness`: a flow
|
|
1509
|
+
start node whose `triggerType` is record-lifecycle-shaped
|
|
1510
|
+
(`record-before|after-<op>`) but names an op the record-change trigger cannot map
|
|
1511
|
+
— e.g. a typo like `record-after-updated` — binds to the record-change trigger
|
|
1512
|
+
yet maps to no ObjectQL hook and never fires, with only a runtime warning. The
|
|
1513
|
+
rule surfaces that never-fire defect at `os validate` time. Warning severity;
|
|
1514
|
+
bare `record-<noun>` shapes (e.g. `record-change`) are out of scope.
|
|
1515
|
+
|
|
1516
|
+
- f022c4d: refactor(lint): one entry point for the reference-integrity suite (#3583 D5)
|
|
1517
|
+
|
|
1518
|
+
Six rules that answer the same question — "does this name resolve to anything?"
|
|
1519
|
+
— were wired by hand into three CLI commands, so landing a rule meant editing
|
|
1520
|
+
`validate`, `lint` and `compile`, and forgetting one meant the same stack got a
|
|
1521
|
+
different verdict depending on which command the author ran.
|
|
1522
|
+
|
|
1523
|
+
New public API on `@objectstack/lint`:
|
|
1524
|
+
|
|
1525
|
+
- `validateReferenceIntegrity(stack)` — runs every reference-integrity rule and
|
|
1526
|
+
returns the concatenated findings.
|
|
1527
|
+
- `REFERENCE_INTEGRITY_RULES` — the ordered list behind it (`validateObjectReferences`,
|
|
1528
|
+
`validateActionNameRefs`, `validatePageFieldBindings`, `validateChartBindings`,
|
|
1529
|
+
`validateNavAccess`, `validateTranslationReferences`).
|
|
1530
|
+
- `ReferenceIntegrityFinding` / `ReferenceIntegrityRule` / `ReferenceIntegritySeverity`
|
|
1531
|
+
— one finding type instead of a six-way union.
|
|
1532
|
+
|
|
1533
|
+
Adding a rule to that list reaches `validate`, `lint` and `compile` with no
|
|
1534
|
+
further wiring. The individual rule exports are unchanged, so nothing that
|
|
1535
|
+
imports them directly needs to move.
|
|
1536
|
+
|
|
1537
|
+
Behaviour-preserving: identical findings on the three example apps (zero) and
|
|
1538
|
+
on the HotCRM corpus (24, unchanged per rule). `os doctor` is deliberately not
|
|
1539
|
+
converted — it runs only `validateWidgetBindings` and is an environment health
|
|
1540
|
+
check rather than an authoring gate.
|
|
1541
|
+
|
|
1542
|
+
- 2343099: feat(lint): translation-bundle reference integrity + option-key validation (#3583)
|
|
1543
|
+
|
|
1544
|
+
The i18n gate only ever ran forward: `os i18n check` asks which keys the
|
|
1545
|
+
metadata expects that no bundle carries. Nothing asked the reverse — which keys
|
|
1546
|
+
a bundle carries that no metadata claims — even though the spec already names
|
|
1547
|
+
the answer (`TranslationDiffStatus 'redundant'`, `TranslationCoverageResult.redundantKeys`,
|
|
1548
|
+
both declared with no producer).
|
|
1549
|
+
|
|
1550
|
+
That direction ships two failure modes, both found in the HotCRM audit: bundles
|
|
1551
|
+
keyed to fields an object no longer declares (a rename that left the translation
|
|
1552
|
+
behind), and select-option translations keyed by the option's **display label**
|
|
1553
|
+
or a variant spelling of its value (`direct-mail` for `direct_mail`, `planned`
|
|
1554
|
+
for `planning`). Neither breaks anything — which is the problem. The resolver
|
|
1555
|
+
finds nothing and renders the source string, so the screen looks translated and
|
|
1556
|
+
one field or one picklist value quietly does not.
|
|
1557
|
+
|
|
1558
|
+
New rule `validateTranslationReferences` walks every bundle in
|
|
1559
|
+
`stack.translations` against the stack it ships with, wired into `os validate`,
|
|
1560
|
+
`os lint`, and `os compile`:
|
|
1561
|
+
|
|
1562
|
+
| Key | Must name |
|
|
1563
|
+
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
1564
|
+
| `objects.{object}` | an object this stack defines, or a platform object |
|
|
1565
|
+
| `objects.{object}.fields.{field}` | a field that object declares |
|
|
1566
|
+
| `objects.{object}.fields.{field}.options.{key}` | an option's stored `value` |
|
|
1567
|
+
| `objects.{object}._views` / `._actions` / `._sections` / `._actions.*.params` | a view `name` / bound action / `fieldGroups[].key` or named section / param `name` |
|
|
1568
|
+
| `apps.{app}` / `.navigation.{id}` | an app `name` / navigation item `id` |
|
|
1569
|
+
| `dashboards.{dash}` / `.widgets.{id}` / `.actions.{actionUrl}` | dashboard `name` / widget `id` / header `actionUrl` |
|
|
1570
|
+
| `globalActions.{action}` | an action with no `objectName` |
|
|
1571
|
+
|
|
1572
|
+
Every finding is a **warning** (`translation-target-unknown`,
|
|
1573
|
+
`translation-option-key-unknown`): an orphan key is inert, not broken, and the
|
|
1574
|
+
severity should say so. Diagnostics carry the declared names to choose from,
|
|
1575
|
+
name the stored value when a key turns out to be the display label, and suggest
|
|
1576
|
+
a namespace-segment match (`task` → `todo_task`) that edit distance alone misses.
|
|
1577
|
+
|
|
1578
|
+
Cross-package objects follow the existing ladder: a registered platform object
|
|
1579
|
+
is skipped wholly (its fields are not visible from a stack lint), a
|
|
1580
|
+
platform-prefixed name no package registers is reported once on the object key,
|
|
1581
|
+
and the subtree is never half-checked. `messages`, `validationMessages`,
|
|
1582
|
+
`settings`, `settingsCommon` and `metadataForms` are deliberately not judged —
|
|
1583
|
+
their keys are owned by application code, plugins, and the platform's own
|
|
1584
|
+
metadata-type registry, so no enumerable universe exists to resolve against.
|
|
1585
|
+
|
|
1586
|
+
- f2b8ac9: Navigation reachability vs. granted access (issue #3583, assessment R5)
|
|
1587
|
+
|
|
1588
|
+
`validate-nav-access` joins what an app's navigation exposes against
|
|
1589
|
+
`buildAccessMatrix` — the first lint consumer of the ADR-0090 D6 matrix, which
|
|
1590
|
+
previously only backed `os compile`'s snapshot gate. An object in the menu that
|
|
1591
|
+
no permission set grants read on renders as an entry and then fails
|
|
1592
|
+
permission-denied when opened: it works while you browse as an administrator
|
|
1593
|
+
(the platform's built-in `admin_full_access` carries a wildcard grant) and
|
|
1594
|
+
breaks for exactly the users the app ships permission sets for.
|
|
1595
|
+
|
|
1596
|
+
Advisory severity — a grant can legitimately come from a permission set another
|
|
1597
|
+
installed package ships. Quiet by construction in three cases: platform-provided
|
|
1598
|
+
objects (their own packages grant them), stacks that declare no permission sets
|
|
1599
|
+
at all (permissions managed elsewhere, so flagging every entry says nothing),
|
|
1600
|
+
and any stack where a set carries a wildcard `objects: { '*': … }` grant — the
|
|
1601
|
+
shape `admin_full_access` itself uses, which the access matrix records under the
|
|
1602
|
+
literal key `*`.
|
|
1603
|
+
|
|
1604
|
+
Wired into `os validate`, `os lint`, and `os compile`.
|
|
1605
|
+
|
|
1606
|
+
- 2a5f04a: `<ObjectChart>` aggregate result-column naming is now a contract, and its axis bindings are validated (issue #3701)
|
|
1607
|
+
|
|
1608
|
+
Split out of #3583 Phase 2 (#3684), which extended ADR-0021 axis checking to
|
|
1609
|
+
report charts, list-view charts, and dataset-bound page chart components but had
|
|
1610
|
+
to leave the react `<ObjectChart>` block out: it is OBJECT-bound (`objectName` +
|
|
1611
|
+
an inline `aggregate`), `aggregate` existed in the contract only as the
|
|
1612
|
+
description string `'{ field, function, groupBy }'`, and nothing in the repo said
|
|
1613
|
+
what the aggregated result columns were called. Without that, `xAxis`/`yAxis` had
|
|
1614
|
+
nothing to resolve against, and guessing a convention would have manufactured
|
|
1615
|
+
false positives (ADR-0072 D1).
|
|
1616
|
+
|
|
1617
|
+
**The convention, recorded rather than invented.** Every path that can serve an
|
|
1618
|
+
object-bound chart already agreed — the engine's structured-`groupBy` aggregate
|
|
1619
|
+
(whose alias objectui sets to `field || function`), the legacy analytics query
|
|
1620
|
+
(which remaps its measure key back to `field`), the client-side fallback, and the
|
|
1621
|
+
console's own chart-view wiring (`xAxisKey: groupBy`, `series[].dataKey: field`).
|
|
1622
|
+
`packages/spec/src/ui/chart-aggregate.ts` writes it down and exports it:
|
|
1623
|
+
|
|
1624
|
+
- an object-bound aggregate returns rows keyed by the **raw field names** —
|
|
1625
|
+
`groupBy` for the category column, `field` for the value column, the literal
|
|
1626
|
+
`count` for a fieldless count, plus `<field>__comparison` under a comparison
|
|
1627
|
+
overlay;
|
|
1628
|
+
- `chartAggregateCategoryKey` / `chartAggregateValueKey` / `chartAggregateResultKeys`
|
|
1629
|
+
derive those columns so producers and checkers cannot re-derive them apart;
|
|
1630
|
+
- `ChartAggregateSchema` replaces the description string with a real Zod schema
|
|
1631
|
+
and rejects a non-`count` function with no `field` (which used to reach the
|
|
1632
|
+
renderer as `sum(undefined)` and render blank).
|
|
1633
|
+
|
|
1634
|
+
This is the deliberate opposite of the dataset path, whose rows are keyed by the
|
|
1635
|
+
declared measure `name` (`sum_amount`) — the trap `chart-measure-unknown` catches.
|
|
1636
|
+
Only the dataset path has an author-chosen name to key by.
|
|
1637
|
+
|
|
1638
|
+
**`<ObjectChart>`'s contract now names the props it actually reads.** The block
|
|
1639
|
+
consumes `xAxisKey` and `series[].dataKey`; `ChartConfig`'s `xAxis`/`yAxis`/`series`
|
|
1640
|
+
shapes reached it and were silently dropped, which ADR-0078 forbids. They are
|
|
1641
|
+
removed from the block's `dataProps`; `chartType`, `xAxisKey`, and `series` are
|
|
1642
|
+
declared in the React overlay where the other bindings live.
|
|
1643
|
+
|
|
1644
|
+
**`validate-react-page-props` now reads attribute VALUES**, not just names, for
|
|
1645
|
+
`<ObjectChart>`:
|
|
1646
|
+
|
|
1647
|
+
- `react-chart-field-unknown` (error) — `aggregate.field` / `aggregate.groupBy`
|
|
1648
|
+
naming a field the bound object does not declare;
|
|
1649
|
+
- `react-chart-aggregate-invalid` (error) — an unimplemented aggregation
|
|
1650
|
+
function, or a non-`count` function with nothing to aggregate;
|
|
1651
|
+
- `react-chart-axis-unknown` (error) — `xAxisKey` / `series[].dataKey` naming a
|
|
1652
|
+
column the aggregate does not return (including a dataset-style `sum_total`),
|
|
1653
|
+
or a category axis bound to the value column;
|
|
1654
|
+
- `react-chart-axis-inert` (warning) — the `xAxis` / `yAxis` shapes this block
|
|
1655
|
+
never reads.
|
|
1656
|
+
|
|
1657
|
+
Value reading is opt-in per block and evaluates only static literals: a prop
|
|
1658
|
+
driven by React state or a variable, a usage carrying a `{...spread}`, a chart
|
|
1659
|
+
given inline `data`, and objects another package defines are all skipped
|
|
1660
|
+
silently — an unresolvable binding is not a wrong one.
|
|
1661
|
+
|
|
1662
|
+
- 4f740b0: `<ObjectChart>`'s author contract is the spec `ChartConfig` shape again (issue #3729)
|
|
1663
|
+
|
|
1664
|
+
#3701 trimmed `xAxis`/`yAxis`/`series` out of the `<ObjectChart>` contract
|
|
1665
|
+
because the renderer read `xAxisKey`/`series[].dataKey` and silently dropped the
|
|
1666
|
+
ChartConfig shapes — an honest record of the runtime gap, not the target state.
|
|
1667
|
+
objectui#2880 closed the gap the other way round (the renderer now honors
|
|
1668
|
+
`ChartConfig` through one normalization boundary), so the contract follows the
|
|
1669
|
+
protocol again (ADR-0082 D1: the spec schema IS the protocol).
|
|
1670
|
+
|
|
1671
|
+
**Contract.** `type`, `xAxis`, `yAxis`, `series`, `subtitle`, `showDataLabels`,
|
|
1672
|
+
`annotations` and `interaction` are published from `ChartConfigSchema`; the
|
|
1673
|
+
internal `chartType`/`xAxisKey`/`series[].dataKey` spellings leave the author
|
|
1674
|
+
contract. `annotations` and `interaction` gained the `.describe()` they never
|
|
1675
|
+
had, so the generated contract stops publishing bare `object[]` with no meaning.
|
|
1676
|
+
|
|
1677
|
+
**The `type` exception.** `ChartConfig.type` is the chart family, but on any
|
|
1678
|
+
surface that flattens chart config into a props bag `type` is already the SDUI
|
|
1679
|
+
envelope's component discriminator — an author writing `type="bar"` used to
|
|
1680
|
+
replace `object-chart` and the block stopped resolving. The collision is created
|
|
1681
|
+
by the flattening and is resolved there (objectui's react-page wrapper), so the
|
|
1682
|
+
contract can publish `type` as the spec spells it. The contract generator's
|
|
1683
|
+
blanket `type` skip is now overridable by an explicit `dataProps` allow-list,
|
|
1684
|
+
since for this one block `type` is a real author prop.
|
|
1685
|
+
|
|
1686
|
+
**Lint.** `validate-react-page-props` reads the axes in the spec spelling —
|
|
1687
|
+
`xAxis.field`, `yAxis[].field`, `series[].name` — and keeps accepting the
|
|
1688
|
+
internal spellings silently, because dashboards and the console's own chart-view
|
|
1689
|
+
wiring emit them. `react-chart-axis-inert` is retired: the props it warned about
|
|
1690
|
+
are honored now, so the warning would be false. The three binding-integrity
|
|
1691
|
+
rules from #3701 are unchanged.
|
|
1692
|
+
|
|
1693
|
+
**Spec.** `chart-aggregate.ts` records the constraint the whole result-column
|
|
1694
|
+
convention rests on: an inline `aggregate` is SINGLE-MEASURE. Keying rows by the
|
|
1695
|
+
raw field name only works because there is exactly one measure to key; two
|
|
1696
|
+
measures over one field would collide, and resolving that needs an author-chosen
|
|
1697
|
+
name per measure — which is what a dataset is. Widening `ChartAggregateSchema`
|
|
1698
|
+
into a measures array would silently invalidate every axis binding these rules
|
|
1699
|
+
validate, so the boundary is now written down rather than left to be rediscovered.
|
|
1700
|
+
|
|
1701
|
+
The chart taxonomy note is corrected too: grouped/stacked bar and stacked area
|
|
1702
|
+
are absent from `ChartTypeSchema` not because they render as their base chart,
|
|
1703
|
+
but because stacking is a property of the SERIES (`ChartSeries.stack`), not a
|
|
1704
|
+
chart family — one `bar` family plus a series stack group expresses all three.
|
|
1705
|
+
`ChartInteraction.zoom` is now marked declared-not-delivered in its own
|
|
1706
|
+
description rather than reading as shipped.
|
|
1707
|
+
|
|
1708
|
+
- 17749fc: Page-component field bindings and non-dashboard chart bindings (issue #3583, Phase 2)
|
|
1709
|
+
|
|
1710
|
+
Two more reference-integrity rules from the #3583 assessment, both wired into
|
|
1711
|
+
`os validate`, `os lint`, and `os compile`.
|
|
1712
|
+
|
|
1713
|
+
**`validate-page-field-bindings`** — `PageComponent.properties` is an untyped
|
|
1714
|
+
bag, so a highlights strip, KPI card, or details section can name a field the
|
|
1715
|
+
bound object does not have; the component silently skips it. Which object a
|
|
1716
|
+
component binds follows `dataSource.object` → `properties.object` → the page's
|
|
1717
|
+
`object`, so multi-object pages are checked per element. `record:related_list`
|
|
1718
|
+
resolves its columns/sort/filter against the **related** object and its
|
|
1719
|
+
add-picker against that picker's own object. Advisory (matching
|
|
1720
|
+
`FORM_FIELD_UNKNOWN`). Relationship paths, system fields, cross-package objects,
|
|
1721
|
+
and unregistered component types are skipped.
|
|
1722
|
+
|
|
1723
|
+
**`validate-chart-bindings`** — extends ADR-0021 axis checking past dashboards to
|
|
1724
|
+
report charts (`report.chart` and `report.blocks[].chart`), list-view charts
|
|
1725
|
+
(`views[].list`, `views[].listViews.*`, `objects[].listViews.*`), and
|
|
1726
|
+
dataset-bound page chart components. An axis naming a raw field instead of a
|
|
1727
|
+
declared measure is an **error** (the series comes back empty); an axis naming a
|
|
1728
|
+
declared-but-unselected measure is a **warning**. The report shape needed its own
|
|
1729
|
+
handling: `ReportChartSchema` narrows `xAxis`/`yAxis` to bare strings, which the
|
|
1730
|
+
dashboard rule's array guard skips silently. The react `<ObjectChart>` block is
|
|
1731
|
+
object-bound, not dataset-bound, and is deliberately left out — nothing defines
|
|
1732
|
+
what its aggregate names the result column.
|
|
1733
|
+
|
|
1734
|
+
**Fixes:** the page walk used by `validate-action-name-refs` read a top-level
|
|
1735
|
+
`page.components` array, which `PageSchema` does not have — components live under
|
|
1736
|
+
`regions[].components[]` and `slots`, and sub-trees nest inside the untyped
|
|
1737
|
+
`properties` bag (`children`, `items[].children`, `body`, `footer`) rather than a
|
|
1738
|
+
`children` key on the component. The rule was therefore visiting nothing on a
|
|
1739
|
+
schema-parsed stack. Traversal now lives in one shared, tested module; on the
|
|
1740
|
+
showcase app it reaches 194 components where the previous shape found 46.
|
|
1741
|
+
Source-authored pages (`kind: 'html' | 'react' | 'jsx'`) are skipped — their
|
|
1742
|
+
`regions` hold a derived cache the `source` wins over.
|
|
1743
|
+
|
|
1744
|
+
- 4340f13: feat(lint,cli): flag flow `update_record` writes to readonly fields at design time (#3425)
|
|
1745
|
+
|
|
1746
|
+
A flow `update_record` node that writes a field the target object declares
|
|
1747
|
+
`readonly: true`, under the default `runAs: 'user'` identity, is a **silent
|
|
1748
|
+
no-op**: the objectql engine strips static-`readonly` fields from a non-system
|
|
1749
|
+
UPDATE payload (#2948), so the intended write never lands — yet the step still
|
|
1750
|
+
reports `success`. #3407/#3413 surfaced the strip as a run-time step warning;
|
|
1751
|
+
this moves the discovery **left** to `os validate` / `os build` so an author
|
|
1752
|
+
finds the mismatch at design time instead of by reading server WARN logs days
|
|
1753
|
+
later.
|
|
1754
|
+
|
|
1755
|
+
- New `@objectstack/lint` rule `validateReadonlyFlowWrites(stack)` — a pure
|
|
1756
|
+
`(stack) => Finding[]` check (ADR-0019). A static `readonly:true` field
|
|
1757
|
+
written by a literal `update_record` under `runAs !== 'system'` is a
|
|
1758
|
+
100%-certain no-op → **error** (gates the build). A `readonlyWhen` field is
|
|
1759
|
+
per-record-state → **warning** (advisory). Deliberately narrow to stay
|
|
1760
|
+
false-positive-free: `create_record` (INSERT is engine-exempt from the strip),
|
|
1761
|
+
`runAs: 'system'` flows (the intended "automation maintains it" channel),
|
|
1762
|
+
templated object names, and non-literal `fields` maps are all skipped.
|
|
1763
|
+
- Wired into `os validate` and `os compile`/`os build`, mirroring the existing
|
|
1764
|
+
security-posture gate (errors fail; advisories print dimmed).
|
|
1765
|
+
|
|
1766
|
+
The formal contract, unchanged in behavior: `readonly` governs the end-user /
|
|
1767
|
+
API surface (REST/UI and `runAs:'user'` flows strip it); trusted system writers
|
|
1768
|
+
(`runAs:'system'`, system hooks, seeds) maintain it. To let a flow maintain a
|
|
1769
|
+
readonly field, declare `runAs: 'system'`.
|
|
1770
|
+
|
|
1771
|
+
- f163028: Reference-integrity validation for object and action names (issue #3583)
|
|
1772
|
+
|
|
1773
|
+
A HotCRM audit found ~20 shipped instances of one bug class — metadata naming
|
|
1774
|
+
something that does not exist — all passing `objectstack validate` / `lint`
|
|
1775
|
+
cleanly and failing silently at runtime. This closes the object-name and
|
|
1776
|
+
action-name half of that class.
|
|
1777
|
+
|
|
1778
|
+
**New — `@objectstack/spec`:** `PLATFORM_PROVIDED_OBJECT_NAMES`, a curated
|
|
1779
|
+
registry of every object name contributed by a platform package, official
|
|
1780
|
+
plugin, or the cloud runtime, plus `isPlatformProvidedObjectName()` and
|
|
1781
|
+
`hasPlatformObjectPrefix()`. This replaces the `startsWith('sys_')` prefix guess
|
|
1782
|
+
that could not tell `sys_user` (real) from `sys_approval_process` (fictional —
|
|
1783
|
+
removed by ADR-0019, registered by nothing), which is why every fictional
|
|
1784
|
+
platform-prefixed reference shipped. A conformance test scans each package's
|
|
1785
|
+
`*.object.ts` declarations and fails if the registry drifts.
|
|
1786
|
+
|
|
1787
|
+
**New lint rules** (wired into both `os validate` and `os lint`):
|
|
1788
|
+
|
|
1789
|
+
- `validate-object-references` — action-param `reference` / `objectOverride`,
|
|
1790
|
+
dashboard `globalFilters[].optionsFrom.object`, and navigation
|
|
1791
|
+
`requiresObject` gates. Severity follows resolvability: an unresolved
|
|
1792
|
+
_unprefixed_ name is a typo (**error** — `object: 'user'` where the platform
|
|
1793
|
+
object is `sys_user`); an unresolved _platform-prefixed_ name is **advisory**,
|
|
1794
|
+
since a third-party package may still provide it.
|
|
1795
|
+
- `validate-action-name-refs` — the surfaces that bind an action BY NAME:
|
|
1796
|
+
list-view `bulkActions` / `rowActions`, page `record:quick_actions`
|
|
1797
|
+
`actionNames`, and nav action items. A name matching no defined action is an
|
|
1798
|
+
**error** (the button renders and does nothing), matching the existing
|
|
1799
|
+
dashboard-action-target rule.
|
|
1800
|
+
|
|
1801
|
+
**Fixes:**
|
|
1802
|
+
|
|
1803
|
+
- `defineStack` cross-reference validation now walks `app.areas[].navigation` —
|
|
1804
|
+
an areas-based app previously got no navigation checking at all — and recurses
|
|
1805
|
+
into `children` on `object` nav items, not only `group` ones.
|
|
1806
|
+
- `os lint` i18n coverage now reads field `options` in the canonical
|
|
1807
|
+
`{value,label}[]` array shape; it only handled the record map, so option-label
|
|
1808
|
+
coverage silently never fired for canonically-shaped select fields.
|
|
1809
|
+
- Hook `condition` expressions are now field-checked when `object` is an ARRAY
|
|
1810
|
+
of targets (previously only a single string target was checked, so a
|
|
1811
|
+
multi-target hook filtering on a nonexistent field passed clean). Per-target
|
|
1812
|
+
diagnostics are de-duplicated.
|
|
1813
|
+
- A dashboard widget binding no `dataset` at all is now reported instead of
|
|
1814
|
+
silently bypassing every binding and chart check on the raw-config
|
|
1815
|
+
(`lint`/`doctor`) paths. `dataset` is schema-required, so this matches what
|
|
1816
|
+
the parsed paths already enforce.
|
|
1817
|
+
|
|
1818
|
+
### Patch Changes
|
|
1819
|
+
|
|
1820
|
+
- 1bd5652: feat(auth): give ADR-0105 D8's scope-bounded issuance a caller — the
|
|
1821
|
+
`delegated_admin` org role, capped so it cannot mint authority (#3697)
|
|
1822
|
+
|
|
1823
|
+
D8 authorizes invitation _placement_ against the issuer's `adminScope`
|
|
1824
|
+
(ADR-0090 D12), so a delegated plant admin may invite only into their own
|
|
1825
|
+
subtree. That gate is implemented, unit-proven and reachable — but no principal
|
|
1826
|
+
could reach it in a state where it did anything:
|
|
1827
|
+
|
|
1828
|
+
- better-auth grants `invitation: ["create"]` to `owner` and `admin` only
|
|
1829
|
+
(`memberAc` holds `invitation: []`, which every other registered role
|
|
1830
|
+
inherits);
|
|
1831
|
+
- under a wall-enforcing posture, owners and admins are auto-elevated to
|
|
1832
|
+
`organization_admin` (`auto-org-admin-grant.ts`), which carries the wildcard
|
|
1833
|
+
`modifyAllRecords` that makes `isTenantAdmin()` true — and the gate
|
|
1834
|
+
short-circuits on tenant admins.
|
|
1835
|
+
|
|
1836
|
+
The two sets were disjoint. Issuance placement was bounded by the Layer 0 org
|
|
1837
|
+
wall (real, and correct) but never by `adminScope`, so D8's motivating story —
|
|
1838
|
+
"a plant admin invites into their own subtree without a platform admin
|
|
1839
|
+
finishing the job" — could not happen.
|
|
1840
|
+
|
|
1841
|
+
**Two pieces, and they only ship together.**
|
|
1842
|
+
|
|
1843
|
+
**1. The role.** `delegated_admin` is now registered with the organization
|
|
1844
|
+
plugin as `memberAc.statements` plus `invitation: ["create"]` — the one
|
|
1845
|
+
membership grade that may reach `/organization/invite-member` without being an
|
|
1846
|
+
org admin. Deliberately _not_ `invitation: ["cancel"]`: better-auth's cancel
|
|
1847
|
+
route checks the permission with no inviterId attribution, so it would mean
|
|
1848
|
+
"cancel anyone's pending invitation in the org".
|
|
1849
|
+
|
|
1850
|
+
The role carries no ObjectStack authority by construction — `mapMembershipRole`
|
|
1851
|
+
passes it through as a position name, and with no `sys_position_permission_set`
|
|
1852
|
+
binding that name resolves to nothing. Role = _can reach the endpoint_;
|
|
1853
|
+
`adminScope` = _what the endpoint permits_.
|
|
1854
|
+
|
|
1855
|
+
`sys_member.role` and `sys_invitation.role` each gain `delegated_admin` as a
|
|
1856
|
+
fourth option. Those selects are **enforced on write** — better-auth's own
|
|
1857
|
+
invitation and membership inserts are validated like any other row — so
|
|
1858
|
+
registering the role with the org plugin without listing it in both would have
|
|
1859
|
+
produced a role nobody could hold and nobody could hand out
|
|
1860
|
+
(`ValidationError: role must be one of: owner, admin, member`). That is exactly
|
|
1861
|
+
how the end-to-end regression caught it, twice; neither unit test could. The
|
|
1862
|
+
three non-English translation bundles carry the English label for the new option
|
|
1863
|
+
until localized.
|
|
1864
|
+
|
|
1865
|
+
**2. The role cap**, in the framework's own `beforeCreateInvitation` hook,
|
|
1866
|
+
beside the D8 placement gate. Registering the role alone would have been a
|
|
1867
|
+
four-step privilege escalation: better-auth's only role-level cap on _what role
|
|
1868
|
+
you may invite someone as_ is its `creatorRole` check (default `owner`), which
|
|
1869
|
+
blocks inviting an **owner** but not an **admin** — and an accepted `admin`
|
|
1870
|
+
membership is auto-elevated to `organization_admin` → `isTenantAdmin()`. A
|
|
1871
|
+
subtree-scoped delegate could have manufactured a tenant admin, with every
|
|
1872
|
+
existing defense off the path (`sys_member` is not a `GOVERNED_OBJECT`, and the
|
|
1873
|
+
acceptance-time membership write runs under better-auth's context, not the
|
|
1874
|
+
issuer's).
|
|
1875
|
+
|
|
1876
|
+
The cap refuses an invitation whose role outranks the issuer's own, and
|
|
1877
|
+
restricts a below-admin issuer to plain `member` — not merely "not admin/owner",
|
|
1878
|
+
because an app-registered role projects into `current_user.positions` and may be
|
|
1879
|
+
bound to permission sets, making it a capability channel too. A delegate's
|
|
1880
|
+
channel for capability is the invitation's _placement_ intent, which the D12
|
|
1881
|
+
gate allowlists position-by-position. The cap applies to every invitation,
|
|
1882
|
+
placement-carrying or not (the escalation is independent of placement), and
|
|
1883
|
+
fails closed: an issuer role that cannot be resolved confers nothing above a
|
|
1884
|
+
plain member.
|
|
1885
|
+
|
|
1886
|
+
**What changes for deployments.** One new class of principal exists: members
|
|
1887
|
+
holding the `delegated_admin` org role, who can invite into the org — as
|
|
1888
|
+
`member` only, into the subtree their `adminScope` allows. It is opt-in twice
|
|
1889
|
+
over (someone must set the membership role _and_ grant an adminScope set), so a
|
|
1890
|
+
default deployment changes not at all. Org owners and admins are unaffected.
|
|
1891
|
+
|
|
1892
|
+
Also exported: `MEMBERSHIP_ROLE_DELEGATED_ADMIN` from `@objectstack/spec`, so
|
|
1893
|
+
console and control-plane surfaces name the role from one place.
|
|
1894
|
+
|
|
1895
|
+
- 9dcc0ae: fix(automation): array-form flow `triggerType` fails loudly instead of silently never firing (#3481)
|
|
1896
|
+
|
|
1897
|
+
An array `triggerType` on a flow start node — the shape an author (or an AI
|
|
1898
|
+
authoring pass) naturally reaches for to fire on more than one event, e.g.
|
|
1899
|
+
|
|
1900
|
+
```ts
|
|
1901
|
+
config: { objectName: 'app_task', triggerType: ['record-after-create', 'record-after-delete'] }
|
|
1902
|
+
```
|
|
1903
|
+
|
|
1904
|
+
was accepted everywhere and armed nowhere. Multi-event unions are deliberately
|
|
1905
|
+
unsupported (only the single tokens plus the `record-after-write` create-OR-update
|
|
1906
|
+
union exist — see #3457), but nothing said so: `defineFlow` passed the array
|
|
1907
|
+
(start-node `config` is an open record), the engine's `typeof === 'string'` check
|
|
1908
|
+
folded it to no trigger and misclassified the flow as **manual**, so it never
|
|
1909
|
+
entered the trigger-binding audit, and the flow-trigger-readiness lint used the
|
|
1910
|
+
same `typeof` narrowing and produced no finding. The flow bound to nothing and
|
|
1911
|
+
never fired, with zero output at any layer — the same silent-never-fire class as
|
|
1912
|
+
#3427 / #3472, and the last authoring shape still slipping past every guard.
|
|
1913
|
+
|
|
1914
|
+
This is a **defensive** fix — arrays remain unsupported; they now fail loudly:
|
|
1915
|
+
|
|
1916
|
+
- **lint** (`validate-flow-trigger-readiness`): an array `triggerType` containing
|
|
1917
|
+
any `record-*` element now yields a `flow-trigger-unknown-event` warning at
|
|
1918
|
+
`os validate` time, steering to `record-after-write` (for created-or-updated) or
|
|
1919
|
+
one flow per event.
|
|
1920
|
+
- **engine** (`resolveTriggerBinding`): such an array is routed to the
|
|
1921
|
+
`record_change` trigger — exactly as an unmappable single token is — instead of
|
|
1922
|
+
being folded to a manual flow, so it reaches the trigger's bind-time rejection.
|
|
1923
|
+
- **trigger** (`record-change`): the bind-time rejection detects the array shape
|
|
1924
|
+
and emits a targeted warning (naming the flow, pointing at `record-after-write`
|
|
1925
|
+
and #3457) rather than the generic unknown-token line.
|
|
1926
|
+
|
|
1927
|
+
- 5b89711: feat(spec,lint): freeze the `{current_user_id}` filter vocabulary and fail the build on unresolvable placeholders (#3574)
|
|
1928
|
+
|
|
1929
|
+
A dashboard widget filtered on `{current_user}` rendered `0`. Not an error — a
|
|
1930
|
+
zero, indistinguishable from a metric that is legitimately empty, with nothing
|
|
1931
|
+
in the console or the server log. `service_dashboard.my_open_cases_by_priority`
|
|
1932
|
+
in the HotCRM template had shipped broken this way since the day it was
|
|
1933
|
+
written.
|
|
1934
|
+
|
|
1935
|
+
The token had never been part of the contract. Date macros were frozen in
|
|
1936
|
+
`date-macros.zod.ts` with a spec vocabulary, a lint-usable predicate, and a
|
|
1937
|
+
single client resolver; `{current_user_id}` had only prose in an `app.zod.ts`
|
|
1938
|
+
JSDoc and three ad-hoc client implementations that each handled one surface's
|
|
1939
|
+
filter shape. Nothing could tell an author their token was wrong.
|
|
1940
|
+
|
|
1941
|
+
- **`@objectstack/spec`** — new `data/context-tokens.zod.ts` freezing
|
|
1942
|
+
`CONTEXT_TOKENS` (`current_user_id`, `current_org_id`) as the sibling of
|
|
1943
|
+
`DATE_MACRO_TOKENS`, with `isContextToken` / `isKnownFilterToken` /
|
|
1944
|
+
`classifyFilterToken` and a `CONTEXT_TOKEN_SUGGESTIONS` near-miss table. The
|
|
1945
|
+
module documents what the tokens are _not_: presentation scope, never an
|
|
1946
|
+
access boundary — that is RLS, which uses the unrelated `current_user.id`
|
|
1947
|
+
expression root.
|
|
1948
|
+
- **`@objectstack/lint`** — new `validateFilterTokens` (rule
|
|
1949
|
+
`filter-token-unknown`, severity `error`). It walks `filter` / `filters` /
|
|
1950
|
+
`runtimeFilter` subtrees across dashboards, objects, views, reports,
|
|
1951
|
+
datasets, pages and apps, and reports any placeholder that resolves in
|
|
1952
|
+
neither vocabulary. It scans for filter _keys_ rather than enumerating known
|
|
1953
|
+
surfaces, so a new surface following the convention is covered the day it
|
|
1954
|
+
ships — enumerating surfaces is how the dashboard was missed in the first
|
|
1955
|
+
place. Navigation `recordId` / `params` are deliberately out of scope: they
|
|
1956
|
+
resolve `AppContextSelector` ids, which are meaningless in a filter.
|
|
1957
|
+
- **`@objectstack/cli`** — the gate runs in `os validate` and `os compile`.
|
|
1958
|
+
|
|
1959
|
+
It is an error rather than a warning because of who authors this metadata. An
|
|
1960
|
+
AI reads a query returning `0` as a correct answer and builds on it; its
|
|
1961
|
+
correction loop is author → validate → fix, so a diagnostic only reaches it if
|
|
1962
|
+
it can fail the build. The three spellings the suggestion table covers —
|
|
1963
|
+
`{current_user}`, `{user_id}`, `{organization_id}` — are each correct
|
|
1964
|
+
_somewhere else_ in the platform, which is exactly why authors reach for them.
|
|
1965
|
+
|
|
1966
|
+
Also fixes a `ViewSchema` JSDoc example that documented `{user_id}`, a token
|
|
1967
|
+
that resolves nowhere.
|
|
1968
|
+
|
|
1969
|
+
- de9af8a: fix(automation,objectql): a filter that loses a condition must not run (#3810)
|
|
1970
|
+
|
|
1971
|
+
Three related holes, all of which end in "the query matched rows the author
|
|
1972
|
+
excluded".
|
|
1973
|
+
|
|
1974
|
+
**1. A flow filter could silently widen to match everything.**
|
|
1975
|
+
|
|
1976
|
+
The flow template interpolator expresses "this token did not resolve" as
|
|
1977
|
+
`undefined`. In a message that renders as empty text — harmless. In a FILTER it
|
|
1978
|
+
removes the condition, and a removed condition matches MORE rows. When it was
|
|
1979
|
+
the only condition, `{ owner: '{record.ownr}' }` became `{}`, and `{}` handed to
|
|
1980
|
+
`deleteMany` is every row in the table.
|
|
1981
|
+
|
|
1982
|
+
So one mistyped field name in a `delete_record` node silently emptied the
|
|
1983
|
+
object. Reproduced with all four causes: a typo (`{record.ownr}`), an input the
|
|
1984
|
+
run never received, a lookup hop (`{record.account.name}` — the trigger record
|
|
1985
|
+
carries a scalar id), and a filter placeholder.
|
|
1986
|
+
|
|
1987
|
+
`get_record` / `update_record` / `delete_record` now refuse to execute when
|
|
1988
|
+
interpolation erased any authored condition, naming the offending template. The
|
|
1989
|
+
guard keys on LOSS, not emptiness: an author who deliberately wrote no filter is
|
|
1990
|
+
unaffected, and losing one of two conditions still fails, because widening from
|
|
1991
|
+
"my open records" to "all open records" is the same class of bug.
|
|
1992
|
+
|
|
1993
|
+
**2. Filter placeholders never reached the engine that resolves them.**
|
|
1994
|
+
|
|
1995
|
+
`config.filter` is where two `{…}` dialects meet — the flow template dialect
|
|
1996
|
+
(`{record.owner}`) and the filter placeholder dialect (`{current_year_start}`,
|
|
1997
|
+
`{current_user_id}`, resolved by `resolveFilterTokens()`). Evaluation order
|
|
1998
|
+
picked the winner by accident: the flow interpolator ran first, found no flow
|
|
1999
|
+
variable by that name, and erased it.
|
|
2000
|
+
|
|
2001
|
+
`interpolateFilter()` hands that position back to the dialect that owns it — a
|
|
2002
|
+
whole-string token that no flow variable resolves and that IS a recognised
|
|
2003
|
+
placeholder passes through verbatim for the engine to expand. Flow variables
|
|
2004
|
+
keep precedence, so a template that works today cannot change meaning.
|
|
2005
|
+
|
|
2006
|
+
**3. The engine resolved placeholders on reads but not on writes.**
|
|
2007
|
+
|
|
2008
|
+
`resolveFilterTokens()` reached `find`/`findOne`/`count`/`aggregate` only. So
|
|
2009
|
+
the SAME filter selected different rows depending on the verb: `find({ owner:
|
|
2010
|
+
'{current_user_id}' })` matched the signed-in user's rows, while
|
|
2011
|
+
`update`/`delete` compared the literal token text and matched none — a flow that
|
|
2012
|
+
previewed with one and acted with the other operated on two different row sets.
|
|
2013
|
+
This is the #3106 shape one layer down: the evaluator existed, only some call
|
|
2014
|
+
sites reached it.
|
|
2015
|
+
|
|
2016
|
+
`update` and `delete` now resolve too, BEFORE the by-id fast path claims a
|
|
2017
|
+
scalar `where.id` (otherwise an unresolved `{current_user_id}` would be bound as
|
|
2018
|
+
the primary key itself). Caller options are never mutated.
|
|
2019
|
+
|
|
2020
|
+
- 5524f84: feat(automation): opt-in single-hop lookup expansion for record-change flow templates (#3475)
|
|
2021
|
+
|
|
2022
|
+
A record-change flow can now declare `expand: ['<lookup_field>', …]` on its start
|
|
2023
|
+
node config so node templates resolve `{record.<lookup>.<field>}` (e.g.
|
|
2024
|
+
`{record.account.name}` in a notify title, closing the #3426 gap for lookups).
|
|
2025
|
+
|
|
2026
|
+
The engine re-reads the declared relations AFTER identity resolution, as the
|
|
2027
|
+
run's OWN principal — `resolveRunDataContext` honors `runAs`, so a `runAs:'user'`
|
|
2028
|
+
run reads the referenced object as the **triggering user** (its RLS/FLS enforced)
|
|
2029
|
+
rather than system-elevated. This is what made expansion unsafe to do in the
|
|
2030
|
+
trigger's re-read (which has no resolved grants) and is why it lives in the
|
|
2031
|
+
engine (new `AutomationEngine.setRecordExpander`, bridged by the plugin to the
|
|
2032
|
+
same data engine the CRUD nodes use).
|
|
2033
|
+
|
|
2034
|
+
Only the declared relation keys are grafted onto the run record, so bare lookup
|
|
2035
|
+
ids and `multiple` lookup arrays (#1872) on other relations — and the formula
|
|
2036
|
+
fields the trigger already hydrated — are untouched. Opt-in ⇒ zero cost when
|
|
2037
|
+
unused; best-effort ⇒ a re-read failure leaves the record unexpanded and never
|
|
2038
|
+
breaks the flow.
|
|
2039
|
+
|
|
2040
|
+
The `os validate` lint rule `flow-template-lookup-traversal` (#3426/#3472) is now
|
|
2041
|
+
suppressed for a relation once the flow declares it in `config.expand`.
|
|
2042
|
+
|
|
2043
|
+
- 169b58a: fix(#3426): build-time warning for unresolvable flow template paths + guard the formula re-read
|
|
2044
|
+
|
|
2045
|
+
Two follow-ups to #3426 (the formula/lookup `{record.<path>}` template gap that #3445 began closing).
|
|
2046
|
+
|
|
2047
|
+
**Build-time signal (the issue's fallback ask).** `os validate` now flags a
|
|
2048
|
+
record-change flow node whose `{record.<path>}` template cannot resolve —
|
|
2049
|
+
turning the previous SILENT blank into an advisory warning. Two cases, via the
|
|
2050
|
+
new `@objectstack/lint` rule `validateFlowTemplatePaths`:
|
|
2051
|
+
|
|
2052
|
+
- `flow-template-unknown-field` — `{record.<x>}` where `<x>` is neither a
|
|
2053
|
+
declared field nor a system column (a typo like `{record.full_naem}`).
|
|
2054
|
+
- `flow-template-lookup-traversal` — `{record.<lookup>.<field>}`, a cross-object
|
|
2055
|
+
hop the seeded record carries only as a scalar id (still unsupported; tracked
|
|
2056
|
+
on #3426).
|
|
2057
|
+
|
|
2058
|
+
Deliberately quiet: formula fields, bare lookup ids, numeric indexes into
|
|
2059
|
+
`multiple` lookups (#1872), `json` sub-paths, and system columns are NOT flagged,
|
|
2060
|
+
and flows bound to an object this stack does not define are skipped (no schema to
|
|
2061
|
+
compare against).
|
|
2062
|
+
|
|
2063
|
+
**Hydration re-read guards.** The `trigger-record-change` computed-field re-read
|
|
2064
|
+
(#3445) is now (a) skipped when the object declares no `formula` field — the only
|
|
2065
|
+
thing it adds — via the engine's optional `getObjectConfig`, and (b) memoized per
|
|
2066
|
+
write on the shared HookContext, so N flows on one written record share ONE
|
|
2067
|
+
re-read instead of N. Any uncertainty falls back to the prior unconditional
|
|
2068
|
+
re-read (correctness over the optimization).
|
|
2069
|
+
|
|
2070
|
+
- 7f4a8a1: fix(lint): flag every never-firing `record-`-prefixed trigger token, incl. `record-change` (#3427)
|
|
2071
|
+
|
|
2072
|
+
Generalizes the `flow-trigger-unknown-event` rule: it now flags ANY `record-`-prefixed
|
|
2073
|
+
`triggerType` that is not a valid firing token
|
|
2074
|
+
(`record-{before,after}-{create,insert,update,delete,write}`) — not just
|
|
2075
|
+
`record-(before|after)-<bad-op>` typos. This closes the `record-change` trap: the
|
|
2076
|
+
engine routes `record-change` ("Record changed (any)") to the record-change trigger,
|
|
2077
|
+
which maps it to no hook so it never fires — now caught at `os validate` time instead
|
|
2078
|
+
of only a runtime warn. Also covers bad-phase tokens like `record-during-update`.
|
|
2079
|
+
Warning severity, unchanged.
|
|
2080
|
+
|
|
2081
|
+
- 0045682: feat(auth)!: membership grade is not a capability channel — the `sys_member.role`
|
|
2082
|
+
vocabulary is closed (ADR-0108, #3723)
|
|
2083
|
+
|
|
2084
|
+
`sys_member.role` answers "what is your standing in this organization". It does
|
|
2085
|
+
not answer "what may you do" — that is what positions are for. One column was
|
|
2086
|
+
answering both.
|
|
2087
|
+
|
|
2088
|
+
`resolve-authz-context` projects EVERY value stored in `sys_member.role` into
|
|
2089
|
+
`current_user.positions`, alongside the rows read from `sys_user_position`. So a
|
|
2090
|
+
business role handed out through the membership role _was_ capability — granted
|
|
2091
|
+
with none of the position system's controls: no `granted_by`, no ADR-0091
|
|
2092
|
+
validity window, no BU-subtree check, no `assignablePermissionSets` allowlist.
|
|
2093
|
+
That is what ADR-0057 D4 ruled out ("feed the names to better-auth **only** so
|
|
2094
|
+
invitations are accepted — **never as the authority for RBAC**"), what
|
|
2095
|
+
ADR-0090 D3's word ban restates (distribution = `position`), and what
|
|
2096
|
+
ADR-0095 D3 keeps out of the enforcement path.
|
|
2097
|
+
|
|
2098
|
+
The vocabulary is therefore closed to the four framework-owned names:
|
|
2099
|
+
`owner` / `admin` / `delegated_admin` / `member`.
|
|
2100
|
+
|
|
2101
|
+
**BREAKING — `additionalOrgRoles` is removed** from `AuthManagerOptions` and
|
|
2102
|
+
`AuthPluginOptions`, together with `plugin-auth/src/org-roles.ts` in full
|
|
2103
|
+
(`collectStackOrgRoles`, `collectRegisteredOrgRoles`,
|
|
2104
|
+
`normalizeAdditionalOrgRoles`, `membershipRoleOptions`,
|
|
2105
|
+
`withMembershipRoleOptions`, `membershipRoleLabel`, `orgRoleNames`,
|
|
2106
|
+
`MEMBERSHIP_ROLE_OBJECTS`, `OrgRoleDescriptor`, `OrgRoleInput`,
|
|
2107
|
+
`OrgRoleLogger`) and the `kernel:ready` derivation hook that fed them. From
|
|
2108
|
+
`@objectstack/spec`, `MEMBERSHIP_ROLE_NAME_PATTERN` and
|
|
2109
|
+
`MEMBERSHIP_ROLE_NAME_MIN_LENGTH` are removed — they existed only to validate
|
|
2110
|
+
app-supplied names. A TypeScript error is the intended failure: an option that
|
|
2111
|
+
is silently ignored is `declared ≠ enforced` one more time.
|
|
2112
|
+
|
|
2113
|
+
FROM → TO:
|
|
2114
|
+
|
|
2115
|
+
```diff
|
|
2116
|
+
- new AuthPlugin({ additionalOrgRoles: ['sales_rep'] })
|
|
2117
|
+
+ new AuthPlugin({ /* nothing — declare `sales_rep` as a position */ })
|
|
2118
|
+
|
|
2119
|
+
- POST /organization/invite-member { email, role: 'sales_rep' }
|
|
2120
|
+
+ POST /organization/invite-member { email, role: 'member',
|
|
2121
|
+
+ businessUnitId, positions: ['sales_rep'] }
|
|
2122
|
+
```
|
|
2123
|
+
|
|
2124
|
+
For an existing member, assign the position through `sys_user_position` (the
|
|
2125
|
+
governed write path). Invitation placement (ADR-0105 D8) is the one-step
|
|
2126
|
+
admission flow: issuance is authorized against the issuer's `adminScope` by
|
|
2127
|
+
dry-running `DelegatedAdminGate`, and acceptance writes real
|
|
2128
|
+
`sys_user_position` rows with a `granted_by` stamp. It reaches **further** than
|
|
2129
|
+
what it replaces — a delegated admin may use it within their subtree, where the
|
|
2130
|
+
membership-role route was open to org admins only (the invitation role cap holds
|
|
2131
|
+
anyone below admin grade to plain `member`).
|
|
2132
|
+
|
|
2133
|
+
An invitation naming an app role now fails at better-auth's door with
|
|
2134
|
+
`ROLE_NOT_FOUND`, before any row is written.
|
|
2135
|
+
|
|
2136
|
+
This reverses two changesets that were never consumed into a release
|
|
2137
|
+
(`app-org-roles-storable`, `auth-org-roles-self-derived`), so no published
|
|
2138
|
+
version ever offered the behaviour; both are removed rather than shipped and
|
|
2139
|
+
retracted in the same changelog. A pre-existing deployment could only have
|
|
2140
|
+
stored a custom value by direct DB write.
|
|
2141
|
+
|
|
2142
|
+
Also derived rather than transcribed: `@objectstack/lint`'s `MEMBERSHIP_TIERS`
|
|
2143
|
+
now reads `BUILTIN_MEMBERSHIP_ROLES` from `@objectstack/spec`. The hand-kept
|
|
2144
|
+
copy carried `guest`, which the `sys_member.role` select has never offered — an
|
|
2145
|
+
approver authored as `{ type: 'org_membership_level', value: 'guest' }`
|
|
2146
|
+
resolved to nobody and the lint whose whole job is to catch that stayed silent.
|
|
2147
|
+
|
|
2148
|
+
- 29ff3c2: feat(lint): warn on replay-unsafe `mode: 'insert'` seed datasets (#3434 follow-up)
|
|
2149
|
+
|
|
2150
|
+
Seeds are replayed — they re-load on every dev-server boot and every package
|
|
2151
|
+
re-publish, not applied once — so `mode: 'insert'` (the loader's one mode with
|
|
2152
|
+
no existing-row check) duplicates its table on every restart. That footgun
|
|
2153
|
+
shipped undetected until #3434 (showcase memberships grew 3 → 6 → 9).
|
|
2154
|
+
|
|
2155
|
+
Adds `validateSeedReplaySafety` to `@objectstack/lint` (a pure `(stack) => Finding[]`
|
|
2156
|
+
rule, ADR-0019) and wires it into `os validate` / `os lint`. Every `data[]` seed
|
|
2157
|
+
declared with `mode: 'insert'` now gets an advisory warning that points at the
|
|
2158
|
+
idempotent modes (`ignore` / `upsert`) and the `externalId` to match on — a
|
|
2159
|
+
single natural-key field, or a COMPOSITE list of fields for a join / junction
|
|
2160
|
+
table with no single key (`['team', 'project']`, the support #3434 added). It
|
|
2161
|
+
catches the mistake at authoring time instead of on the second boot.
|
|
2162
|
+
|
|
2163
|
+
- 95829a0: feat(lint): warn on seed values outside an object's declared state machine (#3433 follow-up)
|
|
2164
|
+
|
|
2165
|
+
#3433 exempts seed writes from the `state_machine` validation rule, so a seeded
|
|
2166
|
+
status the FSM does not declare is no longer rejected at write time. A field-level
|
|
2167
|
+
`select` still catches a value outside its `options`, but a `state_machine` on a
|
|
2168
|
+
free-text field — or a value that is a valid option yet not a declared FSM state —
|
|
2169
|
+
now sails through silently: the exemption is a deliberate but blind back door.
|
|
2170
|
+
|
|
2171
|
+
`validateSeedStateMachine` (a pure `(stack) => Finding[]` rule, run from
|
|
2172
|
+
`os validate` / `os lint`, symmetric with the replay-safety rule from #3434)
|
|
2173
|
+
re-adds that safety net at author time. It flags any seed record whose
|
|
2174
|
+
`state_machine`-governed field carries a value outside the machine's declared
|
|
2175
|
+
states — the union of `initialStates`, the transition-map keys, and the transition
|
|
2176
|
+
targets. Advisory (`warning`): the exemption itself is legitimate, so the fix-it
|
|
2177
|
+
points at either adding the state to the machine or correcting the typo, not a hard
|
|
2178
|
+
build failure. New rule id: `seed-value-outside-state-machine`.
|
|
2179
|
+
|
|
2180
|
+
- 57bab76: Typed `decisionOutputs` declarations (#3447 follow-up). A `decisionOutputs` entry may now be `{ key, label?, type: 'text' | 'user' | 'department' | 'position' | 'team', multiple? }` alongside the bare-string form — a typed entry tells the decision UI to render the matching record picker (id values; `multiple` collects an id array) instead of free text, turning "paste user ids" into "pick people". The type shapes only the input widget: the runtime whitelist works by `key` either way, via the new `normalizeDecisionOutputs` helper exported from `@objectstack/spec/automation` — the single reader of the union shape shared by the service, the request read, and `os lint`. The request read now carries `decision_output_defs` (normalized declarations) alongside the version-skew-safe `decision_outputs` key list.
|
|
2181
|
+
- Updated dependencies [50616d9]
|
|
2182
|
+
- Updated dependencies [08b5a3d]
|
|
2183
|
+
- Updated dependencies [d99aeb3]
|
|
2184
|
+
- Updated dependencies [4727eb8]
|
|
2185
|
+
- Updated dependencies [f63cd09]
|
|
2186
|
+
- Updated dependencies [fa3d0cf]
|
|
2187
|
+
- Updated dependencies [af5a224]
|
|
2188
|
+
- Updated dependencies [71f76e1]
|
|
2189
|
+
- Updated dependencies [37b1346]
|
|
2190
|
+
- Updated dependencies [99736a0]
|
|
2191
|
+
- Updated dependencies [fe67e34]
|
|
2192
|
+
- Updated dependencies [fdb4f50]
|
|
2193
|
+
- Updated dependencies [1bd5652]
|
|
2194
|
+
- Updated dependencies [14252d3]
|
|
2195
|
+
- Updated dependencies [7fb436c]
|
|
2196
|
+
- Updated dependencies [879ea13]
|
|
2197
|
+
- Updated dependencies [201b31f]
|
|
2198
|
+
- Updated dependencies [e2616e0]
|
|
2199
|
+
- Updated dependencies [6fdc5c6]
|
|
2200
|
+
- Updated dependencies [8b9d71e]
|
|
2201
|
+
- Updated dependencies [33f5e23]
|
|
2202
|
+
- Updated dependencies [259af21]
|
|
2203
|
+
- Updated dependencies [587fc91]
|
|
2204
|
+
- Updated dependencies [1986594]
|
|
2205
|
+
- Updated dependencies [ad4af62]
|
|
2206
|
+
- Updated dependencies [d44dbfa]
|
|
2207
|
+
- Updated dependencies [474fe39]
|
|
2208
|
+
- Updated dependencies [0bc685a]
|
|
2209
|
+
- Updated dependencies [b949059]
|
|
2210
|
+
- Updated dependencies [be1c52c]
|
|
2211
|
+
- Updated dependencies [c5ff96d]
|
|
2212
|
+
- Updated dependencies [84e7be9]
|
|
2213
|
+
- Updated dependencies [a6c3f38]
|
|
2214
|
+
- Updated dependencies [debc23a]
|
|
2215
|
+
- Updated dependencies [0f8ad09]
|
|
2216
|
+
- Updated dependencies [8f9689f]
|
|
2217
|
+
- Updated dependencies [57a3bb3]
|
|
2218
|
+
- Updated dependencies [5f9a987]
|
|
2219
|
+
- Updated dependencies [db02d47]
|
|
2220
|
+
- Updated dependencies [0bfdf46]
|
|
2221
|
+
- Updated dependencies [376a061]
|
|
2222
|
+
- Updated dependencies [7c7e246]
|
|
2223
|
+
- Updated dependencies [f35cdc5]
|
|
2224
|
+
- Updated dependencies [9ea2bc5]
|
|
2225
|
+
- Updated dependencies [c2d9098]
|
|
2226
|
+
- Updated dependencies [a227ed7]
|
|
2227
|
+
- Updated dependencies [9613396]
|
|
2228
|
+
- Updated dependencies [e47b342]
|
|
2229
|
+
- Updated dependencies [4ed7ed4]
|
|
2230
|
+
- Updated dependencies [2fa4ca1]
|
|
2231
|
+
- Updated dependencies [f5a2320]
|
|
2232
|
+
- Updated dependencies [deb538f]
|
|
2233
|
+
- Updated dependencies [5b89711]
|
|
2234
|
+
- Updated dependencies [0c8a22f]
|
|
2235
|
+
- Updated dependencies [763931e]
|
|
2236
|
+
- Updated dependencies [de9af8a]
|
|
2237
|
+
- Updated dependencies [c4df271]
|
|
2238
|
+
- Updated dependencies [a41ba5c]
|
|
2239
|
+
- Updated dependencies [189854c]
|
|
2240
|
+
- Updated dependencies [0e3a226]
|
|
2241
|
+
- Updated dependencies [1d4756e]
|
|
2242
|
+
- Updated dependencies [720c5ad]
|
|
2243
|
+
- Updated dependencies [a8d1e24]
|
|
2244
|
+
- Updated dependencies [41642b0]
|
|
2245
|
+
- Updated dependencies [4cca74c]
|
|
2246
|
+
- Updated dependencies [88ef03e]
|
|
2247
|
+
- Updated dependencies [9e2caf3]
|
|
2248
|
+
- Updated dependencies [81ce41a]
|
|
2249
|
+
- Updated dependencies [85e1e4e]
|
|
2250
|
+
- Updated dependencies [dac6a08]
|
|
2251
|
+
- Updated dependencies [394b7a1]
|
|
2252
|
+
- Updated dependencies [677b591]
|
|
2253
|
+
- Updated dependencies [d77d1b7]
|
|
2254
|
+
- Updated dependencies [5b79a34]
|
|
2255
|
+
- Updated dependencies [c757854]
|
|
2256
|
+
- Updated dependencies [0045682]
|
|
2257
|
+
- Updated dependencies [2a5f04a]
|
|
2258
|
+
- Updated dependencies [4f740b0]
|
|
2259
|
+
- Updated dependencies [67452d1]
|
|
2260
|
+
- Updated dependencies [0fc6219]
|
|
2261
|
+
- Updated dependencies [605e190]
|
|
2262
|
+
- Updated dependencies [c6c59f1]
|
|
2263
|
+
- Updated dependencies [b0e78a8]
|
|
2264
|
+
- Updated dependencies [f31cc8d]
|
|
2265
|
+
- Updated dependencies [f343dc4]
|
|
2266
|
+
- Updated dependencies [8269e32]
|
|
2267
|
+
- Updated dependencies [74f7339]
|
|
2268
|
+
- Updated dependencies [a6c35a2]
|
|
2269
|
+
- Updated dependencies [c2f1002]
|
|
2270
|
+
- Updated dependencies [f163028]
|
|
2271
|
+
- Updated dependencies [f07808c]
|
|
2272
|
+
- Updated dependencies [7ffc3d3]
|
|
2273
|
+
- Updated dependencies [88346ba]
|
|
2274
|
+
- Updated dependencies [4631592]
|
|
2275
|
+
- Updated dependencies [32ff033]
|
|
2276
|
+
- Updated dependencies [5ac93d4]
|
|
2277
|
+
- Updated dependencies [93f267f]
|
|
2278
|
+
- Updated dependencies [0024abf]
|
|
2279
|
+
- Updated dependencies [acbf364]
|
|
2280
|
+
- Updated dependencies [7687f7b]
|
|
2281
|
+
- Updated dependencies [1659072]
|
|
2282
|
+
- Updated dependencies [abceb0d]
|
|
2283
|
+
- Updated dependencies [0c302a7]
|
|
2284
|
+
- Updated dependencies [6633337]
|
|
2285
|
+
- Updated dependencies [f00d8d4]
|
|
2286
|
+
- Updated dependencies [503be86]
|
|
2287
|
+
- Updated dependencies [cde1975]
|
|
2288
|
+
- Updated dependencies [0bc685a]
|
|
2289
|
+
- Updated dependencies [11949fc]
|
|
2290
|
+
- Updated dependencies [b098b0e]
|
|
2291
|
+
- Updated dependencies [4d00b13]
|
|
2292
|
+
- Updated dependencies [57bab76]
|
|
2293
|
+
- Updated dependencies [b90086a]
|
|
2294
|
+
- Updated dependencies [b95577a]
|
|
2295
|
+
- Updated dependencies [83c161f]
|
|
2296
|
+
- Updated dependencies [d8c4957]
|
|
2297
|
+
- Updated dependencies [f24cb83]
|
|
2298
|
+
- Updated dependencies [5dbbb92]
|
|
2299
|
+
- Updated dependencies [69f1dfd]
|
|
2300
|
+
- @objectstack/spec@17.0.0-rc.0
|
|
2301
|
+
- @objectstack/formula@17.0.0-rc.0
|
|
2302
|
+
- @objectstack/sdui-parser@17.0.0-rc.0
|
|
2303
|
+
|
|
2304
|
+
## 16.1.0
|
|
2305
|
+
|
|
2306
|
+
### Minor Changes
|
|
2307
|
+
|
|
2308
|
+
- fa006fb: Validate dashboard filter field-existence at build time (extend ADR-0021, #3365).
|
|
2309
|
+
|
|
2310
|
+
`validateWidgetBindings` now checks that every dashboard-level filter (`dateRange`
|
|
2311
|
+
|
|
2312
|
+
- each `globalFilters[]`) resolves to a real field on each bound widget's dataset
|
|
2313
|
+
object. Since #2501 wired these filters into every widget's analytics query, a
|
|
2314
|
+
filter field absent on a widget's object — e.g. a `dateRange` bound to
|
|
2315
|
+
`close_date` inherited by an account/contact widget over a different object —
|
|
2316
|
+
emitted invalid SQL (`no such column: close_date`) and crashed the widget at
|
|
2317
|
+
render time. That build-decidable invariant previously escaped `os validate` /
|
|
2318
|
+
`os build` and failed only when a user opened the dashboard.
|
|
2319
|
+
|
|
2320
|
+
It now fails the build (new rule `dashboard-filter-field-unknown`) with a message
|
|
2321
|
+
naming the dashboard, widget, filter, field, and object, unless the widget opts
|
|
2322
|
+
out via `filterBindings: { <name>: false }` or re-targets to an existing field —
|
|
2323
|
+
mirroring the field-existence invariant ADR-0032 enforces for CEL references.
|
|
2324
|
+
Effective-field resolution matches the runtime (`filterBindings` re-target /
|
|
2325
|
+
opt-out, legacy `targetWidgets` allow-list, filter default). Registry-injected
|
|
2326
|
+
system fields (e.g. `created_at`, the `dateRange` default) and objects outside
|
|
2327
|
+
the validated stack never false-positive.
|
|
2328
|
+
|
|
2329
|
+
- db160dd: Flag dead action/route references in dashboard header & widget actions (ADR-0049 for references, #3367).
|
|
2330
|
+
|
|
2331
|
+
`os validate` / `os build` now run a new `validateDashboardActionRefs` gate over every dashboard `header.actions[]` and widget `actionUrl`:
|
|
2332
|
+
|
|
2333
|
+
- `actionType: 'script' | 'modal'` — **error** unless `actionUrl` resolves to a defined action (`stack.actions` or an object's `actions`). `modal` also resolves via the runtime `<verb>_<object>` convention (`create_/new_/add_/edit_/update_` + a real object) and bare object names. A dangling target ships a button that renders and silently does nothing on click — a false affordance, exactly the "declared ≠ enforced" gap ADR-0049 closes, applied to references.
|
|
2334
|
+
- `actionType: 'url'` — **warning** when a relative in-app path names a `objects/reports/dashboards/pages/views` route whose target does not exist in the stack. External URLs, interpolated (`${…}`) targets, and opaque routes are skipped to keep false positives near zero.
|
|
2335
|
+
|
|
2336
|
+
### Patch Changes
|
|
2337
|
+
|
|
2338
|
+
- Updated dependencies [9e45b63]
|
|
2339
|
+
- @objectstack/spec@16.1.0
|
|
2340
|
+
- @objectstack/formula@16.1.0
|
|
2341
|
+
- @objectstack/sdui-parser@16.1.0
|
|
2342
|
+
|
|
2343
|
+
## 16.0.0
|
|
2344
|
+
|
|
2345
|
+
### Minor Changes
|
|
2346
|
+
|
|
2347
|
+
- 3a18b60: feat(approvals): rename the `role` approver type to `org_membership_level` (#3133)
|
|
2348
|
+
|
|
2349
|
+
`ApproverType.role` was the last platform surface projecting the reserved word
|
|
2350
|
+
"role" (ADR-0090 D3). It is not covered by D3's better-auth exception: that
|
|
2351
|
+
exception protects better-auth's own `sys_member.role` **column**, which we do
|
|
2352
|
+
not own — `ApproverType` is our own enum, an authoring surface, and D3 mandates
|
|
2353
|
+
that the projection of that concept is spelled `org_membership_level` and
|
|
2354
|
+
labelled "organization membership", **never "role"**.
|
|
2355
|
+
|
|
2356
|
+
The sentence licensing the leak was also false: ADR-0090 D3 claims
|
|
2357
|
+
`sys_member.role` is "already relabelled `org_membership_level` in the platform
|
|
2358
|
+
projection", but `org_membership_level` existed nowhere in the codebase and
|
|
2359
|
+
ADR-0057 D7 lists that relabel under "Deferred (evidence-gated, P4)". The
|
|
2360
|
+
projection never landed, so the word reached authors.
|
|
2361
|
+
|
|
2362
|
+
The name manufactured a real, silent failure — "hotcrm class": every other
|
|
2363
|
+
surface renamed to `position` (`sys_role`, `ShareRecipientType.role`,
|
|
2364
|
+
`ctx.roles[]`), so `{ type: 'role', value: 'sales_manager' }` reads as the
|
|
2365
|
+
legacy spelling of a position. It resolves against the membership tier, finds
|
|
2366
|
+
no member row, falls back to an inert `role:sales_manager` literal, and the
|
|
2367
|
+
request waits forever on an approver that cannot exist.
|
|
2368
|
+
|
|
2369
|
+
- **spec**: `ApproverType` gains `org_membership_level`; `role` stays as a
|
|
2370
|
+
deprecated alias for one window (a published 15.x flow keeps loading) with
|
|
2371
|
+
`DEPRECATED_APPROVER_TYPES` + `canonicalApproverType()` as the single source
|
|
2372
|
+
for the mapping. Removed in the next major.
|
|
2373
|
+
- **plugin-approvals**: resolves on the canonical type and warns on the
|
|
2374
|
+
deprecated spelling. The `type:value` fallback literal keeps the **authored**
|
|
2375
|
+
spelling — stored `sys_approval_approver` rows and `pending_approvers` slots
|
|
2376
|
+
from 15.x carry `role:<v>`, and rewriting it would orphan them.
|
|
2377
|
+
- **lint**: `approval-role-not-membership-tier` → `approval-approver-not-membership-tier`
|
|
2378
|
+
(the rule id carried the reserved word too), plus a new
|
|
2379
|
+
`approval-approver-type-deprecated`. The two are mutually exclusive: a bad
|
|
2380
|
+
_value_ wins, because prescribing `org_membership_level` for a position name
|
|
2381
|
+
would be wrong advice — the fix there is `position`.
|
|
2382
|
+
|
|
2383
|
+
Authoring `type: 'role'` keeps working and now says so out loud. Rewrite it as
|
|
2384
|
+
`org_membership_level`; if the value is an org position, the fix is `position`.
|
|
2385
|
+
|
|
2386
|
+
- 2ea08ee: Flow trigger observability — kill the four-layer silence around record-change flows that never fire (2026-07-17 third-party eval).
|
|
2387
|
+
|
|
2388
|
+
A misauthored auto-launched flow (wrong `objectName`, missing `requires: ['automation','triggers']`, failing start condition) produced ZERO output at every layer: the engine's own registration/binding logs land inside the CLI's boot-quiet stdout window (which swallows debug/info/warn — only error/fatal reach stderr), and each "didn't happen" path was itself silent. Fixes:
|
|
2389
|
+
|
|
2390
|
+
- **Startup banner `Flows:` section** (`os serve`/`os dev`/`os start`): flow count, bound-to-trigger count, registered trigger types, draft count — plus loud `⚠` lines for flows declared with no automation engine enabled (`requires` missing), flows whose trigger type has no registered trigger, and bound record-change flows targeting an unknown object (dead binding). Printed after stdout is restored, so it is immune to the boot-quiet window.
|
|
2391
|
+
- **Trigger-fired run failures now log at ERROR** (stderr — always visible): the automation engine no longer drops the AutomationResult of a trigger-fired execution; condition-evaluation faults and node failures surface with the flow name. Condition-not-met skips stay at debug (high-frequency, intentional).
|
|
2392
|
+
- **`RecordChangeTrigger` probes object existence at bind time** and warns when a flow's `objectName` matches no registered object (exact-name matching), instead of silently arming a hook that can never fire.
|
|
2393
|
+
- **`kernel:bootstrapped` binding audit** in the automation plugin: warns per enabled-but-unbound triggered flow with the reason, and reports registered/bound/draft counts (`AutomationEngine.getTriggerBindingAudit()`, extended `getFlowRuntimeStates()` with `status`/`triggerType`/`object`).
|
|
2394
|
+
- **`os validate` flow-wiring advisories** (`@objectstack/lint` `validateFlowTriggerReadiness`): warns when a record-triggered flow targets an object the stack does not define, and when an auto-triggered flow's status is `draft` (authored or defaulted — draft flows still fire; declare `active` or `obsolete`).
|
|
2395
|
+
- Removed leftover boot-debug writes (`registerApp`/`AppPlugin`/`StandaloneStack`/`AuditPlugin` stderr noise) that previous debugging of this same silence had left behind.
|
|
2396
|
+
|
|
2397
|
+
- ea32ec7: feat(formula,lint): advisory type-soundness warnings for formula/predicate expressions (#1928 tier 4)
|
|
2398
|
+
|
|
2399
|
+
Closes the last open guardrail from #1928. A `Field.formula` or record-scoped
|
|
2400
|
+
predicate that uses a **text or boolean field with an arithmetic (`+ - * / %`)
|
|
2401
|
+
or ordering (`< > <= >=`) operator against a number** faults the runtime
|
|
2402
|
+
overload and silently evaluates to `null` (e.g. `record.title * 2`,
|
|
2403
|
+
`record.is_active + 1`). The build now surfaces this as a **non-blocking
|
|
2404
|
+
warning** with the offending field and a corrective message.
|
|
2405
|
+
|
|
2406
|
+
Honours the ADR-0032 design law — the checker only flags what the runtime
|
|
2407
|
+
would also fail:
|
|
2408
|
+
|
|
2409
|
+
- Number / currency / percent / date / datetime fields are declared `dyn`, so
|
|
2410
|
+
the cases the runtime rescues never warn — `record.amount / 100` (the #1930
|
|
2411
|
+
`registerOperator` fix), `record.due == today()` and numeric-string / ISO-date
|
|
2412
|
+
values (the string-hydration retry), and numeric-coded `select` option values.
|
|
2413
|
+
- Equality (`==` / `!=`) is excluded: a heterogeneous equality is runtime-safe
|
|
2414
|
+
(evaluates to `false`), never a fault.
|
|
2415
|
+
|
|
2416
|
+
New `firstTypeMismatch(source, fieldCelTypes, scope)` export in
|
|
2417
|
+
`@objectstack/formula` (and an optional `fieldTypes` hint on
|
|
2418
|
+
`validateExpression`); `@objectstack/lint`'s `validateStackExpressions` threads
|
|
2419
|
+
each object's field types into every checked site:
|
|
2420
|
+
|
|
2421
|
+
- **record-scoped** sites (`record.<field>`) — formula fields, validation rules,
|
|
2422
|
+
action / hook / sharing predicates;
|
|
2423
|
+
- **flattened** flow / automation conditions (bare `field`) — where flow
|
|
2424
|
+
variables stay `dyn` and are never flagged, and equality stays runtime-safe.
|
|
2425
|
+
|
|
2426
|
+
Warnings are advisory in `objectstack build` / `validate` (fatal only under
|
|
2427
|
+
`--strict`), matching the tier-3 channel.
|
|
2428
|
+
|
|
2429
|
+
- a2795f6: feat(triggers): declarative time-relative trigger — daily sweep instead of fragile date-equality (#1874)
|
|
2430
|
+
|
|
2431
|
+
Time-relative business rules ("alert 60 days before a contract's `end_date`")
|
|
2432
|
+
could only be expressed as a `record_change` flow gated on a date-equality
|
|
2433
|
+
condition like `end_date == daysFromNow(60)`. That predicate is only evaluated
|
|
2434
|
+
when the record _happens to change_, so it fires only if a record is edited on
|
|
2435
|
+
exactly the threshold day — i.e. almost never, unattended. The robust
|
|
2436
|
+
alternative was a hand-written cron + range query that every author
|
|
2437
|
+
re-implemented (contracts `renewal_alert`, hr `document_expiring_soon`,
|
|
2438
|
+
procurement `po_overdue`, …).
|
|
2439
|
+
|
|
2440
|
+
A flow's start node can now declare a `timeRelative` descriptor instead:
|
|
2441
|
+
|
|
2442
|
+
```ts
|
|
2443
|
+
config: {
|
|
2444
|
+
timeRelative: {
|
|
2445
|
+
object: 'contracts',
|
|
2446
|
+
dateField: 'end_date',
|
|
2447
|
+
offsetDays: [60, 30, 7], // T-minus reminders — fires on each threshold day
|
|
2448
|
+
// — or — withinDays: 30 // "expiring soon" range; negative = overdue lookback
|
|
2449
|
+
filter: { status: 'active' }, // optional, ANDed with the date window
|
|
2450
|
+
},
|
|
2451
|
+
schedule: { type: 'cron', expression: '0 8 * * *' }, // optional; defaults to daily 08:00 UTC
|
|
2452
|
+
}
|
|
2453
|
+
```
|
|
2454
|
+
|
|
2455
|
+
The new `time_relative` trigger (shipped in `@objectstack/trigger-schedule` as
|
|
2456
|
+
`TimeRelativeTriggerPlugin`) sweeps the object on that schedule and launches the
|
|
2457
|
+
flow **once per matching record**, with the record on the automation context —
|
|
2458
|
+
so the start-node `condition` gate and `{record.<field>}` interpolation work
|
|
2459
|
+
exactly as for a record-change flow. Because the window is evaluated every day,
|
|
2460
|
+
a threshold is never missed regardless of when the record last changed. The
|
|
2461
|
+
discovery query runs as a system operation (RLS-bypassing) and is capped
|
|
2462
|
+
(`maxRecords`, default 1000) so a mis-scoped window can't fan out unboundedly;
|
|
2463
|
+
per-record failures are isolated so one bad row never aborts the sweep.
|
|
2464
|
+
|
|
2465
|
+
The automation engine routes a start node carrying `config.timeRelative` to the
|
|
2466
|
+
`time_relative` trigger (ahead of the plain `schedule` trigger, whose behavior is
|
|
2467
|
+
unchanged), and `os validate` gains readiness checks for the new descriptor
|
|
2468
|
+
(unknown swept object, ambiguous draft status). New authorable spec key:
|
|
2469
|
+
`TimeRelativeTriggerSchema` (`@objectstack/spec/automation`).
|
|
2470
|
+
|
|
2471
|
+
### Patch Changes
|
|
2472
|
+
|
|
2473
|
+
- 524696a: feat(spec)!: `DashboardWidgetSchema.strict()` — reject undeclared widget keys (framework#3251)
|
|
2474
|
+
|
|
2475
|
+
The ADR-0021 analytics endpoint. `DashboardWidgetSchema` now rejects any
|
|
2476
|
+
undeclared top-level key instead of silently stripping it, moving a whole class
|
|
2477
|
+
of author error (a hallucinated or legacy key that renders as a silent no-op)
|
|
2478
|
+
from fallible human review to deterministic CI. `options: z.unknown()` remains
|
|
2479
|
+
the escape hatch for renderer-specific extras.
|
|
2480
|
+
|
|
2481
|
+
A custom error map names the offending key(s) and, when a key is a removed
|
|
2482
|
+
pre-ADR-0021 inline-analytics key (`object` / `categoryField` / `valueField` /
|
|
2483
|
+
`aggregate`, pivot `rowField` / `columnField`) or an objectui-internal prop
|
|
2484
|
+
(`component`, inline `data`), points the author at the dataset shape
|
|
2485
|
+
(`dataset` + `dimensions` + `values`).
|
|
2486
|
+
|
|
2487
|
+
Recorded as protocol-16 migration `step16`
|
|
2488
|
+
(`dashboard-widget-strict-unknown-keys`), mirroring protocol-15's `step15`
|
|
2489
|
+
strict flip on the form/page schemas (ADR-0089 D3a). The inline-analytics shape
|
|
2490
|
+
itself was already removed at protocol 9 (single-form cutover), so there is no
|
|
2491
|
+
mechanical rewrite — the residue is the strictness, delegated to the author.
|
|
2492
|
+
|
|
2493
|
+
**Breaking:** shipped as `minor` per the launch-window policy (a breaking change
|
|
2494
|
+
does not burn a major while the stack is in lockstep), riding the already-pending
|
|
2495
|
+
16.0.0 train. The release train's Version-Packages PR must set
|
|
2496
|
+
`PROTOCOL_VERSION = '16.0.0'`; until then `step16` is inert
|
|
2497
|
+
(`composeMigrationChain` caps at `PROTOCOL_MAJOR`).
|
|
2498
|
+
|
|
2499
|
+
`@objectstack/lint` — the `widget-legacy-analytics-shape` /
|
|
2500
|
+
`widget-legacy-analytics-unrenderable` rules are retained as the friendly,
|
|
2501
|
+
suppressible bridge on the raw-config lint/doctor paths (strict preempts them on
|
|
2502
|
+
the schema-parsed compile/validate paths); doc comment updated to explain the
|
|
2503
|
+
interplay.
|
|
2504
|
+
|
|
2505
|
+
- 8923843: Reject view containers that define no views. A flat list-view object (`{ name, label, type, columns, ... }`) parses to an empty `ViewSchema` container because Zod strips unknown keys — zero views register and the Console silently renders nothing. `defineView()` now throws on a zero-view container, and `os validate` gains a `view-container-shape` check (`validateViewContainers` in `@objectstack/lint`) that reports flat or empty `views: []` entries pre-parse with a wrap-it fix hint.
|
|
2506
|
+
- Updated dependencies [f972574]
|
|
2507
|
+
- Updated dependencies [6289ec3]
|
|
2508
|
+
- Updated dependencies [22013aa]
|
|
2509
|
+
- Updated dependencies [3ad3dd5]
|
|
2510
|
+
- Updated dependencies [8efa395]
|
|
2511
|
+
- Updated dependencies [3a18b60]
|
|
2512
|
+
- Updated dependencies [a8aa34c]
|
|
2513
|
+
- Updated dependencies [a3823b2]
|
|
2514
|
+
- Updated dependencies [43a3efb]
|
|
2515
|
+
- Updated dependencies [524696a]
|
|
2516
|
+
- Updated dependencies [6b51346]
|
|
2517
|
+
- Updated dependencies [80273c8]
|
|
2518
|
+
- Updated dependencies [bfa3c3f]
|
|
2519
|
+
- Updated dependencies [5e3301d]
|
|
2520
|
+
- Updated dependencies [46e876c]
|
|
2521
|
+
- Updated dependencies [7125007]
|
|
2522
|
+
- Updated dependencies [158aa14]
|
|
2523
|
+
- Updated dependencies [62a2117]
|
|
2524
|
+
- Updated dependencies [d2723e2]
|
|
2525
|
+
- Updated dependencies [fefcd54]
|
|
2526
|
+
- Updated dependencies [beaf2de]
|
|
2527
|
+
- Updated dependencies [369eb6e]
|
|
2528
|
+
- Updated dependencies [06ff734]
|
|
2529
|
+
- Updated dependencies [b659111]
|
|
2530
|
+
- Updated dependencies [5754a23]
|
|
2531
|
+
- Updated dependencies [6c270a6]
|
|
2532
|
+
- Updated dependencies [668dd17]
|
|
2533
|
+
- Updated dependencies [8abf133]
|
|
2534
|
+
- Updated dependencies [e0859b1]
|
|
2535
|
+
- Updated dependencies [04ecd4e]
|
|
2536
|
+
- Updated dependencies [4d5a892]
|
|
2537
|
+
- Updated dependencies [16cebeb]
|
|
2538
|
+
- Updated dependencies [86d30af]
|
|
2539
|
+
- Updated dependencies [8923843]
|
|
2540
|
+
- Updated dependencies [ea32ec7]
|
|
2541
|
+
- Updated dependencies [a2795f6]
|
|
2542
|
+
- Updated dependencies [f16b492]
|
|
2543
|
+
- Updated dependencies [4b6fde8]
|
|
2544
|
+
- Updated dependencies [2018df9]
|
|
2545
|
+
- Updated dependencies [fc5a3a2]
|
|
2546
|
+
- Updated dependencies [8ff9210]
|
|
2547
|
+
- @objectstack/spec@16.0.0
|
|
2548
|
+
- @objectstack/formula@16.0.0
|
|
2549
|
+
- @objectstack/sdui-parser@16.0.0
|
|
2550
|
+
|
|
2551
|
+
## 16.0.0-rc.1
|
|
2552
|
+
|
|
2553
|
+
### Patch Changes
|
|
2554
|
+
|
|
2555
|
+
- Updated dependencies [6289ec3]
|
|
2556
|
+
- Updated dependencies [8efa395]
|
|
2557
|
+
- Updated dependencies [bfa3c3f]
|
|
2558
|
+
- Updated dependencies [7125007]
|
|
2559
|
+
- Updated dependencies [62a2117]
|
|
2560
|
+
- Updated dependencies [06ff734]
|
|
2561
|
+
- @objectstack/spec@16.0.0-rc.1
|
|
2562
|
+
- @objectstack/formula@16.0.0-rc.1
|
|
2563
|
+
- @objectstack/sdui-parser@16.0.0-rc.1
|
|
2564
|
+
|
|
2565
|
+
## 16.0.0-rc.0
|
|
2566
|
+
|
|
2567
|
+
### Minor Changes
|
|
2568
|
+
|
|
2569
|
+
- 3a18b60: feat(approvals): rename the `role` approver type to `org_membership_level` (#3133)
|
|
2570
|
+
|
|
2571
|
+
`ApproverType.role` was the last platform surface projecting the reserved word
|
|
2572
|
+
"role" (ADR-0090 D3). It is not covered by D3's better-auth exception: that
|
|
2573
|
+
exception protects better-auth's own `sys_member.role` **column**, which we do
|
|
2574
|
+
not own — `ApproverType` is our own enum, an authoring surface, and D3 mandates
|
|
2575
|
+
that the projection of that concept is spelled `org_membership_level` and
|
|
2576
|
+
labelled "organization membership", **never "role"**.
|
|
2577
|
+
|
|
2578
|
+
The sentence licensing the leak was also false: ADR-0090 D3 claims
|
|
2579
|
+
`sys_member.role` is "already relabelled `org_membership_level` in the platform
|
|
2580
|
+
projection", but `org_membership_level` existed nowhere in the codebase and
|
|
2581
|
+
ADR-0057 D7 lists that relabel under "Deferred (evidence-gated, P4)". The
|
|
2582
|
+
projection never landed, so the word reached authors.
|
|
2583
|
+
|
|
2584
|
+
The name manufactured a real, silent failure — "hotcrm class": every other
|
|
2585
|
+
surface renamed to `position` (`sys_role`, `ShareRecipientType.role`,
|
|
2586
|
+
`ctx.roles[]`), so `{ type: 'role', value: 'sales_manager' }` reads as the
|
|
2587
|
+
legacy spelling of a position. It resolves against the membership tier, finds
|
|
2588
|
+
no member row, falls back to an inert `role:sales_manager` literal, and the
|
|
2589
|
+
request waits forever on an approver that cannot exist.
|
|
2590
|
+
|
|
2591
|
+
- **spec**: `ApproverType` gains `org_membership_level`; `role` stays as a
|
|
2592
|
+
deprecated alias for one window (a published 15.x flow keeps loading) with
|
|
2593
|
+
`DEPRECATED_APPROVER_TYPES` + `canonicalApproverType()` as the single source
|
|
2594
|
+
for the mapping. Removed in the next major.
|
|
2595
|
+
- **plugin-approvals**: resolves on the canonical type and warns on the
|
|
2596
|
+
deprecated spelling. The `type:value` fallback literal keeps the **authored**
|
|
2597
|
+
spelling — stored `sys_approval_approver` rows and `pending_approvers` slots
|
|
2598
|
+
from 15.x carry `role:<v>`, and rewriting it would orphan them.
|
|
2599
|
+
- **lint**: `approval-role-not-membership-tier` → `approval-approver-not-membership-tier`
|
|
2600
|
+
(the rule id carried the reserved word too), plus a new
|
|
2601
|
+
`approval-approver-type-deprecated`. The two are mutually exclusive: a bad
|
|
2602
|
+
_value_ wins, because prescribing `org_membership_level` for a position name
|
|
2603
|
+
would be wrong advice — the fix there is `position`.
|
|
2604
|
+
|
|
2605
|
+
Authoring `type: 'role'` keeps working and now says so out loud. Rewrite it as
|
|
2606
|
+
`org_membership_level`; if the value is an org position, the fix is `position`.
|
|
2607
|
+
|
|
2608
|
+
- 2ea08ee: Flow trigger observability — kill the four-layer silence around record-change flows that never fire (2026-07-17 third-party eval).
|
|
2609
|
+
|
|
2610
|
+
A misauthored auto-launched flow (wrong `objectName`, missing `requires: ['automation','triggers']`, failing start condition) produced ZERO output at every layer: the engine's own registration/binding logs land inside the CLI's boot-quiet stdout window (which swallows debug/info/warn — only error/fatal reach stderr), and each "didn't happen" path was itself silent. Fixes:
|
|
2611
|
+
|
|
2612
|
+
- **Startup banner `Flows:` section** (`os serve`/`os dev`/`os start`): flow count, bound-to-trigger count, registered trigger types, draft count — plus loud `⚠` lines for flows declared with no automation engine enabled (`requires` missing), flows whose trigger type has no registered trigger, and bound record-change flows targeting an unknown object (dead binding). Printed after stdout is restored, so it is immune to the boot-quiet window.
|
|
2613
|
+
- **Trigger-fired run failures now log at ERROR** (stderr — always visible): the automation engine no longer drops the AutomationResult of a trigger-fired execution; condition-evaluation faults and node failures surface with the flow name. Condition-not-met skips stay at debug (high-frequency, intentional).
|
|
2614
|
+
- **`RecordChangeTrigger` probes object existence at bind time** and warns when a flow's `objectName` matches no registered object (exact-name matching), instead of silently arming a hook that can never fire.
|
|
2615
|
+
- **`kernel:bootstrapped` binding audit** in the automation plugin: warns per enabled-but-unbound triggered flow with the reason, and reports registered/bound/draft counts (`AutomationEngine.getTriggerBindingAudit()`, extended `getFlowRuntimeStates()` with `status`/`triggerType`/`object`).
|
|
2616
|
+
- **`os validate` flow-wiring advisories** (`@objectstack/lint` `validateFlowTriggerReadiness`): warns when a record-triggered flow targets an object the stack does not define, and when an auto-triggered flow's status is `draft` (authored or defaulted — draft flows still fire; declare `active` or `obsolete`).
|
|
2617
|
+
- Removed leftover boot-debug writes (`registerApp`/`AppPlugin`/`StandaloneStack`/`AuditPlugin` stderr noise) that previous debugging of this same silence had left behind.
|
|
2618
|
+
|
|
2619
|
+
- ea32ec7: feat(formula,lint): advisory type-soundness warnings for formula/predicate expressions (#1928 tier 4)
|
|
2620
|
+
|
|
2621
|
+
Closes the last open guardrail from #1928. A `Field.formula` or record-scoped
|
|
2622
|
+
predicate that uses a **text or boolean field with an arithmetic (`+ - * / %`)
|
|
2623
|
+
or ordering (`< > <= >=`) operator against a number** faults the runtime
|
|
2624
|
+
overload and silently evaluates to `null` (e.g. `record.title * 2`,
|
|
2625
|
+
`record.is_active + 1`). The build now surfaces this as a **non-blocking
|
|
2626
|
+
warning** with the offending field and a corrective message.
|
|
2627
|
+
|
|
2628
|
+
Honours the ADR-0032 design law — the checker only flags what the runtime
|
|
2629
|
+
would also fail:
|
|
2630
|
+
|
|
2631
|
+
- Number / currency / percent / date / datetime fields are declared `dyn`, so
|
|
2632
|
+
the cases the runtime rescues never warn — `record.amount / 100` (the #1930
|
|
2633
|
+
`registerOperator` fix), `record.due == today()` and numeric-string / ISO-date
|
|
2634
|
+
values (the string-hydration retry), and numeric-coded `select` option values.
|
|
2635
|
+
- Equality (`==` / `!=`) is excluded: a heterogeneous equality is runtime-safe
|
|
2636
|
+
(evaluates to `false`), never a fault.
|
|
2637
|
+
|
|
2638
|
+
New `firstTypeMismatch(source, fieldCelTypes, scope)` export in
|
|
2639
|
+
`@objectstack/formula` (and an optional `fieldTypes` hint on
|
|
2640
|
+
`validateExpression`); `@objectstack/lint`'s `validateStackExpressions` threads
|
|
2641
|
+
each object's field types into every checked site:
|
|
2642
|
+
|
|
2643
|
+
- **record-scoped** sites (`record.<field>`) — formula fields, validation rules,
|
|
2644
|
+
action / hook / sharing predicates;
|
|
2645
|
+
- **flattened** flow / automation conditions (bare `field`) — where flow
|
|
2646
|
+
variables stay `dyn` and are never flagged, and equality stays runtime-safe.
|
|
2647
|
+
|
|
2648
|
+
Warnings are advisory in `objectstack build` / `validate` (fatal only under
|
|
2649
|
+
`--strict`), matching the tier-3 channel.
|
|
2650
|
+
|
|
2651
|
+
- a2795f6: feat(triggers): declarative time-relative trigger — daily sweep instead of fragile date-equality (#1874)
|
|
2652
|
+
|
|
2653
|
+
Time-relative business rules ("alert 60 days before a contract's `end_date`")
|
|
2654
|
+
could only be expressed as a `record_change` flow gated on a date-equality
|
|
2655
|
+
condition like `end_date == daysFromNow(60)`. That predicate is only evaluated
|
|
2656
|
+
when the record _happens to change_, so it fires only if a record is edited on
|
|
2657
|
+
exactly the threshold day — i.e. almost never, unattended. The robust
|
|
2658
|
+
alternative was a hand-written cron + range query that every author
|
|
2659
|
+
re-implemented (contracts `renewal_alert`, hr `document_expiring_soon`,
|
|
2660
|
+
procurement `po_overdue`, …).
|
|
2661
|
+
|
|
2662
|
+
A flow's start node can now declare a `timeRelative` descriptor instead:
|
|
2663
|
+
|
|
2664
|
+
```ts
|
|
2665
|
+
config: {
|
|
2666
|
+
timeRelative: {
|
|
2667
|
+
object: 'contracts',
|
|
2668
|
+
dateField: 'end_date',
|
|
2669
|
+
offsetDays: [60, 30, 7], // T-minus reminders — fires on each threshold day
|
|
2670
|
+
// — or — withinDays: 30 // "expiring soon" range; negative = overdue lookback
|
|
2671
|
+
filter: { status: 'active' }, // optional, ANDed with the date window
|
|
2672
|
+
},
|
|
2673
|
+
schedule: { type: 'cron', expression: '0 8 * * *' }, // optional; defaults to daily 08:00 UTC
|
|
2674
|
+
}
|
|
2675
|
+
```
|
|
2676
|
+
|
|
2677
|
+
The new `time_relative` trigger (shipped in `@objectstack/trigger-schedule` as
|
|
2678
|
+
`TimeRelativeTriggerPlugin`) sweeps the object on that schedule and launches the
|
|
2679
|
+
flow **once per matching record**, with the record on the automation context —
|
|
2680
|
+
so the start-node `condition` gate and `{record.<field>}` interpolation work
|
|
2681
|
+
exactly as for a record-change flow. Because the window is evaluated every day,
|
|
2682
|
+
a threshold is never missed regardless of when the record last changed. The
|
|
2683
|
+
discovery query runs as a system operation (RLS-bypassing) and is capped
|
|
2684
|
+
(`maxRecords`, default 1000) so a mis-scoped window can't fan out unboundedly;
|
|
2685
|
+
per-record failures are isolated so one bad row never aborts the sweep.
|
|
2686
|
+
|
|
2687
|
+
The automation engine routes a start node carrying `config.timeRelative` to the
|
|
2688
|
+
`time_relative` trigger (ahead of the plain `schedule` trigger, whose behavior is
|
|
2689
|
+
unchanged), and `os validate` gains readiness checks for the new descriptor
|
|
2690
|
+
(unknown swept object, ambiguous draft status). New authorable spec key:
|
|
2691
|
+
`TimeRelativeTriggerSchema` (`@objectstack/spec/automation`).
|
|
2692
|
+
|
|
2693
|
+
### Patch Changes
|
|
2694
|
+
|
|
2695
|
+
- 524696a: feat(spec)!: `DashboardWidgetSchema.strict()` — reject undeclared widget keys (framework#3251)
|
|
2696
|
+
|
|
2697
|
+
The ADR-0021 analytics endpoint. `DashboardWidgetSchema` now rejects any
|
|
2698
|
+
undeclared top-level key instead of silently stripping it, moving a whole class
|
|
2699
|
+
of author error (a hallucinated or legacy key that renders as a silent no-op)
|
|
2700
|
+
from fallible human review to deterministic CI. `options: z.unknown()` remains
|
|
2701
|
+
the escape hatch for renderer-specific extras.
|
|
2702
|
+
|
|
2703
|
+
A custom error map names the offending key(s) and, when a key is a removed
|
|
2704
|
+
pre-ADR-0021 inline-analytics key (`object` / `categoryField` / `valueField` /
|
|
2705
|
+
`aggregate`, pivot `rowField` / `columnField`) or an objectui-internal prop
|
|
2706
|
+
(`component`, inline `data`), points the author at the dataset shape
|
|
2707
|
+
(`dataset` + `dimensions` + `values`).
|
|
2708
|
+
|
|
2709
|
+
Recorded as protocol-16 migration `step16`
|
|
2710
|
+
(`dashboard-widget-strict-unknown-keys`), mirroring protocol-15's `step15`
|
|
2711
|
+
strict flip on the form/page schemas (ADR-0089 D3a). The inline-analytics shape
|
|
2712
|
+
itself was already removed at protocol 9 (single-form cutover), so there is no
|
|
2713
|
+
mechanical rewrite — the residue is the strictness, delegated to the author.
|
|
2714
|
+
|
|
2715
|
+
**Breaking:** shipped as `minor` per the launch-window policy (a breaking change
|
|
2716
|
+
does not burn a major while the stack is in lockstep), riding the already-pending
|
|
2717
|
+
16.0.0 train. The release train's Version-Packages PR must set
|
|
2718
|
+
`PROTOCOL_VERSION = '16.0.0'`; until then `step16` is inert
|
|
2719
|
+
(`composeMigrationChain` caps at `PROTOCOL_MAJOR`).
|
|
2720
|
+
|
|
2721
|
+
`@objectstack/lint` — the `widget-legacy-analytics-shape` /
|
|
2722
|
+
`widget-legacy-analytics-unrenderable` rules are retained as the friendly,
|
|
2723
|
+
suppressible bridge on the raw-config lint/doctor paths (strict preempts them on
|
|
2724
|
+
the schema-parsed compile/validate paths); doc comment updated to explain the
|
|
2725
|
+
interplay.
|
|
2726
|
+
|
|
2727
|
+
- 8923843: Reject view containers that define no views. A flat list-view object (`{ name, label, type, columns, ... }`) parses to an empty `ViewSchema` container because Zod strips unknown keys — zero views register and the Console silently renders nothing. `defineView()` now throws on a zero-view container, and `os validate` gains a `view-container-shape` check (`validateViewContainers` in `@objectstack/lint`) that reports flat or empty `views: []` entries pre-parse with a wrap-it fix hint.
|
|
2728
|
+
- Updated dependencies [f972574]
|
|
2729
|
+
- Updated dependencies [22013aa]
|
|
2730
|
+
- Updated dependencies [3ad3dd5]
|
|
2731
|
+
- Updated dependencies [3a18b60]
|
|
2732
|
+
- Updated dependencies [a8aa34c]
|
|
2733
|
+
- Updated dependencies [a3823b2]
|
|
2734
|
+
- Updated dependencies [43a3efb]
|
|
2735
|
+
- Updated dependencies [524696a]
|
|
2736
|
+
- Updated dependencies [6b51346]
|
|
2737
|
+
- Updated dependencies [80273c8]
|
|
2738
|
+
- Updated dependencies [5e3301d]
|
|
2739
|
+
- Updated dependencies [46e876c]
|
|
2740
|
+
- Updated dependencies [158aa14]
|
|
2741
|
+
- Updated dependencies [d2723e2]
|
|
2742
|
+
- Updated dependencies [fefcd54]
|
|
2743
|
+
- Updated dependencies [beaf2de]
|
|
2744
|
+
- Updated dependencies [369eb6e]
|
|
2745
|
+
- Updated dependencies [b659111]
|
|
2746
|
+
- Updated dependencies [5754a23]
|
|
2747
|
+
- Updated dependencies [6c270a6]
|
|
2748
|
+
- Updated dependencies [668dd17]
|
|
2749
|
+
- Updated dependencies [8abf133]
|
|
2750
|
+
- Updated dependencies [e0859b1]
|
|
2751
|
+
- Updated dependencies [04ecd4e]
|
|
2752
|
+
- Updated dependencies [4d5a892]
|
|
2753
|
+
- Updated dependencies [16cebeb]
|
|
2754
|
+
- Updated dependencies [86d30af]
|
|
2755
|
+
- Updated dependencies [8923843]
|
|
2756
|
+
- Updated dependencies [ea32ec7]
|
|
2757
|
+
- Updated dependencies [a2795f6]
|
|
2758
|
+
- Updated dependencies [f16b492]
|
|
2759
|
+
- Updated dependencies [4b6fde8]
|
|
2760
|
+
- Updated dependencies [2018df9]
|
|
2761
|
+
- Updated dependencies [fc5a3a2]
|
|
2762
|
+
- @objectstack/spec@16.0.0-rc.0
|
|
2763
|
+
- @objectstack/formula@16.0.0-rc.0
|
|
2764
|
+
- @objectstack/sdui-parser@16.0.0-rc.0
|
|
2765
|
+
|
|
2766
|
+
## 15.1.1
|
|
2767
|
+
|
|
2768
|
+
### Patch Changes
|
|
2769
|
+
|
|
2770
|
+
- @objectstack/spec@15.1.1
|
|
2771
|
+
- @objectstack/formula@15.1.1
|
|
2772
|
+
- @objectstack/sdui-parser@15.1.1
|
|
2773
|
+
|
|
2774
|
+
## 15.1.0
|
|
2775
|
+
|
|
2776
|
+
### Patch Changes
|
|
2777
|
+
|
|
2778
|
+
- f531a26: ADR-0085 #2548 follow-ups surfaced by the real-backend browser pass:
|
|
2779
|
+
|
|
2780
|
+
- **lint**: new `field-group-shadowed` warning in `validate-semantic-roles` — a
|
|
2781
|
+
declared fieldGroup whose every visible member is hoisted into the detail
|
|
2782
|
+
highlight strip (or is the record title) renders on forms but silently never
|
|
2783
|
+
on detail pages (detail bodies hide the first 4 highlightFields). Warning
|
|
2784
|
+
tier, same as the other semantic-role rules.
|
|
2785
|
+
- **plugin-audit**: feed/audit summaries ("Created … / Deleted … / Updated …")
|
|
2786
|
+
now name the object by its display label ("Semantic Zoo") instead of its API
|
|
2787
|
+
name ("showcase_semantic_zoo") — these strings render verbatim in the record
|
|
2788
|
+
Discussion feed and Setup dashboards. Falls back to the API name when the
|
|
2789
|
+
object definition isn't resolvable. Existing stored rows are unchanged.
|
|
2790
|
+
|
|
2791
|
+
- Updated dependencies [f531a26]
|
|
2792
|
+
- Updated dependencies [f531a26]
|
|
2793
|
+
- Updated dependencies [f531a26]
|
|
2794
|
+
- Updated dependencies [f531a26]
|
|
2795
|
+
- Updated dependencies [f531a26]
|
|
2796
|
+
- Updated dependencies [f531a26]
|
|
2797
|
+
- Updated dependencies [3fe9df1]
|
|
2798
|
+
- Updated dependencies [f531a26]
|
|
2799
|
+
- Updated dependencies [f531a26]
|
|
2800
|
+
- Updated dependencies [f531a26]
|
|
2801
|
+
- Updated dependencies [f531a26]
|
|
2802
|
+
- Updated dependencies [f531a26]
|
|
2803
|
+
- Updated dependencies [f531a26]
|
|
2804
|
+
- Updated dependencies [f531a26]
|
|
2805
|
+
- Updated dependencies [f531a26]
|
|
2806
|
+
- Updated dependencies [f531a26]
|
|
2807
|
+
- Updated dependencies [f531a26]
|
|
2808
|
+
- Updated dependencies [f531a26]
|
|
2809
|
+
- Updated dependencies [4109153]
|
|
2810
|
+
- Updated dependencies [f531a26]
|
|
2811
|
+
- Updated dependencies [f531a26]
|
|
2812
|
+
- Updated dependencies [f531a26]
|
|
2813
|
+
- Updated dependencies [f531a26]
|
|
2814
|
+
- Updated dependencies [f531a26]
|
|
2815
|
+
- Updated dependencies [f531a26]
|
|
2816
|
+
- Updated dependencies [627f225]
|
|
2817
|
+
- Updated dependencies [f531a26]
|
|
2818
|
+
- Updated dependencies [f531a26]
|
|
2819
|
+
- Updated dependencies [f531a26]
|
|
2820
|
+
- @objectstack/spec@15.1.0
|
|
2821
|
+
- @objectstack/formula@15.1.0
|
|
2822
|
+
- @objectstack/sdui-parser@15.1.0
|
|
2823
|
+
|
|
2824
|
+
## 15.0.0
|
|
2825
|
+
|
|
2826
|
+
### Minor Changes
|
|
2827
|
+
|
|
2828
|
+
- 891ea81: ADR-0089 D3b: make the `visibility-root-mislayered` lint check bidirectional. `validateVisibilityPredicates` now accepts an optional `{ layer }` option — `'runtime'` (default, unchanged) flags a `data.`-rooted predicate on a `*.view.ts` / `*.page.ts` surface, and `'metadata'` flags a `record.`-rooted predicate on a `*.form.ts` metadata-editing form. Both directions of the ADR's binding-root rule are now covered. Adds the `VisibilityLayer` / `VisibilityOptions` exported types. Fully back-compat: existing single-argument callers keep the runtime behavior.
|
|
2829
|
+
- e62c233: feat(spec,plugin-security): package-level capability declaration API (ADR-0066 D1)
|
|
2830
|
+
|
|
2831
|
+
Packages can now DEFINE their own authorization capabilities explicitly via the
|
|
2832
|
+
new `defineCapability` factory and a stack's `capabilities` array, instead of
|
|
2833
|
+
relying on the implicit "derive an untitled capability from whatever a permission
|
|
2834
|
+
set references in `systemPermissions[]`" back-door.
|
|
2835
|
+
|
|
2836
|
+
- `@objectstack/spec`: new `defineCapability` / `CapabilityDeclarationSchema`
|
|
2837
|
+
(`{ name, label?, description?, scope, packageId? }`) and a `capabilities`
|
|
2838
|
+
field on the stack definition.
|
|
2839
|
+
- `@objectstack/plugin-security`: new `bootstrapDeclaredCapabilities` seeds
|
|
2840
|
+
declared capabilities into `sys_capability` with `managed_by:'package'` +
|
|
2841
|
+
`package_id` provenance (new `package_id` field on the object). Idempotent,
|
|
2842
|
+
upgrade-aware; refuses to hijack curated platform capabilities or another
|
|
2843
|
+
package's rows, never clobbers admin-authored rows, and CLAIMS a pre-existing
|
|
2844
|
+
derived placeholder (upgrading it to package provenance). The implicit
|
|
2845
|
+
derive-from-`systemPermissions` path still runs for back-compat but now skips
|
|
2846
|
+
any explicitly-declared name so it can't clobber authored metadata.
|
|
2847
|
+
- `@objectstack/runtime`: stack-declared `capabilities` are registered into the
|
|
2848
|
+
metadata registry (type `capability`) so the boot seeder can read them.
|
|
2849
|
+
- `@objectstack/lint`: `validateCapabilityReferences` treats
|
|
2850
|
+
`stack.capabilities` names as a known capability source.
|
|
2851
|
+
|
|
2852
|
+
A capability is not a contract: DEFINE it (`defineCapability`), GRANT it
|
|
2853
|
+
(`systemPermissions`), REQUIRE it (`requiredPermissions`) — no `inputs`.
|
|
2854
|
+
Aligns with ADR-0094 D5 (retire implicit `managed_by`-guessing back-doors).
|
|
2855
|
+
|
|
2856
|
+
### Patch Changes
|
|
2857
|
+
|
|
2858
|
+
- Updated dependencies [28b7c28]
|
|
2859
|
+
- Updated dependencies [13749ec]
|
|
2860
|
+
- Updated dependencies [e62c233]
|
|
2861
|
+
- Updated dependencies [ed61c9b]
|
|
2862
|
+
- Updated dependencies [31d04d4]
|
|
2863
|
+
- @objectstack/spec@15.0.0
|
|
2864
|
+
- @objectstack/formula@15.0.0
|
|
2865
|
+
- @objectstack/sdui-parser@15.0.0
|
|
2866
|
+
|
|
2867
|
+
## 14.8.0
|
|
2868
|
+
|
|
2869
|
+
### Minor Changes
|
|
2870
|
+
|
|
2871
|
+
- 10e8983: ADR-0089 D3b: add the `validateVisibilityPredicates` lint rule for conditional-visibility keys, wired into `os validate` and `os compile` as advisory warnings.
|
|
2872
|
+
|
|
2873
|
+
Two rules, both `warning` (never fail the build):
|
|
2874
|
+
|
|
2875
|
+
- `visibility-alias-deprecated` — a `visibleOn` (view form section/field) or `visibility` (page component) key in authored source. It still works — the schema normalizes it to `visibleWhen` at parse — but the canonical key is `visibleWhen`. Fix: rename the key (same CEL value).
|
|
2876
|
+
- `visibility-root-mislayered` — a runtime view/page visibility predicate rooted at `data.` (the metadata-editing-form root). Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`), so a `data.`-rooted predicate here never matches and the element renders unconditionally. Fix: use `record.`/`page.`.
|
|
2877
|
+
|
|
2878
|
+
The rule runs on the **pre-parse** stack (like `validate-list-view-mode`) so it can see the deprecated alias the author actually wrote before the schema folds it into `visibleWhen`.
|
|
2879
|
+
|
|
2880
|
+
### Patch Changes
|
|
2881
|
+
|
|
2882
|
+
- Updated dependencies [16b4bf6]
|
|
2883
|
+
- Updated dependencies [16b4bf6]
|
|
2884
|
+
- Updated dependencies [10e8983]
|
|
2885
|
+
- Updated dependencies [607aaf4]
|
|
2886
|
+
- Updated dependencies [bb71321]
|
|
2887
|
+
- @objectstack/spec@14.8.0
|
|
2888
|
+
- @objectstack/formula@14.8.0
|
|
2889
|
+
- @objectstack/sdui-parser@14.8.0
|
|
2890
|
+
|
|
2891
|
+
## 14.7.0
|
|
2892
|
+
|
|
2893
|
+
### Patch Changes
|
|
2894
|
+
|
|
2895
|
+
- Updated dependencies [d6a72eb]
|
|
2896
|
+
- @objectstack/spec@14.7.0
|
|
2897
|
+
- @objectstack/formula@14.7.0
|
|
2898
|
+
- @objectstack/sdui-parser@14.7.0
|
|
2899
|
+
|
|
2900
|
+
## 14.6.0
|
|
2901
|
+
|
|
2902
|
+
### Patch Changes
|
|
2903
|
+
|
|
2904
|
+
- Updated dependencies [609cb13]
|
|
2905
|
+
- Updated dependencies [ce6d151]
|
|
2906
|
+
- @objectstack/spec@14.6.0
|
|
2907
|
+
- @objectstack/formula@14.6.0
|
|
2908
|
+
- @objectstack/sdui-parser@14.6.0
|
|
2909
|
+
|
|
2910
|
+
## 14.5.0
|
|
2911
|
+
|
|
2912
|
+
### Patch Changes
|
|
2913
|
+
|
|
2914
|
+
- Updated dependencies [526805e]
|
|
2915
|
+
- Updated dependencies [d79ca07]
|
|
2916
|
+
- Updated dependencies [33ebd34]
|
|
2917
|
+
- Updated dependencies [c044f08]
|
|
2918
|
+
- Updated dependencies [01274eb]
|
|
2919
|
+
- @objectstack/spec@14.5.0
|
|
2920
|
+
- @objectstack/formula@14.5.0
|
|
2921
|
+
- @objectstack/sdui-parser@14.5.0
|
|
2922
|
+
|
|
2923
|
+
## 14.4.0
|
|
2924
|
+
|
|
2925
|
+
### Minor Changes
|
|
2926
|
+
|
|
2927
|
+
- 82e745e: ADR-0091 L1 — grant validity windows: effective-dated assignments, resolution-time filtering, explain expired state, authoring lint.
|
|
2928
|
+
|
|
2929
|
+
- **plugin-security (objects)**: `sys_user_position` and `sys_user_permission_set` gain the D1 lifecycle columns — `valid_from`, `valid_until` (half-open `[from, until)`, UTC; null = unbounded, existing rows unchanged), `reason`, `delegated_from`, `last_certified_at`, `certified_by`.
|
|
2930
|
+
- **core**: new shared predicate `isGrantActive` / `isGrantExpired` (`@objectstack/core`), and `resolveAuthzContext` now filters BOTH grant tables through it (D2, fail-closed — an expired unscoped `admin_full_access` grant no longer derives `platform_admin`). Present-but-unparseable bounds fail closed.
|
|
2931
|
+
- **plugin-security (explain)**: `buildContextForUser` applies the same filter and returns `expiredGrants`; the principal layer reports the dedicated "held until … — expired" contributor state so "why did access disappear" is self-answering. Spec `ExplainLayerSchema` contributors gain an optional `state: 'active' | 'expired'`.
|
|
2932
|
+
- **plugin-sharing**: `PositionGraphService.expandPositionUsers` filters expired holders — sharing-rule recipients stop including them at resolution time.
|
|
2933
|
+
- **lint (D7)**: two new error rules over seed data — `security-grant-expired-at-authoring` (a `valid_until` in the past, or unparseable, is a grant that can never resolve) and `security-delegation-missing-reason` (a `delegated_from` row without `reason` breaks the D3 dual audit). Also re-exported the missing `SECURITY_MASTER_DETAIL_UNGRANTED` constant.
|
|
2934
|
+
|
|
2935
|
+
No background job is involved anywhere — per ADR-0049, an expired grant simply stops resolving, in every edition.
|
|
2936
|
+
|
|
2937
|
+
- 7449476: Permission-zoo audit follow-ups:
|
|
2938
|
+
|
|
2939
|
+
**FLS keys must be object-qualified (`security-fls-unqualified-key`, error).**
|
|
2940
|
+
The runtime evaluator matches field-permission keys by `<object>.<field>`
|
|
2941
|
+
prefix — a bare `budget` key matches NOTHING and the declared masking
|
|
2942
|
+
silently never enforces. The showcase itself shipped exactly that bug: its
|
|
2943
|
+
contributor FLS block (bare `budget`/`spent`/`budget_remaining`) was a
|
|
2944
|
+
runtime no-op, and the "FLS proof" in earlier verification was actually a
|
|
2945
|
+
validation-rule rejection. Fixed: keys qualified
|
|
2946
|
+
(`showcase_project.budget` …), a new D7 lint rule rejects bare keys at
|
|
2947
|
+
compile time with a fix-it, and the permission-zoo dogfood now proves the
|
|
2948
|
+
served pipeline denies a contributor's budget write while allowing ordinary
|
|
2949
|
+
field edits.
|
|
2950
|
+
|
|
2951
|
+
**Release pipeline: PROTOCOL_VERSION auto-sync.** `changeset version` now
|
|
2952
|
+
runs `scripts/sync-protocol-version.mjs`, regenerating the handshake
|
|
2953
|
+
constant from the spec package major. Release PRs opened by
|
|
2954
|
+
changesets/action with the default GITHUB_TOKEN never trigger CI (GitHub's
|
|
2955
|
+
anti-recursion rule), so the lockstep guard could only fire AFTER a release
|
|
2956
|
+
merged — the drift class that broke main at 14.0.0 (#2769) is now fixed at
|
|
2957
|
+
version time, the one spot that cannot be skipped.
|
|
2958
|
+
|
|
2959
|
+
**D11 `externalSharingModel` honestly marked.** The dial has no runtime
|
|
2960
|
+
consumer yet (authoring lint + Studio badges only); its liveness entry
|
|
2961
|
+
moves from a bespoke `authorable` status to the documented `planned` +
|
|
2962
|
+
`authorWarn`, and the sharing docs / design doc / showcase comments now say
|
|
2963
|
+
explicitly that evaluation of external principals lands with the
|
|
2964
|
+
principal-taxonomy phase (#2696).
|
|
2965
|
+
|
|
2966
|
+
### Patch Changes
|
|
2967
|
+
|
|
2968
|
+
- Updated dependencies [7953832]
|
|
2969
|
+
- Updated dependencies [82e745e]
|
|
2970
|
+
- Updated dependencies [f3035bd]
|
|
2971
|
+
- Updated dependencies [82c0d94]
|
|
2972
|
+
- Updated dependencies [7449476]
|
|
2973
|
+
- @objectstack/spec@14.4.0
|
|
2974
|
+
- @objectstack/formula@14.4.0
|
|
2975
|
+
- @objectstack/sdui-parser@14.4.0
|
|
2976
|
+
|
|
2977
|
+
## 14.3.0
|
|
2978
|
+
|
|
2979
|
+
### Minor Changes
|
|
2980
|
+
|
|
2981
|
+
- 02f6af4: ADR-0090 follow-through wave: enforce book audience at the read layer; finish the D2/D3 cleanup the P1 rename missed.
|
|
2982
|
+
|
|
2983
|
+
- **rest**: `/meta/book`, `/meta/doc`, and `/meta/book/:name/tree` now ENFORCE
|
|
2984
|
+
the ADR-0046 §6.7 audience model (ADR-0049 — no unenforced security
|
|
2985
|
+
properties): anonymous callers see only `public` books/docs;
|
|
2986
|
+
`{ permissionSet }`-gated books require the caller to hold the named set;
|
|
2987
|
+
a doc's effective audience is the union over the books that CLAIM it
|
|
2988
|
+
(unclaimed docs default to `org`; orphan rendering never inherits `public`).
|
|
2989
|
+
Gated evaluation fails CLOSED when holdings cannot be resolved. `doc`/`book`
|
|
2990
|
+
single-item reads bypass the shared meta cache (per-caller gate vs shared ETag).
|
|
2991
|
+
- **spec**: new pure helpers powering that gate — `audienceAllows`,
|
|
2992
|
+
`resolveDocAudiences`, `docAudienceAllows`, `resolveBookClaimedDocs`
|
|
2993
|
+
(+ `AudienceCaller`/`AudienceBook` types). BREAKING but ships as a `minor`
|
|
2994
|
+
per the launch-window convention (pre-1.0 semantics — breaking changes do
|
|
2995
|
+
not burn a major version number while the whole stack is in lockstep):
|
|
2996
|
+
`METADATA_FORM_REGISTRY` keys `role`/`profile` are gone — `position` is the
|
|
2997
|
+
registered form (the `position` type had LOST its form layout in the P1
|
|
2998
|
+
rename); `EnvironmentArtifactMetadataSchema` declares `positions` instead of
|
|
2999
|
+
retired `roles`/`profiles`.
|
|
3000
|
+
- **plugin-security**: the `security` service exposes
|
|
3001
|
+
`resolvePermissionSetNames(ctx)` — the same resolution as data-plane
|
|
3002
|
+
enforcement, for the docs gate.
|
|
3003
|
+
- **metadata**: artifact ingestion maps `positions → 'position'` (the stale
|
|
3004
|
+
`roles → 'role'` mapping matched nothing since the P1 rename, silently
|
|
3005
|
+
dropping compiled positions from metadata registration).
|
|
3006
|
+
- **lint**: books join the D3 role-word scan (their `audience` is a
|
|
3007
|
+
permission-model reference now), and a new advisory rule
|
|
3008
|
+
`security-book-audience-unknown-set` flags a `{ permissionSet }` audience
|
|
3009
|
+
naming a set the stack does not declare (runtime fails closed — the typo
|
|
3010
|
+
cost is "nobody can read the book", so say it at author time).
|
|
3011
|
+
- **platform-objects**: metadata-form translations regain `position` (all four
|
|
3012
|
+
locales) and drop the retired `role`/`profile` groups, with a vocabulary
|
|
3013
|
+
regression test.
|
|
3014
|
+
|
|
3015
|
+
### Patch Changes
|
|
3016
|
+
|
|
3017
|
+
- Updated dependencies [2a71f48]
|
|
3018
|
+
- Updated dependencies [02f6af4]
|
|
3019
|
+
- Updated dependencies [c1064f1]
|
|
3020
|
+
- @objectstack/spec@14.3.0
|
|
3021
|
+
- @objectstack/formula@14.3.0
|
|
3022
|
+
- @objectstack/sdui-parser@14.3.0
|
|
3023
|
+
|
|
3024
|
+
## 14.2.0
|
|
3025
|
+
|
|
3026
|
+
### Patch Changes
|
|
3027
|
+
|
|
3028
|
+
- Updated dependencies [ac8f029]
|
|
3029
|
+
- Updated dependencies [4ab9958]
|
|
3030
|
+
- @objectstack/spec@14.2.0
|
|
3031
|
+
- @objectstack/formula@14.2.0
|
|
3032
|
+
- @objectstack/sdui-parser@14.2.0
|
|
3033
|
+
|
|
3034
|
+
## 14.1.0
|
|
3035
|
+
|
|
3036
|
+
### Minor Changes
|
|
3037
|
+
|
|
3038
|
+
- 5a8465f: SLA escalation `escalateTo` is position-first (ADR-0090 D3 follow-up to the `position` approver type).
|
|
3039
|
+
|
|
3040
|
+
- **spec**: `ApprovalEscalationSchema.escalateTo` is documented as a position machine name or a
|
|
3041
|
+
specific user id (was "User id, role, or manager level" — the same pre-D3 'role' trap the
|
|
3042
|
+
`position` approver type fixed); the Studio xRef picker kind moves `role` → `position`.
|
|
3043
|
+
- **plugin-approvals**: on escalation, `escalateTo` now expands position holders via
|
|
3044
|
+
`sys_user_position` ∪ the `sys_member.role` transition source (ADR-0057 D4) for both the
|
|
3045
|
+
`reassign` approver hand-off and the `notify` audience. An empty expansion falls back to
|
|
3046
|
+
treating the value as a literal user id, so configs naming a specific user keep working
|
|
3047
|
+
unchanged. The audit trail keeps the authored target.
|
|
3048
|
+
- **lint**: new `approval-escalation-reassign-no-target` warning — `escalation.action: 'reassign'`
|
|
3049
|
+
with no `escalateTo` silently degrades to a notify at runtime; the fix-it prescribes a position
|
|
3050
|
+
or user id target (or `action: 'notify'`).
|
|
3051
|
+
|
|
3052
|
+
### Patch Changes
|
|
3053
|
+
|
|
3054
|
+
- Updated dependencies [5a8465f]
|
|
3055
|
+
- Updated dependencies [7f8620b]
|
|
3056
|
+
- Updated dependencies [82ba3a6]
|
|
3057
|
+
- @objectstack/spec@14.1.0
|
|
3058
|
+
- @objectstack/formula@14.1.0
|
|
3059
|
+
- @objectstack/sdui-parser@14.1.0
|
|
3060
|
+
|
|
3061
|
+
## 14.0.0
|
|
3062
|
+
|
|
3063
|
+
### Minor Changes
|
|
3064
|
+
|
|
3065
|
+
- 216fa9a: Add a `position` approver type so approvals can route to org positions (ADR-0090 D3 fallout).
|
|
3066
|
+
|
|
3067
|
+
Post ADR-0090 D3 the `role` approver type resolves against the better-auth org-membership
|
|
3068
|
+
tier (`sys_member.role`: `owner`/`admin`/`member`) — it was never a position. Downstream
|
|
3069
|
+
apps that authored `{ type: 'role', value: 'sales_manager' }` silently routed approvals to
|
|
3070
|
+
nobody. Now:
|
|
3071
|
+
|
|
3072
|
+
- **spec**: `ApproverType` gains `'position'` — `value` is the position machine name; the
|
|
3073
|
+
approver expands to its holders via `sys_user_position`. Authoring guidance: keep
|
|
3074
|
+
`type: 'role'` ONLY for membership tiers; for org positions use
|
|
3075
|
+
`{ type: 'position', value: '<position_name>' }` (one-line fix for the mismatch above).
|
|
3076
|
+
- **plugin-approvals**: the engine resolves `position` approvers via `sys_user_position` ∪
|
|
3077
|
+
the `sys_member.role` transition source (same semantics as `PositionGraphService` in
|
|
3078
|
+
plugin-sharing). The `department` approver type is now honored by its spec spelling
|
|
3079
|
+
(previously only the off-spec `business_unit`/`bu` dialect matched).
|
|
3080
|
+
- **lint**: new `validateApprovalApprovers` rule — `approval-role-not-membership-tier`
|
|
3081
|
+
warns when a `role` approver's value is not a membership tier and prescribes the
|
|
3082
|
+
`position` rewrite; `approval-approver-type-unknown` flags off-spec approver types
|
|
3083
|
+
(with a `business_unit` → `department` fix-it). Wired into `os lint`.
|
|
3084
|
+
|
|
3085
|
+
### Patch Changes
|
|
3086
|
+
|
|
3087
|
+
- 2f3581f: feat(lint): warn when a master-detail child has no object-level CRUD grant (ADR-0090 D7)
|
|
3088
|
+
|
|
3089
|
+
New security-posture rule `security-master-detail-ungranted` (advisory
|
|
3090
|
+
`warning`; it does not gate the build). A master-detail DETAIL object derives
|
|
3091
|
+
its RECORD-level access from the master (ADR-0055 `controlled_by_parent`,
|
|
3092
|
+
gate ②), but object-level CRUD is a SEPARATE gate ① (`checkObjectPermission`)
|
|
3093
|
+
that is never derived — a permission set that grants the parent but forgets the
|
|
3094
|
+
child denies role-bound non-admin users a 403 before the parent-derived access
|
|
3095
|
+
is ever consulted, surfacing as the silent "can't fill in / can't submit the
|
|
3096
|
+
subtable" trap (framework#2700, downstream os-tianshun-mtc#43).
|
|
3097
|
+
|
|
3098
|
+
The rule flags a non-system detail (has a `master_detail` field) that NO
|
|
3099
|
+
authored permission set grants (explicit entry or `'*'` wildcard). It stays
|
|
3100
|
+
silent when the package authors no permission sets, when a package-declared
|
|
3101
|
+
`'*'` wildcard grant covers every object, or for `sys_*` / `isSystem` objects —
|
|
3102
|
+
keeping the false-positive rate near zero. The residual per-set gap (one role
|
|
3103
|
+
grants it, another forgets it) is intentionally out of scope, and CRUD
|
|
3104
|
+
auto-inheritance is deliberately NOT adopted (secure-by-default, Salesforce
|
|
3105
|
+
parity).
|
|
3106
|
+
|
|
3107
|
+
- Updated dependencies [0a8e685]
|
|
3108
|
+
- Updated dependencies [afa8115]
|
|
3109
|
+
- Updated dependencies [80f12ca]
|
|
3110
|
+
- Updated dependencies [e2fa074]
|
|
3111
|
+
- Updated dependencies [23c8668]
|
|
3112
|
+
- Updated dependencies [29f017d]
|
|
3113
|
+
- Updated dependencies [216fa9a]
|
|
3114
|
+
- Updated dependencies [6c22b12]
|
|
3115
|
+
- @objectstack/spec@14.0.0
|
|
3116
|
+
- @objectstack/formula@14.0.0
|
|
3117
|
+
- @objectstack/sdui-parser@14.0.0
|
|
3118
|
+
|
|
3119
|
+
## 13.0.0
|
|
3120
|
+
|
|
3121
|
+
### Minor Changes
|
|
3122
|
+
|
|
3123
|
+
- b271691: ADR-0090 P3 — security-domain publish linter (D7) and delegated administration (D12).
|
|
3124
|
+
|
|
3125
|
+
**D7 — `validateSecurityPosture` (@objectstack/lint), wired into `os compile` (errors gate the build) and `os lint`.** Rules, each with a failing fixture: `security-owd-unset` (custom object with no `sharingModel` — the objectui#2348 leave_request shape), `security-owd-alias` (retired D4 alias values, with fix-it), `security-external-wider-than-internal` (D11 `external ≤ internal`), `security-wildcard-vama` (`'*'` + View/Modify All outside the platform admin set, ADR-0066), `security-anchor-high-privilege` (an `isDefault`/everyone-suggested set carrying anchor-forbidden bits), `security-role-word` (D3 vocabulary freeze in security identifiers/labels; ARIA/page roles exempt), and advisory `security-private-no-readscope`.
|
|
3126
|
+
|
|
3127
|
+
**D12 — delegated administration (@objectstack/plugin-security `DelegatedAdminGate`).** `PermissionSetSchema.adminScope` (new in spec, persisted as `sys_permission_set.admin_scope`) declares WHERE (a `sys_business_unit` subtree), WHAT (`manageAssignments` / `manageBindings` / `authorEnvironmentSets`), and WHICH sets a delegate may hand out (`assignablePermissionSets` allowlist). Writes to `sys_user_position`, `sys_position_permission_set`, `sys_user_permission_set`, and `sys_permission_set` are now governed: tenant-level admins (ADR-0066 superuser wildcard) pass through; delegates need a covering scope — inside their subtree, allowlisted sets only (to others AND themselves), single-row writes, `granted_by` audit-stamped; everyone else (including holders of plain CRUD on RBAC tables) is denied. Granting or authoring a set that itself carries an `adminScope` requires a held scope that STRICTLY contains it. The `everyone`/`guest` anchors stay tenant-level only, and direct position assignments to an anchor are rejected for every caller.
|
|
3128
|
+
|
|
3129
|
+
**ADR-0090 Addendum — assignment-level BU anchor.** `sys_user_position.business_unit_id` lands with its three consumers scoped: D12 delegation boundary (enforced here), audit fact, and the depth-anchor contract for enterprise `hierarchy-scope-resolver` implementations (documented on `IHierarchyScopeResolver`).
|
|
3130
|
+
|
|
3131
|
+
**D9 tier tightening.** `describeHighPrivilegeBits` moved to `@objectstack/spec/security` (re-exported from plugin-security) alongside new `describeAnchorForbiddenBits`: `guest` bindings now additionally reject edit bits (read-only by default; create stays the case-by-case exception).
|
|
3132
|
+
|
|
3133
|
+
**BREAKING (@objectstack/plugin-security):** exports renamed to the ADR-0090 D3 vocabulary — `SysRole`→`SysPosition`, `SysUserRole`→`SysUserPosition`, `SysRolePermissionSet`→`SysPositionPermissionSet` (no aliases, pre-launch one-step rename). `sys_position` row actions/list views renamed (`activate_position`, …), labels relabeled Role→Position. Non-tenant-admin writes to the RBAC link tables without an `adminScope` are now denied (previously any CRUD grant on those tables sufficed).
|
|
3134
|
+
|
|
3135
|
+
**BREAKING (@objectstack/platform-objects):** `sys_business_unit_member.role_in_business_unit` → `function_in_business_unit` (D3 reserved-word sweep; values member/lead/deputy unchanged).
|
|
3136
|
+
|
|
3137
|
+
- a5a1e41: ADR-0090 P4 — explain engine (D6), access-matrix snapshot gate, recalibrated benchmark.
|
|
3138
|
+
|
|
3139
|
+
**Explain contract (@objectstack/spec).** `ExplainRequestSchema` / `ExplainDecisionSchema` / `ExplainLayerSchema`: `explain(principal, object, operation)` reports the verdict of every evaluation-pipeline layer in order (principal → required_permissions → object_crud → fls → owd_baseline → depth → sharing → vama_bypass → rls), with per-layer contributor attribution (which permission set, reached via which position/baseline) and — for reads — the composed row filter as the machine artifact. Carries the D10 dual attribution (`principalKind`, `onBehalfOf`).
|
|
3140
|
+
|
|
3141
|
+
**Explain engine (@objectstack/plugin-security).** `explainAccess` is "explained by construction": it calls the SAME permission-set resolution, evaluator, FLS mask, and RLS composition the enforcement middleware calls (injected from `SecurityPlugin`), so the report cannot drift from enforcement. Exposed on the `security` kernel service as `explain(request, callerContext)`; explaining another user requires `manage_users` (the target's context is reconstructed from `sys_user_position` / `sys_user_permission_set` with everyone-anchor semantics via `buildContextForUser`).
|
|
3142
|
+
|
|
3143
|
+
**Access-matrix snapshot gate (@objectstack/lint + os compile).** `buildAccessMatrix(stack)` derives the (permission set × object) capability matrix purely from metadata; `diffAccessMatrix` renders semantic review lines ("'crm_admin' gains delete on 'crm_lead'", depth changes, OWD swings, entry add/remove). `os compile` gains an opt-in gate: with `access-matrix.json` committed next to the config, any drift fails the build with those lines until re-snapshotted via `--update-access-matrix` — every capability change becomes a reviewable diff. Seeded for `examples/app-crm`.
|
|
3144
|
+
|
|
3145
|
+
**Benchmark (ADR-0090 Addendum).** `scripts/bench/permission-bench.mts` — single-org 10k users × 1M rows per the recalibrated topology; asserts the O()-shape property (per-request cost independent of user population; unit-depth IN-set cost tracks unit size). Passing at 0.1µs/eval and 59ms/1M-row IN-set scan.
|
|
3146
|
+
|
|
3147
|
+
- 466adf6: Author-time capability-reference lint (ADR-0066 ⑨) — `os validate` / `os lint`
|
|
3148
|
+
now warn when a `requiredPermissions` names a capability that is registered
|
|
3149
|
+
nowhere.
|
|
3150
|
+
|
|
3151
|
+
`requiredPermissions` (on objects, fields, apps, actions) is a free string, so a
|
|
3152
|
+
typo like `mange_users` is schema-valid and fails closed at runtime (the caller
|
|
3153
|
+
is denied) — safe, but silent. The new `validateCapabilityReferences` rule
|
|
3154
|
+
(`@objectstack/lint`) resolves every reference against the author-time known set
|
|
3155
|
+
and warns on the unresolved ones:
|
|
3156
|
+
|
|
3157
|
+
- built-in platform capabilities — now sourced from a single canonical list in
|
|
3158
|
+
`@objectstack/spec` (`security/capabilities.ts`: `PLATFORM_CAPABILITIES` /
|
|
3159
|
+
`PLATFORM_CAPABILITY_NAMES`), which `@objectstack/plugin-security`'s
|
|
3160
|
+
`bootstrapSystemCapabilities` also seeds from (one source of truth, no drift),
|
|
3161
|
+
- any capability a permission set in the stack grants via `systemPermissions`
|
|
3162
|
+
(granting is what declares it — mirrors the runtime derived-defaults rule), and
|
|
3163
|
+
- any `sys_capability` row shipped as seed data.
|
|
3164
|
+
|
|
3165
|
+
It is a **warning**, not an error: a single package can't see capabilities
|
|
3166
|
+
declared by other installed packages, and the reference fails closed anyway.
|
|
3167
|
+
`systemPermissions` itself is never flagged — it is the declaration side, and a
|
|
3168
|
+
package legitimately introduces new capabilities there. The object case also
|
|
3169
|
+
understands the per-operation `requiredPermissions` map form (ADR-0066 ⑤) and
|
|
3170
|
+
points a finding at the exact operation slice.
|
|
3171
|
+
|
|
3172
|
+
### Patch Changes
|
|
3173
|
+
|
|
3174
|
+
- Updated dependencies [6d83431]
|
|
3175
|
+
- Updated dependencies [01917c2]
|
|
3176
|
+
- Updated dependencies [b271691]
|
|
3177
|
+
- Updated dependencies [a5a1e41]
|
|
3178
|
+
- Updated dependencies [466adf6]
|
|
3179
|
+
- Updated dependencies [5be00c3]
|
|
3180
|
+
- Updated dependencies [466adf6]
|
|
3181
|
+
- Updated dependencies [2bee609]
|
|
3182
|
+
- Updated dependencies [fc7e7f7]
|
|
3183
|
+
- @objectstack/spec@13.0.0
|
|
3184
|
+
- @objectstack/formula@13.0.0
|
|
3185
|
+
- @objectstack/sdui-parser@13.0.0
|
|
3186
|
+
|
|
3187
|
+
## 12.6.0
|
|
3188
|
+
|
|
3189
|
+
### Patch Changes
|
|
3190
|
+
|
|
3191
|
+
- Updated dependencies [6cebf22]
|
|
3192
|
+
- @objectstack/spec@12.6.0
|
|
3193
|
+
- @objectstack/formula@12.6.0
|
|
3194
|
+
- @objectstack/sdui-parser@12.6.0
|
|
3195
|
+
|
|
3196
|
+
## 12.5.0
|
|
3197
|
+
|
|
3198
|
+
### Patch Changes
|
|
3199
|
+
|
|
3200
|
+
- Updated dependencies [8b3d363]
|
|
3201
|
+
- @objectstack/spec@12.5.0
|
|
3202
|
+
- @objectstack/formula@12.5.0
|
|
3203
|
+
- @objectstack/sdui-parser@12.5.0
|
|
3204
|
+
|
|
3205
|
+
## 12.4.0
|
|
3206
|
+
|
|
3207
|
+
### Patch Changes
|
|
3208
|
+
|
|
3209
|
+
- Updated dependencies [60dc3ba]
|
|
3210
|
+
- @objectstack/spec@12.4.0
|
|
3211
|
+
- @objectstack/formula@12.4.0
|
|
3212
|
+
- @objectstack/sdui-parser@12.4.0
|
|
3213
|
+
|
|
3214
|
+
## 12.3.0
|
|
3215
|
+
|
|
3216
|
+
### Patch Changes
|
|
3217
|
+
|
|
3218
|
+
- Updated dependencies [e7eceec]
|
|
3219
|
+
- @objectstack/spec@12.3.0
|
|
3220
|
+
- @objectstack/formula@12.3.0
|
|
3221
|
+
- @objectstack/sdui-parser@12.3.0
|
|
3222
|
+
|
|
3223
|
+
## 12.2.0
|
|
3224
|
+
|
|
3225
|
+
### Patch Changes
|
|
3226
|
+
|
|
3227
|
+
- Updated dependencies [fce8ff4]
|
|
3228
|
+
- Updated dependencies [3962023]
|
|
3229
|
+
- Updated dependencies [2bb193d]
|
|
3230
|
+
- Updated dependencies [0426d27]
|
|
3231
|
+
- Updated dependencies [da807f7]
|
|
3232
|
+
- @objectstack/spec@12.2.0
|
|
3233
|
+
- @objectstack/formula@12.2.0
|
|
3234
|
+
- @objectstack/sdui-parser@12.2.0
|
|
3235
|
+
|
|
3236
|
+
## 12.1.0
|
|
3237
|
+
|
|
3238
|
+
### Patch Changes
|
|
3239
|
+
|
|
3240
|
+
- Updated dependencies [93e6d02]
|
|
3241
|
+
- @objectstack/spec@12.1.0
|
|
3242
|
+
- @objectstack/formula@12.1.0
|
|
3243
|
+
- @objectstack/sdui-parser@12.1.0
|
|
3244
|
+
|
|
3245
|
+
## 12.0.0
|
|
3246
|
+
|
|
3247
|
+
### Minor Changes
|
|
3248
|
+
|
|
3249
|
+
- a8df396: feat(spec,lint): adaptive record surface + semantic field `span` for field-heavy objects (#2578)
|
|
3250
|
+
|
|
3251
|
+
Field-heavy objects need two things the protocol did not express well: multi-column
|
|
3252
|
+
forms, and opening create/edit/detail as a full page rather than a cramped popup —
|
|
3253
|
+
for _some_ objects, automatically. Because all metadata is AI-authored, the design
|
|
3254
|
+
goal is to make AI unable to get it wrong, which reshaped both features away from
|
|
3255
|
+
new authored keys.
|
|
3256
|
+
|
|
3257
|
+
**`deriveRecordSurface` (new spec derivation, ADR-0085 §5).** A record's default
|
|
3258
|
+
surface — full `page` vs `drawer`/`modal` overlay — is _derived_ from how heavy the
|
|
3259
|
+
record is (visible, non-system field count; mobile always pages), not authored. Per
|
|
3260
|
+
ADR-0085 §2's admission test a `recordSurface` object key would fail: field count is
|
|
3261
|
+
exactly the kind of fact a machine can infer, and modal-vs-page is pure
|
|
3262
|
+
re-arrangement, not a business fact. So there is **no new object key** and **no new
|
|
3263
|
+
ADR** — just a single shared derivation renderers consume as a default (an explicit
|
|
3264
|
+
form/navigation config still wins), plus a one-line clarification to ADR-0085 §2's
|
|
3265
|
+
rejected-keys list so `recordSurface` is not re-proposed. Explicit per-object control
|
|
3266
|
+
remains the sanctioned assigned-page path.
|
|
3267
|
+
|
|
3268
|
+
**`FormField.span: 'auto' | 'full'` (new, replaces absolute `colSpan` as the
|
|
3269
|
+
primary primitive).** Under a per-surface derived column count (mobile 1 / modal 2 /
|
|
3270
|
+
page 3-4) an absolute `colSpan: 3` only lines up at the one width the author
|
|
3271
|
+
imagined — fragile by construction. The relative `span` is decoupled from the column
|
|
3272
|
+
count: `auto` (default; omit it) sizes by widget type × current columns, `full` takes
|
|
3273
|
+
the whole row at any count. `colSpan` is retained for back-compat and clamped by the
|
|
3274
|
+
renderer; `half` was considered and deferred (weakest AI-safety). The rationale lives
|
|
3275
|
+
here rather than in a new ADR, per the fewer-ADRs convention.
|
|
3276
|
+
|
|
3277
|
+
**`validateFormLayout` (new lint, ADR-0078/0019).** Two advisory rules over authored
|
|
3278
|
+
form views: `form-field-unknown` (a section references a field not on the bound
|
|
3279
|
+
object — silently never renders) and `absolute-colspan-discouraged` (steers authors
|
|
3280
|
+
to `span: 'full'`). Both warnings, with fix hints, held to the same bar for AI and
|
|
3281
|
+
hand authors.
|
|
3282
|
+
|
|
3283
|
+
**`NavigationConfig.size` (new) replaces pixel `width`.** A T-shirt bucket
|
|
3284
|
+
(`auto`/sm/md/lg/xl/full, default `auto`, aligned with `FormView.modalSize`) for a
|
|
3285
|
+
drawer/modal detail overlay. `width`/`drawerWidth` (pixel) are deprecated: a pixel
|
|
3286
|
+
width cannot be authored blind — the author (often an AI) does not know the client
|
|
3287
|
+
viewport. `auto` means the renderer derives the size from field count and clamps to
|
|
3288
|
+
the viewport, so AI writes nothing.
|
|
3289
|
+
|
|
3290
|
+
All additive: no exports removed, no behavior change for existing metadata.
|
|
3291
|
+
|
|
3292
|
+
- e695fe0: feat(spec,lint): reject userFilters on object list views (ADR-0053 phase 4)
|
|
3293
|
+
|
|
3294
|
+
ADR-0053 reserves `userFilters`/`quickFilters` for page lists ("filters" mode);
|
|
3295
|
+
on an object list view ("views" mode — where the `ViewTabBar` is the only nav
|
|
3296
|
+
control) they are silently dropped. This lands the phase-4 guardrail as a
|
|
3297
|
+
layered defence, so the wrong-context authoring mistake is caught without
|
|
3298
|
+
breaking existing metadata:
|
|
3299
|
+
|
|
3300
|
+
- **Type-level (author time):** new `ObjectListViewSchema` = `ListViewSchema`
|
|
3301
|
+
minus `userFilters`. Object built-in `listViews` and `defineView`
|
|
3302
|
+
`list`/`listViews` now use it, so `userFilters` on an object list view is a
|
|
3303
|
+
`tsc` error. The full `ListViewSchema` (page "filters" mode) is untouched.
|
|
3304
|
+
- **Runtime (back-compat):** the field is STRIPPED at parse (default strip, no
|
|
3305
|
+
throw), so existing metadata keeps loading — `ObjectSchema.parse` never fails
|
|
3306
|
+
on a stray `userFilters`.
|
|
3307
|
+
- **Author/CI (actionable):** new `@objectstack/lint` rule
|
|
3308
|
+
`validateListViewMode`, wired into `os validate`, reports the wrong-context
|
|
3309
|
+
field PRE-parse (before the schema strips it) with a fix hint.
|
|
3310
|
+
|
|
3311
|
+
Closes the schema half of objectui #2219; supersedes the interim runtime warn in
|
|
3312
|
+
objectui #2220.
|
|
3313
|
+
|
|
3314
|
+
### Patch Changes
|
|
3315
|
+
|
|
3316
|
+
- Updated dependencies [a8df396]
|
|
3317
|
+
- Updated dependencies [e695fe0]
|
|
3318
|
+
- Updated dependencies [7c09621]
|
|
3319
|
+
- Updated dependencies [7709db4]
|
|
3320
|
+
- Updated dependencies [2082109]
|
|
3321
|
+
- Updated dependencies [7c09621]
|
|
3322
|
+
- Updated dependencies [9860de4]
|
|
3323
|
+
- Updated dependencies [069c205]
|
|
3324
|
+
- @objectstack/spec@12.0.0
|
|
3325
|
+
- @objectstack/formula@12.0.0
|
|
3326
|
+
- @objectstack/sdui-parser@12.0.0
|
|
3327
|
+
|
|
3328
|
+
## 11.10.0
|
|
3329
|
+
|
|
3330
|
+
### Patch Changes
|
|
3331
|
+
|
|
3332
|
+
- 996c548: Load Sucrase lazily in `validateReactPages` instead of at module top level — the same kernel boot-path contract applied to the TypeScript compiler in `validateReactPageProps` (framework#2544).
|
|
3333
|
+
|
|
3334
|
+
`@objectstack/lint` sits on the kernel boot path, so the eager `import { transform } from 'sucrase'` made every boot parse ~1.5 MB of transpiler (~16 ms cold require) for a syntax gate that only runs when a `kind:'react'` page is actually validated — a rare, trusted-tier case. Sucrase now loads on the first validated react-source page via the same deferred-createRequire pattern; the public API stays synchronous and unchanged, `sucrase` stays a regular dependency, and if the package is missing at call time validation fails with an actionable error instead of killing boot.
|
|
3335
|
+
|
|
3336
|
+
The boot-path guard test is generalized from `lazy-typescript.test.ts` to `lazy-deps.test.ts` and now covers both deps at all three levels (structural no-eager-import scan over src, child-process probes of both built dist formats, in-process lazy-load behavior) — verified to go red for each dep when its eager import is reintroduced.
|
|
3337
|
+
|
|
3338
|
+
- e82a495: Load the TypeScript compiler lazily in `validateReactPageProps` instead of at module top level (ADR-0081 Phase 2 follow-up).
|
|
3339
|
+
|
|
3340
|
+
`@objectstack/lint` sits on the kernel boot path, so the eager `import ts from 'typescript'` (framework#2482) made every boot parse the ~9 MB compiler (~70 ms+ on a warm laptop, worse on container cold starts) for a gate that only runs when a `kind:'react'` page is actually validated — a rare, trusted-tier case. It also hard-crashed boot in deployments that prune the package from the image (cloud's Docker pruner did exactly that; worked around in cloud#728).
|
|
3341
|
+
|
|
3342
|
+
- The compiler now loads on the first validated react-source page, via a deferred `createRequire` (same bundling-safe pattern as driver-sqlite-wasm's knex-wasm-dialect); the public API stays synchronous and unchanged.
|
|
3343
|
+
- Importing the package, and validating stacks with no react pages, no longer touches `typescript` at all — so images that prune it boot fine and only fail (with an actionable error naming the package and the fix) if a react-source page is actually validated.
|
|
3344
|
+
- `typescript` remains a regular dependency of `@objectstack/lint`.
|
|
3345
|
+
- Guarded by a three-level regression test (structural no-eager-import scan, child-process probes of both dist formats, in-process lazy-load behavior), verified to go red if the eager import is reintroduced.
|
|
3346
|
+
|
|
3347
|
+
- Updated dependencies [6a9397e]
|
|
3348
|
+
- Updated dependencies [c0efe5d]
|
|
3349
|
+
- @objectstack/spec@11.10.0
|
|
3350
|
+
- @objectstack/formula@11.10.0
|
|
3351
|
+
- @objectstack/sdui-parser@11.10.0
|
|
3352
|
+
|
|
3353
|
+
## 11.9.0
|
|
3354
|
+
|
|
3355
|
+
### Patch Changes
|
|
3356
|
+
|
|
3357
|
+
- Updated dependencies [d3595d9]
|
|
3358
|
+
- @objectstack/spec@11.9.0
|
|
3359
|
+
- @objectstack/formula@11.9.0
|
|
3360
|
+
- @objectstack/sdui-parser@11.9.0
|
|
3361
|
+
|
|
3362
|
+
## 11.8.0
|
|
3363
|
+
|
|
3364
|
+
### Patch Changes
|
|
3365
|
+
|
|
3366
|
+
- @objectstack/spec@11.8.0
|
|
3367
|
+
- @objectstack/formula@11.8.0
|
|
3368
|
+
- @objectstack/sdui-parser@11.8.0
|
|
3369
|
+
|
|
3370
|
+
## 11.7.0
|
|
3371
|
+
|
|
3372
|
+
### Minor Changes
|
|
3373
|
+
|
|
3374
|
+
- 5178906: ADR-0085: object presentation intent is declared as cross-surface semantic
|
|
3375
|
+
roles, never as per-surface hint blocks.
|
|
3376
|
+
|
|
3377
|
+
**@objectstack/spec**
|
|
3378
|
+
|
|
3379
|
+
- New top-level `stageField: string | false` — names the object's linear
|
|
3380
|
+
lifecycle field (`false` declares the status-like field non-linear and
|
|
3381
|
+
suppresses every consumer's stage heuristics). Legitimizes the key the UI
|
|
3382
|
+
runtime already read but the schema rejected.
|
|
3383
|
+
- `compactLayout` → **`highlightFields`** (the value is an ordered field
|
|
3384
|
+
list, not a layout; "highlight" is already the renderer-side term of art).
|
|
3385
|
+
`compactLayout` stays accepted as a parse-time alias and is preserved on
|
|
3386
|
+
output — the ADR-0079 `displayNameField → nameField` pattern.
|
|
3387
|
+
- `fieldGroups[].collapse: 'none' | 'expanded' | 'collapsed'` replaces
|
|
3388
|
+
`defaultExpanded` AND the UI-dialect `collapsible`/`collapsed` boolean pair
|
|
3389
|
+
(which had drifted two ways: spec declared a key no renderer read, renderers
|
|
3390
|
+
read keys the spec rejected). Old keys map onto the enum at parse and remain
|
|
3391
|
+
accepted for one minor.
|
|
3392
|
+
- `fieldGroups[].visibleOn` removed (no consumer anywhere — ADR-0049
|
|
3393
|
+
enforce-or-remove; re-add together with its enforcement when a surface
|
|
3394
|
+
evaluates it).
|
|
3395
|
+
- The `detail: { … }.passthrough()` UI-hints block is **removed**. Every key
|
|
3396
|
+
in it was either unauthorable, a proven no-op for spec authors
|
|
3397
|
+
(`hideReferenceRail` — the rail is default-off and its enabling key was
|
|
3398
|
+
never typed), or a per-page toggle that belongs to an assigned Page. Zero
|
|
3399
|
+
authors existed across framework and objectui (evidence in ADR-0085); the
|
|
3400
|
+
removal ships as a minor under the documented dead-surface exception
|
|
3401
|
+
(PR #2272 precedent).
|
|
3402
|
+
- New `deriveFieldGroupLayout(def)` in `@objectstack/spec/data` — the single
|
|
3403
|
+
source of the fieldGroups rendering semantics (declared order, empty groups
|
|
3404
|
+
dropped, ungrouped trailing bucket minus audit/system fields, collapse
|
|
3405
|
+
passthrough incl. deprecated aliases). UI renderers consume this instead of
|
|
3406
|
+
their two pre-existing near-identical local copies.
|
|
3407
|
+
|
|
3408
|
+
**@objectstack/lint / @objectstack/cli**
|
|
3409
|
+
|
|
3410
|
+
- New `validateSemanticRoles` (wired into `os lint`): warns on
|
|
3411
|
+
`Field.group` → undeclared group, declared-but-unreferenced groups, and
|
|
3412
|
+
`stageField`/`highlightFields` entries naming non-existent fields — the
|
|
3413
|
+
dangling-pointer shapes that are Zod-valid but silently inert at render
|
|
3414
|
+
time (ADR-0078 completeness gate).
|
|
3415
|
+
|
|
3416
|
+
**@objectstack/platform-objects**
|
|
3417
|
+
|
|
3418
|
+
- All 35 system objects renamed `compactLayout:` → `highlightFields:`
|
|
3419
|
+
(behaviour unchanged via the alias).
|
|
3420
|
+
|
|
3421
|
+
### Patch Changes
|
|
3422
|
+
|
|
3423
|
+
- Updated dependencies [5178906]
|
|
3424
|
+
- @objectstack/spec@11.7.0
|
|
3425
|
+
- @objectstack/formula@11.7.0
|
|
3426
|
+
- @objectstack/sdui-parser@11.7.0
|
|
3427
|
+
|
|
3428
|
+
## 11.6.0
|
|
3429
|
+
|
|
3430
|
+
### Patch Changes
|
|
3431
|
+
|
|
3432
|
+
- @objectstack/spec@11.6.0
|
|
3433
|
+
- @objectstack/formula@11.6.0
|
|
3434
|
+
- @objectstack/sdui-parser@11.6.0
|
|
3435
|
+
|
|
3436
|
+
## 11.5.0
|
|
3437
|
+
|
|
3438
|
+
### Minor Changes
|
|
3439
|
+
|
|
3440
|
+
- 5a5bf61: ADR-0081 Phase 2: a build-time prop check for `kind:'react'` pages. After the
|
|
3441
|
+
syntax gate, `validateReactPageProps` parses the real JSX (TypeScript compiler)
|
|
3442
|
+
and checks each usage of an injected block (`<ObjectForm>`, `<ListView>`, …)
|
|
3443
|
+
against the react-tier contract (`REACT_BLOCKS` from `@objectstack/spec/ui`):
|
|
3444
|
+
missing a required binding (e.g. `<ObjectForm>` with no `objectName`) is an
|
|
3445
|
+
error; a near-miss prop (`onSucces` → `onSuccess`) is a warning. Wired into
|
|
3446
|
+
`os validate`. Curated data props are not flagged (low false-positive); a spread
|
|
3447
|
+
`{...props}` escapes the required check. (`typescript` moves to `@objectstack/lint`
|
|
3448
|
+
dependencies so it externalizes instead of bundling into the CLI.)
|
|
3449
|
+
- ec7175d: Add the source-page styling guardrail (ADR-0065): `os validate`/`os build` now flags Tailwind `className` in `kind:'html'`/`kind:'react'` page source, which silently produces no CSS because the build never scans authored metadata. New `validatePageSourceStyling` rule with an actionable inline-style/`hsl(var(--token))` fix; also corrects the react-blocks contract, the objectstack-ui skill, the layout-dsl docs, and ADR-0080/0081 away from the "HTML + Tailwind" framing.
|
|
3450
|
+
|
|
3451
|
+
### Patch Changes
|
|
3452
|
+
|
|
3453
|
+
- Updated dependencies [6ee4f04]
|
|
3454
|
+
- Updated dependencies [c1e3a65]
|
|
3455
|
+
- @objectstack/spec@11.5.0
|
|
3456
|
+
- @objectstack/formula@11.5.0
|
|
3457
|
+
- @objectstack/sdui-parser@11.5.0
|
|
3458
|
+
|
|
3459
|
+
## 11.4.0
|
|
3460
|
+
|
|
3461
|
+
### Minor Changes
|
|
3462
|
+
|
|
3463
|
+
- 5821c51: ADR-0081: split the AI page-authoring surface into honest tiers.
|
|
3464
|
+
|
|
3465
|
+
- `PageSchema.kind` gains `'html'` and `'react'`. `'html'` is the constrained
|
|
3466
|
+
parse-never-execute tier (the renamed `'jsx'`, kept as a deprecated alias);
|
|
3467
|
+
`'react'` is the real-React tier (executed at render by
|
|
3468
|
+
`@object-ui/react-runtime`). It runs author JS, so it is gated by a host
|
|
3469
|
+
capability that **defaults ON** (the platform trusts reviewed, draft-gated
|
|
3470
|
+
authors) and is disabled **server-side** via the `OS_PAGE_REACT=off`
|
|
3471
|
+
env toggle. The completeness gate now requires `source` for all three kinds.
|
|
3472
|
+
- `@objectstack/cli` console serving injects the disable global into the served
|
|
3473
|
+
HTML when `OS_PAGE_REACT=off` (read per request, no rebuild).
|
|
3474
|
+
- `validate-jsx-pages` lints `html`/`jsx` (constrained parse). A new
|
|
3475
|
+
`validate-react-pages` transpiles `react` source with Sucrase (transpile-only,
|
|
3476
|
+
never executed) so syntax errors fail at `os build` instead of at render.
|
|
3477
|
+
|
|
3478
|
+
### Patch Changes
|
|
3479
|
+
|
|
3480
|
+
- Updated dependencies [5821c51]
|
|
3481
|
+
- Updated dependencies [a0fce3f]
|
|
3482
|
+
- @objectstack/spec@11.4.0
|
|
3483
|
+
- @objectstack/formula@11.4.0
|
|
3484
|
+
- @objectstack/sdui-parser@11.4.0
|
|
3485
|
+
|
|
3486
|
+
## 11.3.0
|
|
3487
|
+
|
|
3488
|
+
### Minor Changes
|
|
3489
|
+
|
|
3490
|
+
- 58e8e31: feat(lint): ADR-0079 record-title gate — deprecate titleFormat + record-title validator
|
|
3491
|
+
|
|
3492
|
+
A record's human title is a structural invariant (ADR-0079): every object
|
|
3493
|
+
resolves a primary title from a real STORED field via `nameField` (the
|
|
3494
|
+
canonical pointer; `displayNameField` is the deprecated alias) or a
|
|
3495
|
+
deterministic derivation. This adds build-time diagnostics so `os build` /
|
|
3496
|
+
`os lint`, the MCP authoring surface, and hand-authoring all get the coverage
|
|
3497
|
+
cloud graph-lint already has (the ADR-0078 "not cloud-only" principle):
|
|
3498
|
+
|
|
3499
|
+
- `title-format-retired` — flags an object that declares a `titleFormat`. That
|
|
3500
|
+
key is a render-only template the server can neither return nor query;
|
|
3501
|
+
ADR-0079 retires it in favour of `nameField`. The schema still parses it
|
|
3502
|
+
(existing metadata keeps loading), so this is advisory, not an error.
|
|
3503
|
+
- `title-unresolvable` — flags an object whose title cannot be resolved from any
|
|
3504
|
+
stored field (`objectTitleCompleteness` reports `status: 'none'`).
|
|
3505
|
+
|
|
3506
|
+
`@objectstack/spec` carries the `titleFormat` `.describe()` deprecation note;
|
|
3507
|
+
the `@objectstack/cli` `lint` command wires the new validator into its run.
|
|
3508
|
+
|
|
3509
|
+
### Patch Changes
|
|
3510
|
+
|
|
3511
|
+
- Updated dependencies [58e8e31]
|
|
3512
|
+
- Updated dependencies [b4a5df0]
|
|
3513
|
+
- @objectstack/spec@11.3.0
|
|
3514
|
+
- @objectstack/formula@11.3.0
|
|
3515
|
+
- @objectstack/sdui-parser@11.3.0
|
|
3516
|
+
|
|
3517
|
+
## 11.2.0
|
|
3518
|
+
|
|
3519
|
+
### Minor Changes
|
|
3520
|
+
|
|
3521
|
+
- 8ea1f4f: ADR-0080 M3b②: `os validate` / `os build` now parse `kind:'jsx'` page `source` via `@objectstack/sdui-parser` (new `validateJsxPages` lint rule) — malformed JSX fails loudly at author time (ADR-0078) instead of being stored and breaking only at render. Parse-level for now (syntax, tag matching, forbidden constructs like event handlers / dangerouslySetInnerHTML); full component/prop whitelist validation arrives once the registry manifest is threaded through `compile()`.
|
|
3522
|
+
- 21c37d8: ADR-0080 M3b① (consumption seam): the `os build` / `os validate` JSX gate now does **full component/prop validation** (unknown component, missing/wrong prop, bad enum, bindings) when a `sdui.manifest.json` is present at the project root — falling back to parse-level otherwise. `validateJsxPages` accepts an optional manifest; the validate command loads the file when present. Generating + shipping that manifest from the registry's public tier remains a build/CI step.
|
|
3523
|
+
|
|
3524
|
+
### Patch Changes
|
|
3525
|
+
|
|
3526
|
+
- Updated dependencies [d0f4b13]
|
|
3527
|
+
- Updated dependencies [302bdab]
|
|
3528
|
+
- Updated dependencies [012c046]
|
|
3529
|
+
- @objectstack/spec@11.2.0
|
|
3530
|
+
- @objectstack/sdui-parser@11.2.0
|
|
3531
|
+
- @objectstack/formula@11.2.0
|
|
3532
|
+
|
|
3533
|
+
## 11.1.0
|
|
3534
|
+
|
|
3535
|
+
### Patch Changes
|
|
3536
|
+
|
|
3537
|
+
- Updated dependencies [ecf193f]
|
|
3538
|
+
- Updated dependencies [51bec81]
|
|
3539
|
+
- Updated dependencies [3e593a7]
|
|
3540
|
+
- Updated dependencies [63d5403]
|
|
3541
|
+
- @objectstack/spec@11.1.0
|
|
3542
|
+
- @objectstack/formula@11.1.0
|
|
3543
|
+
|
|
3544
|
+
## 11.0.0
|
|
3545
|
+
|
|
3546
|
+
### Patch Changes
|
|
3547
|
+
|
|
3548
|
+
- Updated dependencies [ab5718a]
|
|
3549
|
+
- Updated dependencies [4845c12]
|
|
3550
|
+
- Updated dependencies [c1a754a]
|
|
3551
|
+
- Updated dependencies [6fbe91f]
|
|
3552
|
+
- Updated dependencies [715d667]
|
|
3553
|
+
- Updated dependencies [5eef4cf]
|
|
3554
|
+
- Updated dependencies [72759e1]
|
|
3555
|
+
- Updated dependencies [6c4fbd9]
|
|
3556
|
+
- Updated dependencies [ef3ed67]
|
|
3557
|
+
- Updated dependencies [cd51229]
|
|
3558
|
+
- Updated dependencies [7697a0e]
|
|
3559
|
+
- Updated dependencies [e7e04f1]
|
|
3560
|
+
- Updated dependencies [cfd5ac4]
|
|
3561
|
+
- Updated dependencies [2be5c1f]
|
|
3562
|
+
- Updated dependencies [ad143ce]
|
|
3563
|
+
- Updated dependencies [5c4a8c8]
|
|
3564
|
+
- Updated dependencies [3afaeed]
|
|
3565
|
+
- Updated dependencies [8801c02]
|
|
3566
|
+
- Updated dependencies [3d04e06]
|
|
3567
|
+
- Updated dependencies [4a84c98]
|
|
3568
|
+
- Updated dependencies [d980f0d]
|
|
3569
|
+
- Updated dependencies [a658523]
|
|
3570
|
+
- Updated dependencies [82ff91c]
|
|
3571
|
+
- Updated dependencies [638f472]
|
|
3572
|
+
- @objectstack/spec@11.0.0
|
|
3573
|
+
- @objectstack/formula@11.0.0
|
|
3574
|
+
|
|
3575
|
+
## 10.3.0
|
|
3576
|
+
|
|
3577
|
+
### Minor Changes
|
|
3578
|
+
|
|
3579
|
+
- f75943a: feat(lint): SDUI styling validator (ADR-0065)
|
|
3580
|
+
|
|
3581
|
+
`validateResponsiveStyles` — a pure `(stack) => Finding[]` rule wired into
|
|
3582
|
+
`os validate` and `os compile`, so hand-authored and AI-generated pages are
|
|
3583
|
+
held to the same bar (ADR-0019). Catches the deterministic ways a
|
|
3584
|
+
`responsiveStyles` block silently fails: a styled node with no `id` (CSS can't
|
|
3585
|
+
be scoped → dropped) is an **error**; warnings cover Tailwind-in-`className`
|
|
3586
|
+
(silently dead in metadata), a smaller breakpoint with no `large` base, unknown
|
|
3587
|
+
CSS properties, and unknown/typo'd design tokens. Quality/visual judgement
|
|
3588
|
+
(is it ugly) is out of scope — that needs render + a VLM gate.
|
|
3589
|
+
|
|
3590
|
+
### Patch Changes
|
|
3591
|
+
|
|
3592
|
+
- @objectstack/spec@10.3.0
|
|
3593
|
+
- @objectstack/formula@10.3.0
|
|
3594
|
+
|
|
3595
|
+
## 10.2.0
|
|
3596
|
+
|
|
3597
|
+
### Minor Changes
|
|
3598
|
+
|
|
3599
|
+
- 63f3219: feat(lint): extract static metadata validators into @objectstack/lint (ADR-0019 P3)
|
|
3600
|
+
|
|
3601
|
+
New public package `@objectstack/lint` holds the pure, build-time metadata
|
|
3602
|
+
validators as `(stack) => Finding[]` functions, so the same rules run wherever a
|
|
3603
|
+
stack can be assembled — the CLI's `os validate`/`compile` and any other
|
|
3604
|
+
consumer (notably AI-driven authoring), instead of being trapped in CLI
|
|
3605
|
+
internals where only the CLI could reach them.
|
|
3606
|
+
|
|
3607
|
+
First release moves the two validators the AI build needs:
|
|
3608
|
+
|
|
3609
|
+
- `validateWidgetBindings` — dashboard widget → dataset → measure/dimension
|
|
3610
|
+
reference integrity + measure-aggregation coherence (ADR-0021).
|
|
3611
|
+
- `validateStackExpressions` — CEL/predicate validity for field conditionals,
|
|
3612
|
+
sharing rules, action visible/disabled, lifecycle hooks (ADR-0032).
|
|
3613
|
+
|
|
3614
|
+
`@objectstack/cli` now imports both from `@objectstack/lint` (was `./utils/*`);
|
|
3615
|
+
pure move, no behavior change. Dependency direction is one-way `lint → spec`;
|
|
3616
|
+
the package never depends on a runtime and is never bundled into a frontend
|
|
3617
|
+
(that is why the validators do NOT live in the frontend-facing `@objectstack/spec`).
|
|
3618
|
+
|
|
3619
|
+
Filesystem-coupled checks (`lint-liveness-properties`) and CLI-command-coupled
|
|
3620
|
+
ones (`score` → `lintConfig`) deliberately stay in the CLI for now; they can
|
|
3621
|
+
move in a later increment.
|
|
3622
|
+
|
|
3623
|
+
### Patch Changes
|
|
3624
|
+
|
|
3625
|
+
- Updated dependencies [b496498]
|
|
3626
|
+
- @objectstack/spec@10.2.0
|
|
3627
|
+
- @objectstack/formula@10.2.0
|