@objectstack/lint@16.0.0
Minor Changes
-
3a18b60: feat(approvals): rename the
roleapprover type toorg_membership_level(#3133)ApproverType.rolewas the last platform surface projecting the reserved word
"role" (ADR-0090 D3). It is not covered by D3's better-auth exception: that
exception protects better-auth's ownsys_member.rolecolumn, which we do
not own —ApproverTypeis our own enum, an authoring surface, and D3 mandates
that the projection of that concept is spelledorg_membership_leveland
labelled "organization membership", never "role".The sentence licensing the leak was also false: ADR-0090 D3 claims
sys_member.roleis "already relabelledorg_membership_levelin the platform
projection", butorg_membership_levelexisted nowhere in the codebase and
ADR-0057 D7 lists that relabel under "Deferred (evidence-gated, P4)". The
projection never landed, so the word reached authors.The name manufactured a real, silent failure — "hotcrm class": every other
surface renamed toposition(sys_role,ShareRecipientType.role,
ctx.roles[]), so{ type: 'role', value: 'sales_manager' }reads as the
legacy spelling of a position. It resolves against the membership tier, finds
no member row, falls back to an inertrole:sales_managerliteral, and the
request waits forever on an approver that cannot exist.- spec:
ApproverTypegainsorg_membership_level;rolestays as a
deprecated alias for one window (a published 15.x flow keeps loading) with
DEPRECATED_APPROVER_TYPES+canonicalApproverType()as the single source
for the mapping. Removed in the next major. - plugin-approvals: resolves on the canonical type and warns on the
deprecated spelling. Thetype:valuefallback literal keeps the authored
spelling — storedsys_approval_approverrows andpending_approversslots
from 15.x carryrole:<v>, and rewriting it would orphan them. - lint:
approval-role-not-membership-tier→approval-approver-not-membership-tier
(the rule id carried the reserved word too), plus a new
approval-approver-type-deprecated. The two are mutually exclusive: a bad
value wins, because prescribingorg_membership_levelfor a position name
would be wrong advice — the fix there isposition.
Authoring
type: 'role'keeps working and now says so out loud. Rewrite it as
org_membership_level; if the value is an org position, the fix isposition. - spec:
-
2ea08ee: Flow trigger observability — kill the four-layer silence around record-change flows that never fire (2026-07-17 third-party eval).
A misauthored auto-launched flow (wrong
objectName, missingrequires: ['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:- 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 (requiresmissing), 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. - 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).
RecordChangeTriggerprobes object existence at bind time and warns when a flow'sobjectNamematches no registered object (exact-name matching), instead of silently arming a hook that can never fire.kernel:bootstrappedbinding audit in the automation plugin: warns per enabled-but-unbound triggered flow with the reason, and reports registered/bound/draft counts (AutomationEngine.getTriggerBindingAudit(), extendedgetFlowRuntimeStates()withstatus/triggerType/object).os validateflow-wiring advisories (@objectstack/lintvalidateFlowTriggerReadiness): warns when a record-triggered flow targets an object the stack does not define, and when an auto-triggered flow's status isdraft(authored or defaulted — draft flows still fire; declareactiveorobsolete).- Removed leftover boot-debug writes (
registerApp/AppPlugin/StandaloneStack/AuditPluginstderr noise) that previous debugging of this same silence had left behind.
- Startup banner
-
ea32ec7: feat(formula,lint): advisory type-soundness warnings for formula/predicate expressions (#1928 tier 4)
Closes the last open guardrail from #1928. A
Field.formulaor record-scoped
predicate that uses a text or boolean field with an arithmetic (+ - * / %)
or ordering (< > <= >=) operator against a number faults the runtime
overload and silently evaluates tonull(e.g.record.title * 2,
record.is_active + 1). The build now surfaces this as a non-blocking
warning with the offending field and a corrective message.Honours the ADR-0032 design law — the checker only flags what the runtime
would also fail:- Number / currency / percent / date / datetime fields are declared
dyn, so
the cases the runtime rescues never warn —record.amount / 100(the #1930
registerOperatorfix),record.due == today()and numeric-string / ISO-date
values (the string-hydration retry), and numeric-codedselectoption values. - Equality (
==/!=) is excluded: a heterogeneous equality is runtime-safe
(evaluates tofalse), never a fault.
New
firstTypeMismatch(source, fieldCelTypes, scope)export in
@objectstack/formula(and an optionalfieldTypeshint on
validateExpression);@objectstack/lint'svalidateStackExpressionsthreads
each object's field types into every checked site:- record-scoped sites (
record.<field>) — formula fields, validation rules,
action / hook / sharing predicates; - flattened flow / automation conditions (bare
field) — where flow
variables staydynand are never flagged, and equality stays runtime-safe.
Warnings are advisory in
objectstack build/validate(fatal only under
--strict), matching the tier-3 channel. - Number / currency / percent / date / datetime fields are declared
-
a2795f6: feat(triggers): declarative time-relative trigger — daily sweep instead of fragile date-equality (#1874)
Time-relative business rules ("alert 60 days before a contract's
end_date")
could only be expressed as arecord_changeflow gated on a date-equality
condition likeend_date == daysFromNow(60). That predicate is only evaluated
when the record happens to change, so it fires only if a record is edited on
exactly the threshold day — i.e. almost never, unattended. The robust
alternative was a hand-written cron + range query that every author
re-implemented (contractsrenewal_alert, hrdocument_expiring_soon,
procurementpo_overdue, …).A flow's start node can now declare a
timeRelativedescriptor instead:config: { timeRelative: { object: 'contracts', dateField: 'end_date', offsetDays: [60, 30, 7], // T-minus reminders — fires on each threshold day // — or — withinDays: 30 // "expiring soon" range; negative = overdue lookback filter: { status: 'active' }, // optional, ANDed with the date window }, schedule: { type: 'cron', expression: '0 8 * * *' }, // optional; defaults to daily 08:00 UTC }
The new
time_relativetrigger (shipped in@objectstack/trigger-scheduleas
TimeRelativeTriggerPlugin) sweeps the object on that schedule and launches the
flow once per matching record, with the record on the automation context —
so the start-nodeconditiongate and{record.<field>}interpolation work
exactly as for a record-change flow. Because the window is evaluated every day,
a threshold is never missed regardless of when the record last changed. The
discovery query runs as a system operation (RLS-bypassing) and is capped
(maxRecords, default 1000) so a mis-scoped window can't fan out unboundedly;
per-record failures are isolated so one bad row never aborts the sweep.The automation engine routes a start node carrying
config.timeRelativeto the
time_relativetrigger (ahead of the plainscheduletrigger, whose behavior is
unchanged), andos validategains readiness checks for the new descriptor
(unknown swept object, ambiguous draft status). New authorable spec key:
TimeRelativeTriggerSchema(@objectstack/spec/automation).
Patch Changes
-
524696a: feat(spec)!:
DashboardWidgetSchema.strict()— reject undeclared widget keys (framework#3251)The ADR-0021 analytics endpoint.
DashboardWidgetSchemanow rejects any
undeclared top-level key instead of silently stripping it, moving a whole class
of author error (a hallucinated or legacy key that renders as a silent no-op)
from fallible human review to deterministic CI.options: z.unknown()remains
the escape hatch for renderer-specific extras.A custom error map names the offending key(s) and, when a key is a removed
pre-ADR-0021 inline-analytics key (object/categoryField/valueField/
aggregate, pivotrowField/columnField) or an objectui-internal prop
(component, inlinedata), points the author at the dataset shape
(dataset+dimensions+values).Recorded as protocol-16 migration
step16
(dashboard-widget-strict-unknown-keys), mirroring protocol-15'sstep15
strict flip on the form/page schemas (ADR-0089 D3a). The inline-analytics shape
itself was already removed at protocol 9 (single-form cutover), so there is no
mechanical rewrite — the residue is the strictness, delegated to the author.Breaking: shipped as
minorper the launch-window policy (a breaking change
does not burn a major while the stack is in lockstep), riding the already-pending
16.0.0 train. The release train's Version-Packages PR must set
PROTOCOL_VERSION = '16.0.0'; until thenstep16is inert
(composeMigrationChaincaps atPROTOCOL_MAJOR).@objectstack/lint— thewidget-legacy-analytics-shape/
widget-legacy-analytics-unrenderablerules are retained as the friendly,
suppressible bridge on the raw-config lint/doctor paths (strict preempts them on
the schema-parsed compile/validate paths); doc comment updated to explain the
interplay. -
8923843: Reject view containers that define no views. A flat list-view object (
{ name, label, type, columns, ... }) parses to an emptyViewSchemacontainer because Zod strips unknown keys — zero views register and the Console silently renders nothing.defineView()now throws on a zero-view container, andos validategains aview-container-shapecheck (validateViewContainersin@objectstack/lint) that reports flat or emptyviews: []entries pre-parse with a wrap-it fix hint. -
Updated dependencies [f972574]
-
Updated dependencies [6289ec3]
-
Updated dependencies [22013aa]
-
Updated dependencies [3ad3dd5]
-
Updated dependencies [8efa395]
-
Updated dependencies [3a18b60]
-
Updated dependencies [a8aa34c]
-
Updated dependencies [a3823b2]
-
Updated dependencies [43a3efb]
-
Updated dependencies [524696a]
-
Updated dependencies [6b51346]
-
Updated dependencies [80273c8]
-
Updated dependencies [bfa3c3f]
-
Updated dependencies [5e3301d]
-
Updated dependencies [46e876c]
-
Updated dependencies [7125007]
-
Updated dependencies [158aa14]
-
Updated dependencies [62a2117]
-
Updated dependencies [d2723e2]
-
Updated dependencies [fefcd54]
-
Updated dependencies [beaf2de]
-
Updated dependencies [369eb6e]
-
Updated dependencies [06ff734]
-
Updated dependencies [b659111]
-
Updated dependencies [5754a23]
-
Updated dependencies [6c270a6]
-
Updated dependencies [668dd17]
-
Updated dependencies [8abf133]
-
Updated dependencies [e0859b1]
-
Updated dependencies [04ecd4e]
-
Updated dependencies [4d5a892]
-
Updated dependencies [16cebeb]
-
Updated dependencies [86d30af]
-
Updated dependencies [8923843]
-
Updated dependencies [ea32ec7]
-
Updated dependencies [a2795f6]
-
Updated dependencies [f16b492]
-
Updated dependencies [4b6fde8]
-
Updated dependencies [2018df9]
-
Updated dependencies [fc5a3a2]
-
Updated dependencies [8ff9210]
- @objectstack/spec@16.0.0
- @objectstack/formula@16.0.0
- @objectstack/sdui-parser@16.0.0