@objectstack/lint 17.0.0 → 17.1.0
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 +622 -0
- package/dist/index.cjs +894 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +310 -66
- package/dist/index.d.ts +310 -66
- package/dist/index.js +871 -278
- package/dist/index.js.map +1 -1
- package/dist/{runtime-H-nDodRy.d.cts → runtime-s3X9D9hm.d.cts} +89 -2
- package/dist/{runtime-H-nDodRy.d.ts → runtime-s3X9D9hm.d.ts} +89 -2
- package/dist/runtime.cjs +986 -409
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +959 -378
- package/dist/runtime.js.map +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,627 @@
|
|
|
1
1
|
# @objectstack/lint
|
|
2
2
|
|
|
3
|
+
## 17.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 13d7864: Dashboard writes are now judged by `validateWidgetBindings` at the runtime publish gate (#7529). A dashboard widget bound to a dataset that resolves to nothing — previously a `200` on both save and publish, failing only as a runtime error on the live board — is refused at **publish** with a located 422 (`INVALID_METADATA`, the offending key path named). Drafts are unaffected: a draft may still hold a forward reference to a dataset not yet authored, and only the draft→active promotion runs the gate.
|
|
8
|
+
|
|
9
|
+
Because rule surfaces are registered per-rule, all six of the rule's error-tier findings now gate a dashboard publish as one reference-integrity class: `widget-dataset-unknown`, `widget-dimension-unknown`, `widget-measure-unknown`, `chart-field-unknown`, `widget-legacy-analytics-unrenderable`, `dashboard-filter-field-unknown`. Warning-tier findings (`table-count-only`, `chart-config-missing`, …) ride the non-blocking `advisories` channel on the save response. Config-authored stacks are unaffected — `os validate` / `os build` / `os lint` already ran this rule; the newly gated population is exactly the `sys_metadata` overlay writes (Studio / REST `/meta` / MCP) that previously bypassed it.
|
|
10
|
+
|
|
11
|
+
The per-write snapshot (`RuntimeStackContext`) now carries the live `datasets` collection so bindings resolve against the real dataset universe — without it every legitimate board would read as dangling. Existing stored rows are untouched (the gate blocks new publishes only), and `OS_ALLOW_UNLINTED_METADATA_WRITES=1` remains the migration-window escape hatch.
|
|
12
|
+
- 8640fb2: `os validate`: a dashboard header `modal` action's target resolves against declared PAGES, only (#9013)
|
|
13
|
+
|
|
14
|
+
`validateDashboardActionRefs` resolved an `actionType: 'modal'` header button's
|
|
15
|
+
`actionUrl` the way objectui's `DashboardView` used to dispatch it: a defined
|
|
16
|
+
action name, a bare object name, or the `<verb>_<object>` prefix form
|
|
17
|
+
(`create_`/`new_`/`add_`/`edit_`/`update_` + a defined object) all passed, and a
|
|
18
|
+
target naming a declared page ERRORED unless it collided with one of those.
|
|
19
|
+
|
|
20
|
+
That mirror is gone. Maintainer ruling objectstack#6739-A (2026-08-09): a
|
|
21
|
+
`type: 'modal'` string target names a PAGE, only — the spec TSDoc, the published
|
|
22
|
+
docs and `defineStack`'s cross-reference walk already said so, and objectui#4764
|
|
23
|
+
/ objectui#4782 retired the renderer's object fallback and `DashboardView`'s
|
|
24
|
+
second copy of the prefix convention (enumerated across both repos' corpora:
|
|
25
|
+
zero producers). After that, `os validate` blessed exactly the buttons the
|
|
26
|
+
runtime refuses — the false affordance the rule exists to eliminate — while
|
|
27
|
+
refusing the one shape the runtime serves.
|
|
28
|
+
|
|
29
|
+
**BREAKING** accept-set change on the `os validate` gating tier (landing after
|
|
30
|
+
the v17.0.0 cut; the lockstep launch-window convention ships it as `minor`):
|
|
31
|
+
|
|
32
|
+
- A `modal` header target naming a defined action, a bare object, or a
|
|
33
|
+
`<verb>_<object>` form now **fails** validation. Those buttons already
|
|
34
|
+
dispatch to a named refusal at runtime.
|
|
35
|
+
- A `modal` header target naming a declared page now **passes** — it was
|
|
36
|
+
wrongly refused before.
|
|
37
|
+
|
|
38
|
+
## FROM → TO
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// before — passed validation; the runtime now refuses the click
|
|
42
|
+
header: {
|
|
43
|
+
actions: [{ label: 'New Deal', actionType: 'modal', actionUrl: 'create_opportunity' }],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// after — name a declared page…
|
|
47
|
+
header: {
|
|
48
|
+
actions: [{ label: 'Intake', actionType: 'modal', actionUrl: 'deal_intake' }], // pages: [{ name: 'deal_intake' }]
|
|
49
|
+
}
|
|
50
|
+
// …or, to open an object's form, use the validated first-class shape
|
|
51
|
+
header: {
|
|
52
|
+
actions: [{ label: 'New Deal', actionType: 'form', actionUrl: 'opportunity.edit' }],
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
There is deliberately no automatic rewrite: a retired-shape target is a
|
|
57
|
+
name-shaped guess (`create_opportunity` names the page `create_opportunity`, or
|
|
58
|
+
it names nothing — the ruling explicitly declined keeping the prefix), and only
|
|
59
|
+
the author knows whether the button meant a page or an object form.
|
|
60
|
+
`objectstack migrate meta` surfaces the change as a structured TODO (semantic
|
|
61
|
+
entry `dashboard-header-modal-target-page-only`, protocol major 18).
|
|
62
|
+
|
|
63
|
+
<!-- adr-0087: registered dashboard-header-modal-target-page-only -->
|
|
64
|
+
- 8b9eba5: feat(spec): field-level `relatedListFilter` — a declarative default filter for auto-derived related lists (#8704)
|
|
65
|
+
|
|
66
|
+
<!-- adr-0087: not-required (no-migration-prescription) Pure accept-set
|
|
67
|
+
widening: one new optional key joins the existing field-level related-list
|
|
68
|
+
family. Nothing is renamed, retired, narrowed or tombstoned, so there is no
|
|
69
|
+
conversion to register and no retirement registry entry. -->
|
|
70
|
+
|
|
71
|
+
The field-level related-list family (`relatedList` / `relatedListTitle` /
|
|
72
|
+
`relatedListColumns`) gains its fourth member, `relatedListFilter` — closing the
|
|
73
|
+
gap where the only way to filter an auto-derived related list was to abandon the
|
|
74
|
+
auto-derived record page for a hand-written `record:related_list` page
|
|
75
|
+
(maintainer ruling 2026-08-15 on #8704).
|
|
76
|
+
|
|
77
|
+
- **No new filter dialect**: the key carries the canonical Query-DSL
|
|
78
|
+
`FilterCondition` (the same authoring face as a query `where`, dataset scope
|
|
79
|
+
filters, and `summaryOperations.filter`). The FILTER-axis doors therefore
|
|
80
|
+
apply automatically — the schema door refuses bare date-range preset
|
|
81
|
+
comparands in ordering positions at parse (#8793), and the engine doors judge
|
|
82
|
+
the composed query at run time (`formula` keys refused `INVALID_FIELD`,
|
|
83
|
+
#8296).
|
|
84
|
+
- **Contract semantics, pinned**: the declared constraint is AND-composed with
|
|
85
|
+
the parent-relationship condition `{ [referenceField]: parentId }` — an
|
|
86
|
+
authored constraint, never a user-editable suggestion — and the related-list
|
|
87
|
+
tab badge count honors the same composed filter, so counts match visible
|
|
88
|
+
rows. Both clauses are normative in the key's contract text and pinned by
|
|
89
|
+
tests.
|
|
90
|
+
- **`@objectstack/lint`**: the shared authored-filter walk (`FILTER_KEYS`) now
|
|
91
|
+
recognizes `relatedListFilter`, extending the filter-token, empty-combinator
|
|
92
|
+
and preset-comparand rules to the new position.
|
|
93
|
+
|
|
94
|
+
The consumption half (RecordDetailView auto-derivation + tab badge) is
|
|
95
|
+
objectui#4664, `Blocked-by:` this change; until it lands the key is ledgered
|
|
96
|
+
`planned` with an author warning.
|
|
97
|
+
- a777944: feat(spec,lint): refuse a bare date-range preset name in an ordering filter comparand at publish time (#8793 — the ruled C half of #8690)
|
|
98
|
+
|
|
99
|
+
**BREAKING** accept-set narrowing on a published authoring surface, landing
|
|
100
|
+
after the v17.0.0 cut (the lockstep launch-window convention ships it as
|
|
101
|
+
`minor`; the migration prescription is registered under protocol major 18).
|
|
102
|
+
|
|
103
|
+
`last_7_days` / `last_30_days` / `last_90_days` and their ten calendar
|
|
104
|
+
siblings are real, declared preset names — for the dashboard date-filter
|
|
105
|
+
positions, where the console lowers them to `{date-macro}` bounds before any
|
|
106
|
+
query is sent. Authored as a bare filter comparand nothing resolves them:
|
|
107
|
+
measured on #8690, `$gte "last_30_days"` returned HTTP 200 with 0 of 51 rows
|
|
108
|
+
where `$gte "{30_days_ago}"` returned the 38 in-window. The engine now
|
|
109
|
+
refuses the bare name on a declared temporal field at query time
|
|
110
|
+
(`INVALID_FILTER` / 400, PR #8808 — the B half); this change is the
|
|
111
|
+
authoring-time half the same ruling shipped alongside it.
|
|
112
|
+
|
|
113
|
+
**What is refused — ordering positions only, in all three authored filter
|
|
114
|
+
shapes:** a `$gt` / `$gte` / `$lt` / `$lte` comparand or `$between` endpoint
|
|
115
|
+
on every carrier of `FilterConditionSchema` (dashboard widget filter, dataset
|
|
116
|
+
filter, report `runtimeFilter`, page/component filter, rollup filter), a
|
|
117
|
+
`greater_than` / `less_than` / `before` / `after` / `between` view filter
|
|
118
|
+
rule value, and an ordering `[field, op, value]` filter triple (the latter
|
|
119
|
+
two via `@objectstack/lint`'s new gating rule `filter-preset-comparand`,
|
|
120
|
+
which also runs at the runtime publish gate for `dashboard` / `view` /
|
|
121
|
+
`object` / `page` / `flow` writes). The refusal names the offending value,
|
|
122
|
+
the position, and the exact `{date-macro}` window that works.
|
|
123
|
+
|
|
124
|
+
**What stays accepted:** the preset names in the dashboard date-filter
|
|
125
|
+
positions (`dateRange.defaultRange`, a date global filter's `defaultValue`) —
|
|
126
|
+
the only positions any layer ever resolved them; equality and membership
|
|
127
|
+
comparands (`{ period: 'this_quarter' }`, `$in: [...]`) — a select/picklist
|
|
128
|
+
column legitimately stores colliding values, and the engine's field-typed
|
|
129
|
+
door already covers the temporal case; undeclared strings
|
|
130
|
+
(`'not-a-date-at-all'`) — the field-typed engine door owns those; and the
|
|
131
|
+
empty-string cell, which stays its own card by ruling.
|
|
132
|
+
|
|
133
|
+
## FROM → TO
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// before — parsed green, returned a silent zero (or 400 at query time since #8808)
|
|
137
|
+
filter: { closed_at: { $gte: 'last_30_days' } }
|
|
138
|
+
|
|
139
|
+
// after — rejected naming the window; write the date-macro spelling
|
|
140
|
+
filter: { closed_at: { $gte: '{30_days_ago}' } }
|
|
141
|
+
// calendar presets prescribe their pair:
|
|
142
|
+
filter: { closed_at: { $between: ['{week_start}', '{week_end}'] } }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`DATE_RANGE_PRESETS` moved to `@objectstack/spec/data`
|
|
146
|
+
(`data/date-range-presets.ts`) with `ui` re-exporting it, so both import
|
|
147
|
+
paths keep working; `DATE_RANGE_PRESET_MACRO_WINDOWS` (the per-preset macro
|
|
148
|
+
window table the refusals quote) and `isDateRangePresetName` are new exports.
|
|
149
|
+
|
|
150
|
+
<!-- adr-0087: registered filter-preset-ordering-comparand-refused -->
|
|
151
|
+
- 73cfddf: fix(lint): the ADR-0091 D3 "delegation row needs a reason" rule is scoped to `sys_user_position` (#9730)
|
|
152
|
+
|
|
153
|
+
`delegated_from` was retired from `sys_user_permission_set` (ADR-0049
|
|
154
|
+
enforce-or-remove, maintainer ruling 2026-08-18), so the security-posture
|
|
155
|
+
lint's D3 dual-audit rule no longer reads the key on that table — linting a
|
|
156
|
+
retired column would imply it still exists, and on that table this rule was
|
|
157
|
+
the column's *only* enforcement, which is exactly the advisory-security shape
|
|
158
|
+
the ruling removed. A seed row that still carries the key is refused loudly
|
|
159
|
+
downstream by the engine's schema preflight (`400 INVALID_FIELD`).
|
|
160
|
+
|
|
161
|
+
The D2 rule (a seed grant whose `valid_until` is already past or unparseable
|
|
162
|
+
is dead on arrival) still covers **both** grant tables — `valid_until` remains
|
|
163
|
+
declared and resolution-enforced on both. Only the two rules' object scopes
|
|
164
|
+
diverge; no rule id, severity or message changed.
|
|
165
|
+
- 1408ae3: feat(lint): the five gating object rules cross the runtime publish gate — `object` writes are now judged by `validateFunctionalCompleteness`, `validateManagedApiMethods`, `lintAutonumberFormats`, `validateRuleCompilability` and `validateRuleSchemaFormats` (#4716)
|
|
166
|
+
|
|
167
|
+
An `active`-state `object` save through `saveMetaItem` (Studio's field editor,
|
|
168
|
+
REST `/meta` item CRUD, an MCP/AI author) is now refused with the existing 422
|
|
169
|
+
`invalid_metadata` envelope when it carries a defect these five rules judge:
|
|
170
|
+
an inert `summary`/`lookup`/`select` shape, a managed-API verb the object's own
|
|
171
|
+
affordances refuse, an autonumber format referencing an unknown field, a
|
|
172
|
+
`format` regex or `json_schema` schema the runtime's own compilers reject, or a
|
|
173
|
+
`json_schema` `format` name ajv would silently drop. All five already gated
|
|
174
|
+
`os validate` / `os build` / `os lint`; the runtime door — the only door a
|
|
175
|
+
tenant overlay row has — ran none of them.
|
|
176
|
+
|
|
177
|
+
Scope is deliberately the five **gating** rules only (the #4716 adjudication):
|
|
178
|
+
the six advisory-tier object rules stay off the runtime surface, so a clean
|
|
179
|
+
save's response is byte-identical and no new advisory volume reaches Studio's
|
|
180
|
+
designer. Draft saves are untouched (D1), stored rows keep being served
|
|
181
|
+
(ADR-0087 asymmetry — the gate's differential blames a write only for what it
|
|
182
|
+
adds), and `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still degrades the refusal to
|
|
183
|
+
a loud log for migration windows.
|
|
184
|
+
|
|
185
|
+
Boot-path note: the two schema-judging rules load ajv lazily, only when the
|
|
186
|
+
judged snapshot actually carries a `json_schema` validation — an ordinary
|
|
187
|
+
field edit still loads no compiler, which `runtime-lazy-deps.test.ts` now pins
|
|
188
|
+
as a three-tier contract (parsers never; ajv never without a schema; ajv
|
|
189
|
+
required, on demand, when one is present).
|
|
190
|
+
- 6f5a449: The runtime publish gate judges a package write against that package's own closure (#9612)
|
|
191
|
+
|
|
192
|
+
The gate handed every rule the tenant's **entire** `objects` collection on every
|
|
193
|
+
publish. That is the wrong validation unit, not merely a large one: a tenant
|
|
194
|
+
that has grown to hundreds of objects is many packages, and judging one
|
|
195
|
+
package's write against all of them asks a question nobody wanted answered.
|
|
196
|
+
Per the maintainer's ruling, the unit is now the package —
|
|
197
|
+
「客户开发开发,校验是否也应该基于软件包」·「当然这里面要考虑系统对象」.
|
|
198
|
+
|
|
199
|
+
`buildRuntimeWriteSnapshots` accepts an optional `packageScope`
|
|
200
|
+
(`{ packageId, dependencies }`) and reduces `objects` to that closure:
|
|
201
|
+
|
|
202
|
+
- the package being written;
|
|
203
|
+
- the transitive closure of its **declared** `manifest.dependencies` — a
|
|
204
|
+
package's declared dependencies bound what it may reference, so this is a set
|
|
205
|
+
the platform computes exactly rather than estimates;
|
|
206
|
+
- platform / system objects, **unconditionally** — a package legitimately
|
|
207
|
+
references `sys_*` objects it never declares, and a closure that dropped them
|
|
208
|
+
would report unresolved references that are not there;
|
|
209
|
+
- rows carrying no package provenance (tenant-authored overlays), because
|
|
210
|
+
nothing declares what they may reference and so nothing bounds them.
|
|
211
|
+
|
|
212
|
+
`ObjectStackProtocolImplementation` resolves that scope from the package
|
|
213
|
+
registry and passes it through `evaluateRuntimeAuthoringGate`.
|
|
214
|
+
|
|
215
|
+
**A write that names no package, or names one the registry cannot produce,
|
|
216
|
+
narrows nothing** and is judged exactly as before. That direction is the whole
|
|
217
|
+
design: an unresolvable package buys a write *more* validation input, never
|
|
218
|
+
less. There is no branch that skips rules, and none that skips them past a
|
|
219
|
+
size.
|
|
220
|
+
|
|
221
|
+
One behaviour change follows from the unit being right: a **package-scoped**
|
|
222
|
+
write that references an object in a package it never declared a dependency on
|
|
223
|
+
is now judged against a closure that does not contain it, so the reference is
|
|
224
|
+
reported. That is the ruling's intended consequence — such a reference is not
|
|
225
|
+
resolvable by declaration — and it applies only to writes that state a package.
|
|
226
|
+
|
|
227
|
+
Also exported: `narrowObjectsToPackageClosure` and the `RuntimePackageScope`
|
|
228
|
+
type from `@objectstack/lint` and `@objectstack/lint/runtime`, and
|
|
229
|
+
`isSystemObject` from the security-posture rule module so the closure and the
|
|
230
|
+
rules share one reading of what "system" means rather than two.
|
|
231
|
+
- b849e69: fix(lint): ask the provenance question at the fifth blanket-`SYSTEM_FIELDS` read site — `searchableFields` (#8404)
|
|
232
|
+
|
|
233
|
+
`validate-searchable-fields.ts` judged a declared `searchableFields` entry
|
|
234
|
+
against the object-independent `SYSTEM_FIELDS` union, exactly as the four
|
|
235
|
+
filter/page-binding rules did before #8340 wired them to the per-object index.
|
|
236
|
+
Both of its gates were correct about EXISTENCE and structurally blind to
|
|
237
|
+
PROVENANCE: `:345` keeps `searchable-field-unknown` silent for any name in the
|
|
238
|
+
union, and `resolveAllowedSet` goes further — it manufactures a stub meta for
|
|
239
|
+
such an entry so it survives the resolution's existence filter exactly as it
|
|
240
|
+
does at runtime.
|
|
241
|
+
|
|
242
|
+
On an ADR-0015 `external` object the platform registers its injected anchors
|
|
243
|
+
(`owner_id`, `organization_id`, the audit family, …) and provisions no storage
|
|
244
|
+
behind them (#7865 / #8116), so:
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
searchableFields: ['name', 'owner_id'] // external object
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
linted clean, the stub kept the entry in the resolved allow-list, and the
|
|
251
|
+
view's `$searchFields` narrowing then scanned a column empty on every record —
|
|
252
|
+
#4830's own failure mode (a narrower search than declared, silently) reached by
|
|
253
|
+
a different route.
|
|
254
|
+
|
|
255
|
+
A new `searchable-field-unprovisioned` rule now warns on such an entry, on the
|
|
256
|
+
object's own canonical set and on a list view's narrowing alike, reusing
|
|
257
|
+
`unprovisionedAnchorCause` / `unprovisionedAnchorHint` so the sentence matches
|
|
258
|
+
the four #8340 rules verbatim rather than becoming a second copy (#4830). WARN,
|
|
259
|
+
never gating, per #4330's cost asymmetry: the remote schema is not visible to
|
|
260
|
+
this pass, so the finding describes a degradation rather than a refusal.
|
|
261
|
+
|
|
262
|
+
**The `:239` stub is KEPT.** It is not incidental — it is what makes the linter's
|
|
263
|
+
resolution agree with the runtime's, which resolves the declared branch against
|
|
264
|
+
the registry field map. Measured by disabling it: the existing "keeps runtime
|
|
265
|
+
parity when the object declares system columns searchable" test goes red
|
|
266
|
+
(`expected [] to have a length of 1 but got +0`), because the declaration
|
|
267
|
+
existence-filters to empty and resolution falls through to the auto-default.
|
|
268
|
+
Dropping it would have been a behaviour change dressed as a warning.
|
|
269
|
+
|
|
270
|
+
The warning is emitted per declared entry in the checker's entry loop, never
|
|
271
|
+
inside `resolveAllowedSet` — that helper reads the OBJECT's declaration and runs
|
|
272
|
+
once per narrowing, so warning there would repeat one object-level fact for
|
|
273
|
+
every view and attribute it to the view's path.
|
|
274
|
+
|
|
275
|
+
`checkSearchableFieldList` takes the index as an OPTIONAL trailing parameter,
|
|
276
|
+
the same shape #8340 gave `checkFieldRefs`: its absence means the caller did not
|
|
277
|
+
build the index and the provenance question goes unasked — the previous
|
|
278
|
+
behaviour, preserved for out-of-repo callers (cloud graph-lint, the AI authoring
|
|
279
|
+
path). Both in-repo callers pass it.
|
|
280
|
+
- 71ac21c: feat(lint): a sharing rule anchored where sharing has nothing to widen is now an authoring-time error (#9698)
|
|
281
|
+
|
|
282
|
+
`validateSharingRuleEnforceability` gains its second arm. It already judged a
|
|
283
|
+
sharing rule's `condition` against the compiler that lowers it; it now judges
|
|
284
|
+
the rule's `object` against the verdict that decides whether the grant can
|
|
285
|
+
exist at all.
|
|
286
|
+
|
|
287
|
+
Two new `error` ids, both decidable from authored metadata before anything
|
|
288
|
+
boots, and both mirroring `SharingService.inertGrantReason` (ADR-0111 D7)
|
|
289
|
+
rather than modelling it:
|
|
290
|
+
|
|
291
|
+
- **`sharing-rule-object-not-shareable`** — the anchor object's effective
|
|
292
|
+
sharing model is `public` (an explicit `sharingModel: 'public_read_write'`,
|
|
293
|
+
or no `sharingModel` on a system object, which ADR-0090 D1 resolves to
|
|
294
|
+
public). Sharing only ever WIDENS an OWD baseline, so on the widest baseline
|
|
295
|
+
there is nothing to widen.
|
|
296
|
+
- **`sharing-rule-object-controlled-by-parent`** — the anchor is a
|
|
297
|
+
master-detail detail, whose visibility is derived from its master
|
|
298
|
+
(ADR-0055). It gets its own id and its own fix-it ("share the master
|
|
299
|
+
record instead"), because `effectiveSharingModel` collapses it onto the same
|
|
300
|
+
`public` verdict while the correct repair is completely different.
|
|
301
|
+
|
|
302
|
+
Both were previously accepted by `SharingRuleSchema`, accepted by `defineRule`,
|
|
303
|
+
seeded into `sys_sharing_rule`, and only then refused — once per boot, as a
|
|
304
|
+
WARN line inside the boot diagnostics block. That WARN is not a sufficient
|
|
305
|
+
diagnostic, and the reason is measured rather than argued: a rule whose criteria
|
|
306
|
+
match no seeded row never reaches `grant`, so it never throws and warns nothing
|
|
307
|
+
while being exactly as dead. The WARN is a function of the DATA; the defect is a
|
|
308
|
+
property of the DECLARATION.
|
|
309
|
+
|
|
310
|
+
**Blast radius, measured through `objectstack build` before deciding the
|
|
311
|
+
severity:** 5 sharing rules are declared in this repo. 3 fire, all of them in
|
|
312
|
+
`examples/app-crm` — `share_high_value_opps_with_managers`,
|
|
313
|
+
`share_active_leads_with_manager` and `share_won_deal_activities`, anchored on
|
|
314
|
+
`crm_opportunity`, `crm_lead` and `crm_activity`, every one of them
|
|
315
|
+
`sharingModel: 'public_read_write'`. They have been failing their boot backfill
|
|
316
|
+
on every boot of that app since they were written, and they are removed here
|
|
317
|
+
under ADR-0049 enforce-or-remove — the same call #9237 made for the two
|
|
318
|
+
equivalent rules in `app-showcase`. The other 2 (app-showcase's, both on
|
|
319
|
+
`private` objects) stay silent, which is the direction that had to be proven
|
|
320
|
+
rather than hoped for.
|
|
321
|
+
|
|
322
|
+
The CRM's smoke test used to assert that these rules existed and were of the
|
|
323
|
+
enforced `criteria` type. Both assertions passed while all three rules enforced
|
|
324
|
+
nothing, so the assertion is replaced by the property their greenness hid: no
|
|
325
|
+
declared rule may be anchored where sharing has nothing to widen.
|
|
326
|
+
|
|
327
|
+
Deliberately NOT judged, because they are not decidable from authored metadata:
|
|
328
|
+
the `owner_id` arm (`owner_id` is injected by the schema registry, so asserting
|
|
329
|
+
it would fail every object that correctly does not declare it by hand), the
|
|
330
|
+
`bypassObjects` arm (plugin configuration, not stack metadata), and the
|
|
331
|
+
federated phantom-anchor arm (a provenance test over that same injected column).
|
|
332
|
+
- 192213f: Three write-surface lint rules now ask provenance, not just membership, before exempting a system column (#8663).
|
|
333
|
+
|
|
334
|
+
`validate-hook-body-writes`, `validate-action-body-writes` and `validate-flow-node-writes` share one `IMPLICIT_FIELDS` set, which is object-INDEPENDENT: it answers "could this name be implicitly writable somewhere", never "did the platform provision a column for it on THIS object". On an ADR-0015 `external` object those diverge — the registry injects `owner_id` / `organization_id` / the audit family onto a federated object exactly as onto a local one, but the remote database owns the schema and no column exists behind them.
|
|
335
|
+
|
|
336
|
+
Each rule now emits a new advisory finding on that path instead of staying silent — `hook-body-write-unprovisioned-anchor`, `action-body-write-unprovisioned-anchor`, `flow-node-write-unprovisioned-anchor` — sharing the `unprovisionedAnchorCause` / `unprovisionedAnchorHint` wording the read-axis rules already use. All three are `warning`: the flow-node rule's existence finding still gates at `error`, and its provenance finding deliberately does not, because the claim is about a remote schema this repo cannot see.
|
|
337
|
+
|
|
338
|
+
An author-DECLARED column of the same name is untouched — on a federated object it maps a remote column the author vouches for. `FlowNodeWriteSeverity` widens from `'error'` to `'error' | 'warning'` accordingly.
|
|
339
|
+
- 42d8990: feat(lint): refuse a list-view `sort` that names a formula field, or no field at all, at authoring time (#9257)
|
|
340
|
+
|
|
341
|
+
<!-- adr-0087: not-required (already-registered engine-find-formula-order-by-refused)
|
|
342
|
+
This rule refuses no shape the runtime accepts — it moves an EXISTING refusal
|
|
343
|
+
earlier. `engine-find-formula-order-by-refused` (semantic, protocol 17) already
|
|
344
|
+
registers the condition and carries the identical FROM → TO prescription
|
|
345
|
+
("denormalise the value onto the object — a stored field, written when the
|
|
346
|
+
source changes — and sort by that", with `summary` explicitly unaffected); the
|
|
347
|
+
FROM → TO block below restates that entry's remedy for the list-view position
|
|
348
|
+
rather than prescribing a second, different one. The `sort-field-unknown` half
|
|
349
|
+
is covered by `assertSortFieldsExist` (#6994), a REST ingress refusal already
|
|
350
|
+
shipped. Nothing authorable is renamed, retired or tombstoned, and no
|
|
351
|
+
`sys_metadata` row changes shape, so there is no new conversion to register —
|
|
352
|
+
what changes is only WHEN the author is told. -->
|
|
353
|
+
|
|
354
|
+
**BREAKING** accept-set narrowing on a published authoring surface, shipped as
|
|
355
|
+
`minor` under the same lockstep launch-window convention the sibling
|
|
356
|
+
`filter-preset-comparand` refusal used. Measured against the shipped corpus
|
|
357
|
+
before landing at `error`: **56 reachable `sort` declarations across
|
|
358
|
+
`examples/app-showcase`, `examples/app-crm`, `examples/app-todo` and
|
|
359
|
+
`packages/platform-objects`, 0 violations** — so this narrows the accept set
|
|
360
|
+
without failing any metadata that ships today.
|
|
361
|
+
|
|
362
|
+
The SORT axis had a runtime refusal on both doors and no authoring gate. This
|
|
363
|
+
adds the missing half, which is the exact shape #6674 closed for the SEARCH
|
|
364
|
+
axis one axis over.
|
|
365
|
+
|
|
366
|
+
**What was broken.** `ListViewSchema.sort` is
|
|
367
|
+
`z.union([z.string(), Array<{ field, order }>])`, so the field name is a bare
|
|
368
|
+
string and Zod validates only the shape. A list view authored with
|
|
369
|
+
`sort: 'expected_revenue desc'` — a `formula` field — validated, published, and
|
|
370
|
+
reported valid, then answered `400 INVALID_SORT` on **first load and every
|
|
371
|
+
load**: the declared sort is the view's initial fetch, not an optional
|
|
372
|
+
interaction, so the whole view fails with a status the author cannot connect to
|
|
373
|
+
the declaration. Both runtime doors already refuse it — `assertSortFieldsExist`
|
|
374
|
+
(`@objectstack/metadata-protocol`, #6994) at the REST ingress and
|
|
375
|
+
`assertOrderByIsMaterializable` (`@objectstack/objectql`, #7095) on the engine's
|
|
376
|
+
own boundary — and neither can reach the author.
|
|
377
|
+
|
|
378
|
+
**What is refused**, at `error`, on every list-view sort a stack declares
|
|
379
|
+
(`objects[].listViews.*.sort`, `views[].list.sort`, `views[].listViews.*.sort`):
|
|
380
|
+
|
|
381
|
+
- `sort-field-unknown` — the name resolves to no field on the bound object.
|
|
382
|
+
Judged on the head segment, matching the ingress gate's own rule so the two
|
|
383
|
+
doors cannot disagree about which names are unknown.
|
|
384
|
+
- `sort-field-unsortable` — the name is a real field whose type is **virtual**:
|
|
385
|
+
computed on read, no stored column, nothing for any driver to `ORDER BY`. An
|
|
386
|
+
unrefused sort on one returns `asc` and `desc` in byte-identical order.
|
|
387
|
+
|
|
388
|
+
**What stays accepted, and this is the load-bearing half:** `summary` and
|
|
389
|
+
`autonumber` sorts. Virtuality is judged by `isVirtualSearchField` /
|
|
390
|
+
`SEARCH_VIRTUAL_TYPES` (`@objectstack/spec/data`), pinned to `formula` alone —
|
|
391
|
+
the same spec storage fact the search ingress gate, the engine's search
|
|
392
|
+
resolution and the FILTER axis' dotted-head classifier already read. It is
|
|
393
|
+
deliberately **not** the spec's `COMPUTED_VALUE_TYPES`: that set is the WRITE
|
|
394
|
+
contract ("never client-written") and gating a sort with it would refuse the two
|
|
395
|
+
types that sort correctly — `summary` is a `table.float` the engine maintains,
|
|
396
|
+
`autonumber` a `table.string` the engine assigns. Both directions are pinned by
|
|
397
|
+
test, and the predicate boundary itself is pinned alongside them so the two
|
|
398
|
+
"must not flag" cases cannot quietly stop meaning anything.
|
|
399
|
+
|
|
400
|
+
Registry-injected system columns (`created_at`, `owner_id`, …) are skipped:
|
|
401
|
+
they are real at runtime, never appear in authored `fields`, and `created_at` is
|
|
402
|
+
the single most common ordering in the platform's own list views.
|
|
403
|
+
|
|
404
|
+
## FROM → TO
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
// before — parsed green, published, then 400 INVALID_SORT on every load
|
|
408
|
+
listViews: {
|
|
409
|
+
forecast: { type: 'grid', sort: [{ field: 'expected_revenue', order: 'desc' }] },
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// after — refused at authoring time, naming the field, the position and the fix
|
|
413
|
+
listViews: {
|
|
414
|
+
// denormalise the computed value onto a stored column and sort by that
|
|
415
|
+
forecast: { type: 'grid', sort: [{ field: 'expected_revenue_stored', order: 'desc' }] },
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The rule joins `REFERENCE_INTEGRITY_RULES`, so it runs on `os validate`,
|
|
420
|
+
`os lint` and `os compile` at once rather than being wired per command.
|
|
421
|
+
|
|
422
|
+
### Patch Changes
|
|
423
|
+
|
|
424
|
+
- 34392a1: docs(lint): the `readonlyWhen` field-rule diagnostic no longer cites `ADR-0057 D10` (#9255)
|
|
425
|
+
|
|
426
|
+
The author-visible consequence text for a faulting `readonlyWhen` predicate said
|
|
427
|
+
"Per ADR-0057 D10 the server is the one that decides". The rule it states is
|
|
428
|
+
correct and unchanged — the server locks the field while the form still renders
|
|
429
|
+
it editable — but the citation does not resolve: `D10` of the ERP-authorization
|
|
430
|
+
`ADR-0057` decides Setup-nav capability surfacing, and the other `ADR-0057`
|
|
431
|
+
(system data lifecycle) carries no D-numbered decisions at all. An author who
|
|
432
|
+
followed the anchor landed on an unrelated decision and had no way to tell
|
|
433
|
+
whether the code or their search was wrong.
|
|
434
|
+
|
|
435
|
+
The diagnostic now states the rule on its own authority, which is where it
|
|
436
|
+
always rested. No behaviour, no message semantics and no rule changed — only
|
|
437
|
+
the traceability claim. Recording the rule as an actual decision is tracked
|
|
438
|
+
separately in #9628.
|
|
439
|
+
- 62b1427: fix(lint): drop the `element:filter` entry from `COMPONENT_FIELD_SPECS` (#9220)
|
|
440
|
+
|
|
441
|
+
The whole `element:filter` element retired at element grain (ADR-0049 — no
|
|
442
|
+
renderer ever shipped for it), so every `ElementFilterProps` key is a
|
|
443
|
+
`retiredKey()` tombstone and no spec-conformant page carries `fields` on it.
|
|
444
|
+
The field-binding rule's job (resolve a field NAME against the object) is not
|
|
445
|
+
the question a retired key raises: an authored key is already reported by name
|
|
446
|
+
with the element-retirement prescription through the #5068 props gate, and the
|
|
447
|
+
binding entry would only add a second finding about a key that no longer
|
|
448
|
+
exists — the #5775/#6629 residue class the package's own
|
|
449
|
+
`component-field-specs-liveness` gate refuses.
|
|
450
|
+
- 818c27c: fix(lint): the three ADR-0120 uniqueness rules name the object in the `where` slot instead of repeating the config path (#9600)
|
|
451
|
+
|
|
452
|
+
`AuthoringFinding` declares two location slots with different jobs — `where`
|
|
453
|
+
("human-readable location", e.g. `object "leave_request"`) and `path` ("config
|
|
454
|
+
path", e.g. `objects[3].sharingModel`). Three registry adapters set the first
|
|
455
|
+
from the second (`where: f.path`), so every CLI command printed the same
|
|
456
|
+
positional string twice and the only human-readable slot said nothing the `at`
|
|
457
|
+
clause did not already say:
|
|
458
|
+
|
|
459
|
+
```
|
|
460
|
+
• objects[44].indexes[1]: "sys_account" declares index [provider_id, account_id] with bare `unique: true` …
|
|
461
|
+
rule: unique/unscoped-declared-index at objects[44].indexes[1]
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
That index is a position in the MERGED object array, which appears in no file
|
|
465
|
+
the author wrote. `unique/unscoped-declared-index`, `unique/double-declaration`
|
|
466
|
+
and `unique/legacy-organization-composite` now spell it the way the rest of the
|
|
467
|
+
table does:
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
• object "sys_account" · index [provider_id, account_id]: "sys_account" declares index …
|
|
471
|
+
rule: unique/unscoped-declared-index at objects[44].indexes[1]
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
An index is identified by its `name` when it has one, and otherwise by the
|
|
475
|
+
columns the author actually wrote (`· index [provider_id, account_id]`) — both
|
|
476
|
+
searchable in their source, which a bare ordinal is not.
|
|
477
|
+
|
|
478
|
+
`where` is stated by the rule functions themselves rather than reconstructed in
|
|
479
|
+
the adapter, because only the rule still holds the object it walked. Their
|
|
480
|
+
return type is now `LocatedLintIssue` (a `LintIssue` with a REQUIRED `where`),
|
|
481
|
+
newly exported, so a fourth rule joining this family cannot reach the adapter
|
|
482
|
+
without one — a `f.where ?? f.path` fallback at the adapter would have let the
|
|
483
|
+
positional spelling ship again silently.
|
|
484
|
+
|
|
485
|
+
Display text only, and the rules' population is unchanged: measured over the 45
|
|
486
|
+
object declarations `@objectstack/platform-objects` and
|
|
487
|
+
`@objectstack/metadata-core` ship, the registry produced 1050 findings from the
|
|
488
|
+
same 5 rules before and after, with the count of findings whose `where` was a
|
|
489
|
+
bare config path going 72 to 0. `path` is deliberately untouched and stays
|
|
490
|
+
positional — it is the slot that is supposed to be a config path, and the
|
|
491
|
+
runtime gate's `fingerprint` reads `where` and `path` together, so making
|
|
492
|
+
`where` more specific cannot merge two findings that were distinct.
|
|
493
|
+
- e43b211: fix(spec): the retirement prescriptions state what `os migrate meta` actually does (#9529)
|
|
494
|
+
|
|
495
|
+
Every `retiredKey()` prescription whose surface an ADR-0087 conversion covers
|
|
496
|
+
closed with a maintainer-ruled sentence (2026-08-09, #6856):
|
|
497
|
+
|
|
498
|
+
> Run `os migrate meta --from N` to rewrite existing sources automatically.
|
|
499
|
+
|
|
500
|
+
The command has never rewritten an authored source file. It replays the
|
|
501
|
+
conversion chain over the loaded stack **in memory**, prints the attributed
|
|
502
|
+
mechanical change list (`Applied N mechanical change(s)`, one line per site as
|
|
503
|
+
`path: from → to (conversionId)`), and writes exactly one file — the `--out`
|
|
504
|
+
JSON snapshot, when you ask for it. Every write site in
|
|
505
|
+
`packages/cli/src/commands/migrate/meta.ts` is that snapshot; there is no
|
|
506
|
+
`--write` / `--fix` / in-place flag. So an author who followed the prescription
|
|
507
|
+
got the chain replayed, a printed diff and optionally a JSON document in a shape
|
|
508
|
+
their per-artifact `.ts` modules are not written in — and then still edited every
|
|
509
|
+
file by hand, with nothing in the message saying so.
|
|
510
|
+
|
|
511
|
+
Under the maintainer's ruling of 2026-08-18 the sentence is withdrawn in favour
|
|
512
|
+
of an honest one, class-wide:
|
|
513
|
+
|
|
514
|
+
> Run `os migrate meta --from N` to list the mechanical edits for existing
|
|
515
|
+
> sources; apply them by hand.
|
|
516
|
+
|
|
517
|
+
The partial-value conversions keep their two-clause shape, reworded the same way
|
|
518
|
+
(`… to list the mechanical edits for the \`1y\` case; the other durations are
|
|
519
|
+
reported for you to re-state.`). Behaviour is unchanged in both packages — this
|
|
520
|
+
is message text only, and no accept/reject verdict moves.
|
|
521
|
+
|
|
522
|
+
The claim is withdrawn from every shipped site, not only the canonical sentence:
|
|
523
|
+
the variant phrasings in tombstone and conversion-registry prose ("rewrites
|
|
524
|
+
author sources", "rewrites it for you", "only `os migrate meta` rewrites
|
|
525
|
+
sources") go with it, as do the upgrade-path statements in the hand-written docs
|
|
526
|
+
(`upgrading.mdx` now carries the same "does not rewrite your source files" fact
|
|
527
|
+
the `objectstack-upgrade` skill already told operators). The class-wide pin
|
|
528
|
+
`packages/spec/src/shared/retired-key-migrate-sentence.test.ts` moves in
|
|
529
|
+
lockstep and now holds **both** directions: the new sentence is required where a
|
|
530
|
+
prescription names the command, and the withdrawn claim is a hard failure
|
|
531
|
+
wherever it reappears — including in a prescription that spells the bare command
|
|
532
|
+
without `--from N`, which the sentence-shape check alone would not have seen.
|
|
533
|
+
|
|
534
|
+
The in-place AST codemod that would make the original claim true is commissioned
|
|
535
|
+
separately for v18 (#9591); when it lands, the sentence may be restored by
|
|
536
|
+
editing that one pin in the same PR.
|
|
537
|
+
- Updated dependencies [56656aa]
|
|
538
|
+
- Updated dependencies [07e630e]
|
|
539
|
+
- Updated dependencies [2f65b1b]
|
|
540
|
+
- Updated dependencies [720ee95]
|
|
541
|
+
- Updated dependencies [f287435]
|
|
542
|
+
- Updated dependencies [9aa8890]
|
|
543
|
+
- Updated dependencies [7c9c1dd]
|
|
544
|
+
- Updated dependencies [75b7c24]
|
|
545
|
+
- Updated dependencies [d5552ca]
|
|
546
|
+
- Updated dependencies [d9813a9]
|
|
547
|
+
- Updated dependencies [8640fb2]
|
|
548
|
+
- Updated dependencies [2420641]
|
|
549
|
+
- Updated dependencies [2ad91c3]
|
|
550
|
+
- Updated dependencies [f57fb38]
|
|
551
|
+
- Updated dependencies [00777a0]
|
|
552
|
+
- Updated dependencies [d491625]
|
|
553
|
+
- Updated dependencies [420804d]
|
|
554
|
+
- Updated dependencies [716ac9b]
|
|
555
|
+
- Updated dependencies [62b1427]
|
|
556
|
+
- Updated dependencies [7ea1372]
|
|
557
|
+
- Updated dependencies [23abe27]
|
|
558
|
+
- Updated dependencies [985a9cd]
|
|
559
|
+
- Updated dependencies [a8189ae]
|
|
560
|
+
- Updated dependencies [26e70fb]
|
|
561
|
+
- Updated dependencies [42b05af]
|
|
562
|
+
- Updated dependencies [2b292ce]
|
|
563
|
+
- Updated dependencies [abcf853]
|
|
564
|
+
- Updated dependencies [8b9eba5]
|
|
565
|
+
- Updated dependencies [d575779]
|
|
566
|
+
- Updated dependencies [94f7ef8]
|
|
567
|
+
- Updated dependencies [c5ac5e4]
|
|
568
|
+
- Updated dependencies [a777944]
|
|
569
|
+
- Updated dependencies [dd88e1c]
|
|
570
|
+
- Updated dependencies [856527c]
|
|
571
|
+
- Updated dependencies [870f710]
|
|
572
|
+
- Updated dependencies [79c46da]
|
|
573
|
+
- Updated dependencies [7ff3975]
|
|
574
|
+
- Updated dependencies [29d055b]
|
|
575
|
+
- Updated dependencies [65589d6]
|
|
576
|
+
- Updated dependencies [2c86fe3]
|
|
577
|
+
- Updated dependencies [e196c6a]
|
|
578
|
+
- Updated dependencies [4ab7523]
|
|
579
|
+
- Updated dependencies [19539b4]
|
|
580
|
+
- Updated dependencies [11b779e]
|
|
581
|
+
- Updated dependencies [739fe5b]
|
|
582
|
+
- Updated dependencies [4bfe1a5]
|
|
583
|
+
- Updated dependencies [2065e31]
|
|
584
|
+
- Updated dependencies [b69d0f5]
|
|
585
|
+
- Updated dependencies [4d47afe]
|
|
586
|
+
- Updated dependencies [e4e5c6e]
|
|
587
|
+
- Updated dependencies [9a56784]
|
|
588
|
+
- Updated dependencies [d00d2f6]
|
|
589
|
+
- Updated dependencies [df0c12d]
|
|
590
|
+
- Updated dependencies [d31785f]
|
|
591
|
+
- Updated dependencies [c308a4f]
|
|
592
|
+
- Updated dependencies [e2899f6]
|
|
593
|
+
- Updated dependencies [3851f87]
|
|
594
|
+
- Updated dependencies [2a29caa]
|
|
595
|
+
- Updated dependencies [09a6eee]
|
|
596
|
+
- Updated dependencies [1a7f907]
|
|
597
|
+
- Updated dependencies [cd455c8]
|
|
598
|
+
- Updated dependencies [30d3752]
|
|
599
|
+
- Updated dependencies [c80e7ae]
|
|
600
|
+
- Updated dependencies [09a9a8a]
|
|
601
|
+
- Updated dependencies [07026cf]
|
|
602
|
+
- Updated dependencies [5d4f3d5]
|
|
603
|
+
- Updated dependencies [4d80e8b]
|
|
604
|
+
- Updated dependencies [30b1c63]
|
|
605
|
+
- Updated dependencies [079b457]
|
|
606
|
+
- Updated dependencies [e43b211]
|
|
607
|
+
- Updated dependencies [890b38f]
|
|
608
|
+
- Updated dependencies [8bee54b]
|
|
609
|
+
- Updated dependencies [7a537ce]
|
|
610
|
+
- Updated dependencies [593c4bf]
|
|
611
|
+
- Updated dependencies [ff08691]
|
|
612
|
+
- Updated dependencies [60e0f90]
|
|
613
|
+
- Updated dependencies [90c5285]
|
|
614
|
+
- Updated dependencies [7901b2d]
|
|
615
|
+
- Updated dependencies [56bca91]
|
|
616
|
+
- Updated dependencies [79394d7]
|
|
617
|
+
- Updated dependencies [730fd9a]
|
|
618
|
+
- Updated dependencies [44bc51d]
|
|
619
|
+
- Updated dependencies [73cfddf]
|
|
620
|
+
- Updated dependencies [d634e66]
|
|
621
|
+
- @objectstack/spec@17.1.0
|
|
622
|
+
- @objectstack/formula@17.1.0
|
|
623
|
+
- @objectstack/sdui-parser@17.1.0
|
|
624
|
+
|
|
3
625
|
## 17.0.0
|
|
4
626
|
|
|
5
627
|
### Minor Changes
|