From bc0dd4845624790a495f8d8c7fd917bc67401ebb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 18:34:14 +0000 Subject: [PATCH] fix(spec): name timeRelative.object in the time-relative arm of the flow type-mismatch upgrade entry The ADR-0087 semantic entry flow-record-read-and-decision-type-mismatch-refused said its refusal covers a time-relative flow sweeping the start node's objectName. The author-time gate types a time-relative flow's conditions against config.timeRelative.object when the start names no objectName, and against the start's objectName when that is the swept object; the surface and the matching reason sentence now say so. A migrations pin asserts the surface names config.timeRelative.object. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P --- .changeset/30279-time-relative-entry-surface.md | 10 ++++++++++ ...cord-read-and-decision-type-mismatch-refused.ts | 12 +++++++++--- packages/spec/src/migrations/migrations.test.ts | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 .changeset/30279-time-relative-entry-surface.md diff --git a/.changeset/30279-time-relative-entry-surface.md b/.changeset/30279-time-relative-entry-surface.md new file mode 100644 index 00000000000..f0284eabbc6 --- /dev/null +++ b/.changeset/30279-time-relative-entry-surface.md @@ -0,0 +1,10 @@ +--- +'@objectstack/spec': patch +--- + +fix(spec): the `flow-record-read-and-decision-type-mismatch-refused` upgrade-guide entry names the object a time-relative flow's conditions are typed against: the swept `config.timeRelative.object`, when the start node names no `objectName`, and the start node's `objectName` when that is the swept object (#30279) + +Clause-②: no +Metadata-protocol: none + +Text only: the entry's `surface` and the matching sentence of its `reason`, which `os migrate meta` and the upgrade guide show, said the refusal covers a time-relative flow "sweeping the start node's objectName". The author-time gate also types such a flow against `config.timeRelative.object` when its start names no `objectName`, and refuses the same conditions there; the entry now says so. Its id, kind, replacement and acceptance criteria are unchanged, and no verdict moves. diff --git a/packages/spec/src/migrations/entries/semantic/18.flow-record-read-and-decision-type-mismatch-refused.ts b/packages/spec/src/migrations/entries/semantic/18.flow-record-read-and-decision-type-mismatch-refused.ts index 473046098e1..f0920514840 100644 --- a/packages/spec/src/migrations/entries/semantic/18.flow-record-read-and-decision-type-mismatch-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.flow-record-read-and-decision-type-mismatch-refused.ts @@ -10,7 +10,9 @@ export const entry: SemanticMigration = { + 'config.condition, edges[].condition or a decision branch, where FIELD is a text or boolean field of ' + 'the trigger object ordered or arithmetic\'d against a number (record.name > 5, title > 5 in a ' + 'decision branch, record.active > 0); and the same conditions, bare name included, of a time-relative ' - + 'flow (start config.timeRelative) sweeping the start node\'s objectName', + + 'flow (start config.timeRelative) sweeping config.timeRelative.object, typed against that object when ' + + 'the start node names no objectName and against the start node\'s objectName when that is the swept ' + + 'object', replacement: 'Compare like with like. size(record.name) > 5 for the text\'s length; a number field ' + '(record.score > 5) for a value; double(record.name) > 5 only when every value is numeric text, ' @@ -23,8 +25,12 @@ export const entry: SemanticMigration = { + 'run fails at that condition while the record-change trigger isolates the write that fired it. The ' + 'publish gate refused the bare name in a start or edge condition already, but passed the record.FIELD ' + 'read and every decision branch with no verdict at all. It now refuses them with the same run-fails ' - + 'wording. A time-relative sweep launches each run with the swept row as its record, the same total ' - + 'row of the same object, so its conditions get the same refusal where 18.3.0 warned. A bare name a ' + + 'wording. A time-relative sweep launches each run with a row of the object it sweeps ' + + '(config.timeRelative.object) as its record, the same total row of one object, so its conditions are ' + + 'typed against that object when the start node names no objectName, and against the start node\'s ' + + 'objectName when that is the swept object, and get the same refusal: where 18.3.0 warned on a sweep of ' + + 'the start node\'s objectName, and where a start that names no objectName got no verdict at all. ' + + 'A bare name a ' + 'declared flow variable or node id holds is the variable, not the field (the run seeds those before ' + 'it flattens the record), so it is not typed as the field. Any other flow keeps a warning, now worded for the failed run instead ' + 'of a null. There is no mechanical rewrite: whether the author meant the length, a number stored as ' diff --git a/packages/spec/src/migrations/migrations.test.ts b/packages/spec/src/migrations/migrations.test.ts index d9316c94ffa..cc58113820c 100644 --- a/packages/spec/src/migrations/migrations.test.ts +++ b/packages/spec/src/migrations/migrations.test.ts @@ -802,3 +802,17 @@ describe('per-release section (ADR-0087 D4, package-version resolution)', () => expect(Object.keys(release.added[0])).not.toContain('since'); }); }); + +describe('flow-record-read-and-decision-type-mismatch-refused names the object a time-relative flow is typed against', () => { + // The author-time gate types a time-relative flow's conditions against + // config.timeRelative.object when the start node names no objectName + // (packages/lint validate-expressions.ts, the flow walk), so the entry's + // surface must name that key, not only the start node's objectName. + const entry = () => + MIGRATIONS_BY_MAJOR[18]!.semantic.find((s) => s.id === 'flow-record-read-and-decision-type-mismatch-refused'); + + it('the surface names config.timeRelative.object as the swept object', () => { + expect(entry()).toBeDefined(); + expect(entry()!.surface).toMatch(/sweeping config\.timeRelative\.object/); + }); +});