Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/20596-service-automation-provenance-anchors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@objectstack/service-automation': patch
---

Provenance comments in `service-automation` were re-anchored

Comment and docblock lines under `src/` that cited tracker numbers which no
longer resolve on GitHub now cite the record in this repository that decided
the matter (an ADR where one exists, otherwise the commit in this repository's
history), and say in their own words what was decided. Comments only: no type,
schema, export, log or refusal text, or runtime behaviour changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// failures that child CONTAINED, and the fold `failed = Σ nodes[].failures`
// therefore answers "what did this run cause", subflows included. Before it, a
// parent whose child lost a row read `failed: 0` while `acted` had rolled up
// all along — the misreading the run-level count exists to prevent (#13681),
// all along — the misreading the run-level count exists to prevent (commit 18d816a50),
// one level up.
//
// The measured target these tests drive is the card's, from #15617:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// #14456 — the visibility half of the ruled containment contract (#13681).
// #14456 — the visibility half of the ruled containment contract (commit 18d816a50).
//
// `loop { body: [ try_catch { try, catch } ] }` is the containment spelling for
// a per-iteration failure that must not end the sweep (maintainer ruling
// 2026-08-31, branch B; there is deliberately no `loop.config.onIterationError`
// key). Containment already worked. What did not exist was any way to SEE what
// it contained: the measurement these tests reproduce (#13681) found a run that
// it contained: the measurement behind commit 18d816a50, reproduced here, found a run that
// lost one row out of five reporting
//
// status=completed selected=5 acted=9 skipped=0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #14419 — `create_record` used to collapse EVERY `data.insert()` failure into
* Commit c5a7448d5 — `create_record` used to collapse EVERY `data.insert()` failure into
* one opaque string (`create_record(OBJECT_NAME) failed: MESSAGE_TEXT`), so a
* flow's only two error-handling primitives — `try_catch` and a `fault` edge
* — could not tell "the row is already there" (`engine.insert`'s own
Expand Down Expand Up @@ -257,7 +257,7 @@ describe('#14419 (PR #14948 patch round 1) — a stale $error.code must not leak
* The tier contract review's reproduction. `try_catch`'s catch region reads
* `code` off the run-wide `$error` (necessarily — see the second describe
* block above), so a stale `$error` becomes a store failure misread as a
* duplicate and SWALLOWED — a different door than #14419's original bug,
* duplicate and SWALLOWED — a different door than the bug commit c5a7448d5 fixed,
* the exact same failure mode, and the very thing fence 4 of the ruling of
* record exists to rule out.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ export function registerCrudNodes(engine: AutomationEngine, ctx: PluginContext):
metrics: { acted: 1 },
};
} catch (err) {
// #14419 — `engine.insert` (#14095) raises `DuplicateRecordError`
// Commit c5a7448d5 — `engine.insert` (#14095) raises `DuplicateRecordError`
// for a unique-constraint violation, carrying the ADR-0112
// `code: 'DUPLICATE_RECORD'` this executor used to throw away by
// folding every failure into one opaque string. Surfacing it here
Expand All @@ -436,9 +436,9 @@ export function registerCrudNodes(engine: AutomationEngine, ctx: PluginContext):
// dependency here via `@objectstack/spec`).
//
// Deliberately narrow to THIS verb: `update_record` / `delete_record`
// collapse identically, but `engine.update` still leaks the raw
// driver error (#14390, not yet fixed) — those node results have
// nothing structured to surface yet, so they are untouched here.
// collapse identically; `engine.update` gained the same `DUPLICATE_RECORD`
// envelope for a unique violation (commit 9d7f7259f), but those node results
// are untouched here — this repair was scoped to `create_record` alone.
const rawCode =
err && typeof err === 'object' && 'code' in err
? (err as { code?: unknown }).code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ export function registerNotifyNode(engine: AutomationEngine, ctx: PluginContext)
// Waiting for the real outcome is not on the table: a notify
// node must not block a flow on a downstream channel.
//
// ── `selected`: what makes a ZERO dispatch readable (#17123) ──
// ── `selected`: what makes a ZERO dispatch readable (commit ae6dcf6a4) ──
//
// `acted` and `unmeasuredEffect` above answer "what did this
// node cause". Neither can answer "this node tried to notify
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #17123 — a `notify` node that reached NOBODY must not read like a run that
* The card behind commit ae6dcf6a4: a `notify` node that reached NOBODY must not read like a run that
* had nobody to reach.
*
* ## What was measured, and why a green suite proved nothing
Expand Down Expand Up @@ -63,15 +63,15 @@
* context. A test that invented its own two context shapes could agree with
* itself while disagreeing with both doors.
*
* ## Why #16659 landing does not close this, stated as a measurement
* ## Why commit ecdfc9411 does not close this, stated as a measurement
*
* #16659 makes a scheduled flow carry its organization. That stops ONE cause
* Commit ecdfc9411 makes a scheduled flow carry its organization. That stops ONE cause
* of a zero delivery; it does not make a zero delivery visible. The schedule
* arm here is therefore driven in BOTH shapes:
*
* - `cronTickToday()` — no organization, the shape production builds now;
* - `cronTickWithOrg()` — carrying the PLATFORM organization, the shape a
* scheduled flow has once #16659 lands.
* scheduled flow takes under commit ecdfc9411.
*
* On a multi-organization install the platform organization is not where the
* recipients live, so the org-scoped `role:` expansion
Expand Down Expand Up @@ -142,7 +142,7 @@ function cronTickToday(): AutomationContext {
}

/**
* `type: 'schedule'` as it fires once #16659 lands: the same context, now
* `type: 'schedule'` as it fires under commit ecdfc9411: the same context, now
* carrying the organization the scheduled flow belongs to. On a
* multi-organization install that is the platform organization, not the
* employer one the recipients live in.
Expand Down Expand Up @@ -411,7 +411,7 @@ describe.each(DRIVERS)('#17123 zero-delivery is distinguishable [driver=%s]', (k
it('DIFFERENTIAL CONTROL: the two trigger families no longer render the same run', async () => {
stack = await boot(kind);

// Family 1 — the cron tick, in the shape #16659 gives it. The platform
// Family 1 — the cron tick, in the shape commit ecdfc9411 gives it. The platform
// organization has no admin members, so the org-scoped `role:` expansion
// resolves to nobody and `emit()` returns delivered 0 / enqueued 0.
const scheduled = await stack.engine.execute('nudge', cronTickWithOrg());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/**
* A signal-less `resume(runId)` is held to the suspended screen's declared
* field contract exactly like a signal-carrying one (#13648).
* field contract exactly like a signal-carrying one (commit 7307191db).
*
* `refuseInvalidScreenInput` (#4477) used to open with `if (!signal) return
* null;` — so `resume(runId, { variables: {} })` was refused with
Expand Down Expand Up @@ -301,7 +301,7 @@ describe("engine-built continuation stays exempt — the flag is the ONLY exempt

const degraded = records.filter((r) => r.message.includes(DEGRADED_SENTENCE));
expect(degraded).toHaveLength(1);
// The level does NOT move (#13398-class): this branch keeps `warn`.
// The level does NOT move (the published-sink ruling, commit e238c79f0): this branch keeps `warn`.
expect(degraded[0].level).toBe('warn');
expect(degraded[0].message).toContain(child.runId);
expect(degraded[0].message).toContain(parentRunId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ describe('subflow delegation: a child REFUSAL is answered as a refusal (#14379)'
});

it('refuses the signal-less gesture the same way, both pauses intact', async () => {
// #13648 normalises an absent signal to `{}` at the public door, so
// Commit 7307191db normalises an absent signal to `{}` at the public door, so
// `resume(parentRunId)` lands on this same delegation path.
const [parentRunId, childRunId] = await startPair();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function registerSubflowNode(engine: AutomationEngine, ctx: PluginContext
// live in the child's log, so until this slot existed the parent's fold
// could not see them and a parent whose child lost a row read
// `failed: 0` — the misreading the run-level `failed` was added to
// prevent (#13681), one level up. Rolled up here, it folds into this
// prevent (commit 18d816a50), one level up. Rolled up here, it folds into this
// node's `failures` and so into the run-level `failed`.
//
// Deliberately the SAME exit the three totals above already leave by, so
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #11060 — the flow VALUE-expression function table, both halves of the ruling:
* Commit 815585513 — the flow VALUE-expression function table, both halves of the ruling:
*
* 1. `round` / `floor` / `ceil` / `abs` / `min` / `max` work in value
* expressions, every name and semantic mirrored **1:1 from the CEL
Expand Down
16 changes: 8 additions & 8 deletions packages/services/service-automation/src/builtin/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
* {round(x)} {floor(x)} {ceil(x)}
* {abs(x)} {min(a, b)} {max(a, b)}
* → the CEL stdlib's numeric six, names and
* semantics mirrored 1:1 (#11060 — see
* semantics mirrored 1:1 (commit 815585513 — see
* KNOWN_EXPRESSION_FUNCTIONS below)
*
* Anything that fails to resolve becomes the literal `null` value (for
* single-token templates) or the empty string (for embedded substitution),
* matching the behavior of common low-code formula engines — with ONE loud
* exception (#11060): an identifier in CALL position (`name(…)`) that is not a
* exception (commit 815585513): an identifier in CALL position (`name(…)`) that is not a
* supported function throws {@link FlowExpressionFunctionError} instead of
* being rewritten to `null`. Before that diagnostic, `ROUND(…)` /
* `Math.round(…)` / `(x).toFixed(2)` all compiled to `null(…)`, the TypeError
Expand All @@ -42,11 +42,11 @@ import { markGuardRefusal } from '../guard-refusal.js';
export type VariableMap = Map<string, unknown>;

/**
* A function-shaped defect in a flow VALUE expression (#11060) — an unknown
* A function-shaped defect in a flow VALUE expression (commit 815585513) — an unknown
* name in call position, a supported name called at the wrong arity, or an
* argument outside the function's domain.
*
* This is the LOUD half of the #11060 ruling: the silent-`null` rewrite of
* This is the LOUD half of the ruling commit 815585513 records: the silent-`null` rewrite of
* unknown identifiers hid every one of these as an `undefined` field write.
* The error is a guard refusal (#3863) — the metadata (the authored
* expression) is wrong, re-running the flow unchanged can never succeed, and
Expand All @@ -68,7 +68,7 @@ export class FlowExpressionFunctionError extends Error {
}

/**
* The value-expression function table (#11060) — maintainer ruling 2026-08-23:
* The value-expression function table (commit 815585513) — maintainer ruling 2026-08-23:
* exactly `round` / `floor` / `ceil` / `abs` / `min` / `max`, every name and
* semantic mirrored **1:1 from the CEL stdlib** (`@objectstack/formula`
* `src/stdlib.ts`, "Numbers" block), ⛔ no second semantics invented. A parity
Expand Down Expand Up @@ -113,7 +113,7 @@ function requireArity(fn: string, args: unknown[]): void {
if (args.length !== want) {
// cel-js refuses the same call with "no matching overload" — same
// outcome, message written for self-correction (ADR-0032 §1d). The
// `round(x, 2)` precision form is THE anticipated misuse (#11060), so
// `round(x, 2)` precision form is THE anticipated misuse (commit 815585513), so
// its refusal carries the supported spelling.
const precisionHint = fn === 'round' && args.length === 2
? ' There is no precision form — the CEL stdlib\'s round() is integer-only; for N-decimal rounding write round(x * 100) / 100.0 (scale 2). Keep the decimal point: in CEL round() returns an int and int / int is integer division, so / 100 drops the decimals there, while / 100.0 is right in both dialects.'
Expand Down Expand Up @@ -281,7 +281,7 @@ function resolveToken(token: string, variables: VariableMap, context: Automation
// Don't substitute reserved literals
if (match === 'true' || match === 'false' || match === 'null' || match === 'undefined') return match;
// CALL position (`name(…)`) resolves against the function table, never
// against flow variables (#11060). A known name stays literal — it is
// against flow variables (commit 815585513). A known name stays literal — it is
// bound as a Function parameter below. An unknown one is the loud half
// of the ruling: refuse with a named error instead of the old `null`
// rewrite, whose swallowed TypeError wrote the field as `undefined`.
Expand All @@ -307,7 +307,7 @@ function resolveToken(token: string, variables: VariableMap, context: Automation
} catch (err) {
// The named diagnostics (arity / domain, thrown inside a table
// function) must escape — swallowing them here would re-create the
// exact silence #11060 removes. Everything else (junk syntax after
// exact silence commit 815585513 removed. Everything else (junk syntax after
// substitution) keeps the documented fail-soft contract.
if (err instanceof FlowExpressionFunctionError) throw err;
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { attachPartialSteps } from '../partial-steps.js';
* `retry` policy re-runs the `try` region with exponential backoff. If the
* region still fails after retries, the optional `catch` region runs with the
* caught error bound to `errorVariable` (default `$error`) — `{ nodeId,
* message }`, plus `code` (#14419) when the failing node's own result set a
* message }`, plus `code` (commit c5a7448d5) when the failing node's own result set a
* platform-classified one (e.g. `create_record`'s `DUPLICATE_RECORD`), so the
* catch region can branch on `{$error.code}` instead of only ever seeing a
* string, plus `iteration` and `item` (#14456) when the container is running
Expand Down Expand Up @@ -137,7 +137,7 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
// container still reports `success` when it recovers. Only the record of
// what happened changes.
let lastError = 'unknown error';
// #14419 — the classified `code` (ADR-0112 `StandardErrorCode`, e.g.
// Commit c5a7448d5 — the classified `code` (ADR-0112 `StandardErrorCode`, e.g.
// `DUPLICATE_RECORD`) of whichever node inside the try region last
// failed, when that node's executor set one. Captured from `$error`
// (below) rather than from the caught exception itself: a node that
Expand All @@ -148,7 +148,7 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
// either the next retry attempt or this executor's own `errorVariable`
// write (below) can shadow it. Without this, a `try_catch`'s catch
// region could never distinguish "the row is already there" from any
// other failure — the whole point of #14419.
// other failure — the whole point of commit c5a7448d5.
let lastErrorCode: string | undefined;
const failedAttemptSteps: StepLogEntry[] = [];
for (let attempt = 0; attempt <= maxRetries; attempt++) {
Expand Down Expand Up @@ -257,7 +257,7 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
// WHICH thing, and `message` names one only when it happens to echo
// the template.
//
// `code` (#14419) is bound alongside and IS declared on
// `code` (commit c5a7448d5) is bound alongside and IS declared on
// `TryCatchErrorValueSchema` — an open, optional `string` whose
// docblock in `packages/spec/src/automation/control-flow.zod.ts`
// states why the type stays open rather than closing over
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ describe('#15788 — one interpolator, not a second template engine', () => {
'first={record.tags.0}',
// Context token, resolved from `AutomationContext`, not from variables.
'by {$User.Id}',
// The CEL-mirrored numeric stdlib (#11060) — nothing a naive
// The CEL-mirrored numeric stdlib (commit 815585513) — nothing a naive
// substitution implements.
'score {round(record.score)}',
// Unresolvable embedded token renders as the empty string, not the
Expand Down
2 changes: 1 addition & 1 deletion packages/services/service-automation/src/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1480,7 +1480,7 @@ describe('AutomationEngine - Execution History', () => {
* assert the opposite — that unregistering a flow FORGOT it had been
* switched off, so re-registering it came back enabled. That was a
* faithful pin of the retired `flowEnabled` map: an in-process bit with
* no durable home, which is exactly the mechanism #10243 measured
* no durable home, which is exactly the mechanism commit 02b41232d measured
* leaking and ADR-0126 §7.2 retires.
*
* Under the activation ledger the answer inverts, and it is ADR-0126 §6
Expand Down
Loading
Loading