Repository navigation
Commit d85615d
Part of #19939
Clause-②: no (narrowing)
Pass 3 of #19939, under ruling D on #11182 (maintainer 「11182 D 其他同意」):
the date macros `{TODAY()}` / `{TODAY() ± N}` / `{NOW()}` / `{NOW() ±
N}` leave the flow value slots, refused with their CEL string form. With
them, a value slot reads no single-brace token at all. The pass also
carries the three carries named in the claim (`6067854904`,
`6092547738`, and the kept-spelling leak from `6092730938`), plus seat
3's pointer `6094071127` on the `has()` guard. #19939 stays open for the
six rider positions, so this PR uses `Part of`.
This branch was built by two dev runs. The first stopped before opening
a PR. This run merged `main` (`d748ae80af`) through `os-regen-merge.sh`,
added the guard fix, and re-ran every reading below on the final head.
## What lands
**The refusal (`@objectstack/spec`).** `valueSlotTemplateRefusals` keeps
no token kind. A date macro, alone or beside other text and tokens, is
refused at `objectstack validate`, at `registerFlow` and by the
executor, naming its CEL string form (`celDateMacro` in the
package-internal token module):
| you wrote | the refusal names |
|:--|:--|
| `'{TODAY()}'` | `isoDate(today())` |
| `'{TODAY() + N}'`, `'{TODAY() - N}'` | `isoDate(daysFromNow(N))`,
`isoDate(daysAgo(N))` |
| `'{NOW()}'` | `isoDatetime(now())` |
| `'{NOW() + N}'`, `'{NOW() - N}'` | `isoDatetime(addDays(now(), N))`
with `N` signed; `daysFromNow` would land on midnight |
| `'{TODAY() + days}'` | `isoDate(addDays(today(), days))` |
**The edges the remedy names** (carry `6067854904` item 2), each said in
the refusal itself:
- A fractional offset. The template added the offset to the day of the
month and truncated the sum, while `addDays` truncates the offset. Going
back, they land a day apart once the day of the month passes the offset.
`daysAgo(1.5)` / `daysFromNow(1.5)` are refused at build.
- A variable offset. The template looked the offset up as one variable
name and added 0 days, without a word, when it found none or the value
was not a number. CEL reads the path, an absent variable fails the run,
and a value that is not a number is an invalid date that `isoDate`
refuses.
- An offset that is neither a number nor a variable name (`{TODAY() +
3d}`). The template added 0 days, so the remedy writes
`isoDate(today())` and says why.
**The kept-spelling leak** (`6092730938`). `'Due {TODAY()} by
{$User.Id}'` was kept whole while the macros were kept. It is refused
now with one concatenation, `'Due ' + isoDate(today()) + ' by ' +
current_user.id`, and a sentence per token (the run user's guard
included).
**Where an envelope is literal data, the remedy names none there**
(carry `6092547738` item 1). At an element of the legacy `assignments`
array, a key of the bare legacy config, and a string inside an object or
list literal, the executor reads an envelope as data. The judge used to
prescribe one at that position, and the run then stored the envelope
object. **Chosen: judge those positions as non-evaluating.** The refusal
there names what does evaluate:
- the whole value built as one envelope, a CEL map or list literal
holding the string's CEL spelling at its place (`{'note': 'for ' +
name}`), with mixed values each wrapped in `dyn(…)`;
- or, for a legacy shape, the node's assignments moved into the
canonical `assignments` map.
Why not make the executor evaluate envelopes there: an envelope-shaped
object at those positions is data today, pinned by `the legacy shapes
are untouched` and the CRUD nested-envelope pin. Evaluating it would
change what an existing flow writes. It would also need the spec ledger
to declare those positions as value slots, a contract change out of
proportion to a remedy text. The judge-side fix is text only, consistent
with how the executor reads the positions, and pinned through the
engine: each named spelling, read off the refusal and put back, writes
the template's value, and the envelope the old remedy named is stored as
an object (controls).
**`flow-double-brace-interpolation`'s value-slot hint** (carry
`6092547738` item 2). In a value slot it named `{record.title}`, which
the judge refuses. It now reads the author's intent: each `{{ path }}`
hole becomes the single-brace token it means, and the hint carries the
envelope the judge writes for that string at the string's own position
(pass 2's probe, `valueSlotRefusalAt`). Elsewhere the hint is unchanged.
`flow-bare-dollar-reference`'s value-slot hint also asks the judge at
the position now, so a nested or legacy-shape string gets the spelling
that evaluates there. The two #1315 false-positive guards are re-titled;
their assertions are unchanged.
**The `has()` guard holds for an absent variable** (seat 3's pointer
`6094071127`). For a path that may be absent, the refusal named a guard
on the last key, `has(source.id) ? source.id : null`. `has()` evaluates
everything but its last selection, so where the variable itself was
never bound (an `isInput` variable the caller left out, with no default)
that guard failed the run, `Unknown variable: source`. Measured through
`AutomationEngine` at `0593be8782`: `has(vars.source.id)` fails too (`No
such key: source`). The judge now prints a chain off `vars`, which holds
only the bound variables: `has(vars.source) && has(vars.source.id) ?
vars.source.id : null`. That answers `null` for an unbound variable, an
absent key or intermediate key, a `null` variable and a non-map
variable, and the value when bound. `flow-bare-dollar-reference`'s hint
carries the judge's words, so it is fixed at the producer.
**Texts that said the macros were kept, or "no string form yet"** (carry
`6067854904` item 3): `content/docs/automation/flows.mdx` (FROM → TO
rows, the literal-data paragraph, the dialect table, the failure modes,
the guard row), the judge's and the token module's docblocks,
`builtin/template.ts`'s docblock, the
`validate-expressions.fields-value-slot.test.ts` comment, the published
`service-automation` README *Expressions* section, and the two
value-slot `.describe()` strings with the regenerated
`builtin-node-config.mdx`. `skills/**` is not touched (#22585).
**The ledger.** The step-18 D3 entry
`flow-value-slot-template-dialect-refused` is amended: its surface,
replacement, reason and acceptance criteria name the macros, their forms
and edges, the literal-data positions and the guard.
`flow-text-slot-single-brace-refused` is amended where its replacement
computed a date through the value-slot spelling this change refuses.
`registry.ts` is regenerated (`gen:migration-registry`); it is
byte-identical to the generator's output after the merge of `main`. No
D2 conversion and no new entry.
**Census.** No date macro sits in a value slot in `examples/**` or in
`packages/**` source at `3c97f71067`. The remaining hits are filter
positions, which keep the dialect, and docs. This repository's own test
fixtures and docs that used one are migrated. Cross-repo note: pass 1's
census measured 15 date-macro value-slot sites in hotcrm at `c529de2` (8
`{NOW()}`, 3 `{TODAY()}`, 3 `{TODAY() + N}`, 1 `{TODAY() + var}`). They
migrate there; hotcrm is not edited.
**Changeset.**
`.changeset/19939-flow-value-slot-date-macros-refused.md`:
`@objectstack/spec` major, `@objectstack/service-automation` major,
`@objectstack/lint` patch (pass 1 and pass 2's levels, on the `next`
line), with FROM → TO rows and the ADR-0087 `already-registered`
disposition. `Clause-②: no (narrowing)` holds on measurement. The new
`celDateMacro` / `CelDateMacro` exports stay in the package-internal
`flow-template-token.ts`, which no entry re-exports (spec's `exports`
has no wildcard subpath). No `api-surface/` or `export-origins/`
snapshot moved, and `check:api-surface` / `check:export-origins` exit 0.
## Surface crossings
The claim lists the judge, the token module, `builtin/template.ts`, the
parity pin, `lint-flow-patterns.ts`, the `.describe()` strings, the
step-18 value-slot entry and `registry.ts`, `flows.mdx`, the README,
examples and one changeset. These files are outside that list. Each one
would have stated something false after this change, or pinned a
spelling it refuses:
- `service-automation/src/builtin/crud-nodes.ts`, `logic-nodes.ts`:
docblock comments only. They said the date macros were kept and reached
`interpolate()`.
- `spec/src/automation/flow-text-slot-template.ts` (and its test), with
the step-18 entry `18.flow-text-slot-single-brace-refused.ts`: the
text-slot remedy for a date macro named `assignments: { v: '{TODAY() +
7}' }`, a spelling this change refuses. It now names the CEL envelope.
- Tests that pinned the kept spelling:
`spec/src/automation/builtin-node-config.test.ts`, and in
`service-automation`, `logic-nodes.test.ts`,
`value-slot-template-grammar.test.ts` and
`assignment-value-envelope.test.ts` (the literal-position engine pins).
`value-slot-template-grammar.test.ts` also hosts the engine pin of the
guard.
No governed surface is touched (`skills/**`, `.claude/**`,
`docs/adr/**`, `AGENTS.md`, `CLAUDE.md`).
## Tests and gates (head `3c97f71067`)
`3c97f71067` is the final head. It holds the merge of `main` at
`d748ae80af` (`5027462a6a`), the regenerated reference (`31f20e025e`),
the guard fix (`0593be8782`), then the merge of `main` at `e22315238f`.
Both merges went through `os-regen-merge.sh`. `registry.ts` regenerates
byte-identical after each, and every entry id on `main`'s registry is
present. The build is spec plus the dependency closures of
`service-automation` and `lint` (29 turbo tasks, exit 0), rebuilt after
each merge.
All readings below were taken at `3c97f71067`. The same set was green at
`0593be8782` before the second merge.
- `@objectstack/spec` local project: 642 files, 19164 passed and 1 todo.
Repo project: 54 files, 915 passed.
- `@objectstack/service-automation`: 181 files, 2316 passed.
- `@objectstack/lint`: 134 files, 6130 passed.
- Typecheck exit 0 for spec, lint and service-automation (each package's
script, `check:test-typecheck` included).
- Gates: `dispatch-gates --commands`, re-derived on the actual diff,
gives 117 commands: the 114 at dispatch plus spec `check:generated`,
`check:quick-reference-counts` and `check:swallow-census-controls`. All
117 exit 0, and `dispatch-gates --ran` reports 117 derived, 117 run, 0
NOT-MEASURED and 0 UNRUN.
- Repo lint, narrowed and proven:
- ① The population is `eslint . --no-inline-config` over
`eslint.config.mjs`.
- ② `--format json` over the 20 changed lintable files reports 20
linted, 0 ignored, 0 errors and 0 warnings.
- ③ That config enables no type-aware linting (no
`parserOptions.project`, no typed rules, as its own note says), so this
diff cannot move a verdict on a file it does not touch. The full `pnpm
lint` is CI's.
- `main` moved to `f368b7e980` after these readings. Its six new commits
share no path with this PR, and `registry.ts` is not among them. The
PR's CI runs on the merge ref.
## Ablations
Five ablations, one per behaviour: the order's four, plus the guard.
Each went through `scripts/ablation-replace.mjs` in WRAP mode under the
verify lock, from the committed state `0593be8782`. Each mutation is
verified on disk (anchor x1 → x0, blob changed). Each restore is proven
blob == HEAD with an empty `git diff HEAD`.
The suites under `service-automation` and `lint` resolve
`@objectstack/spec` through its built `dist/` (no source alias). So A2,
A3 and A5 rebuilt spec on the mutated source. Each confirmed with
`ablation-dist-preflight.mjs` that the marker reached `dist/` before
reading a result. After the restore, each rebuilt spec again and
confirmed with `--absent` that the marker left `dist/`, with `git status
--porcelain` empty.
- **A1, the refusal.** The judge skips a string carrying a date macro
again (the old kept kind). `flow-value-slot-template.test.ts` (src) goes
red: 23 failed, 94 passed. Every date-macro refusal, the edges, the
kept-spelling leak and the doors' contract pins fail. Restored to blob
`e403d51fd5cc`.
- **A2, the parity.** `{NOW() ± N}`'s remedy is mutated to land on
midnight, `isoDatetime(addDays(today(), N))`. The live parity pin in
`crud-fields-value-envelope.test.ts` evaluates the spelling the judge
prints and goes red on the written bytes: 2 failed, 57 passed (`{NOW() +
2}`, `{NOW() - 1}`). Example: `expected '2026-03-10T00:00:00.000Z' to be
'2026-03-10T09:30:00.000Z'`. Restored to blob `5ca227356df7`.
- **A3, the literal-data positions.** `literalPositionLead` answers
nothing, which puts back the envelope at the position. The judge test
(src) goes red: 5 failed, 112 passed. The engine pins go red: 8 failed,
97 passed, across `assignment-value-envelope.test.ts` and
`crud-fields-value-envelope.test.ts`. The controls stay green: the
envelope at those positions is stored as an object, and the top-level
slot keeps its envelope remedy. Restored to blob `e403d51fd5cc`.
- **A4, the double-brace hint.** The old `Use {var}` hint at every
position. `lint-flow-patterns.test.ts` (src) goes red: 3 failed, 203
passed, exactly the three value-slot double-brace pins. Both controls
stay green. Restored to blob `6366250123ba`.
- **A5, the guard.** The guard is put back on the last step alone,
`has(vars.source.id) ? …`. The judge test goes red: 5 failed, 112
passed. The engine pins in `value-slot-template-grammar.test.ts` go red:
5 failed, 70 passed, each on evaluation, for example `{source.id}: … No
such key: source`. The single-segment `{x}` row stays green, as it
should. The lint hint pin goes red: 1 failed, 205 passed. Restored to
blob `e403d51fd5cc`.
## Acceptance notes
Nothing here is filed. Each item is for the seat:
- **Pending release notes this change makes false.** Pass 1's
`19939-flow-value-slot-template-dialect-refused.md` lists the date
macros as accepted and prints the last-key guard. Pass 2's
`19939-flow-value-slot-run-user-refused.md` lists the date macros as
still accepted. #22110's `22110-flow-text-slot-double-brace.md` computes
a date through the value-slot spelling. This PR's changeset says it
supersedes those lines. Correcting them in place is
`check:empty-changeset`'s DELIBERATE CORRECTION path, which #22586
carries.
- **`record` with no `record` variable.** In a flow CEL expression,
`record` reads the variables map itself when the run binds no `record`
variable: `buildScope` binds the scope's `record` argument, and
`celScope` passes `vars` there. So `has(record.assignee) ?
record.assignee : null` returns a variable named `assignee` ("u9" over
`{ assignee: 'u9' }`). Measured through `evaluateValueEnvelope` at
`0593be8782`; no public door was measured, so it is not filed. The
judge's chain guard reads `vars.record` and answers `null` there, as the
template did.
- **Comments outside the surface that still show a refused spelling:**
`packages/lint/src/validate-field-consumers.ts:335` (`fields: {
added_date: '{NOW()}' }`, an illustration of a write key) and
`packages/spec/src/automation/flow-node-expression-paths.test.ts:130` (a
`{token}` string "keeps its 17.x meaning", stale since pass 1). The
generated `spec-changes.json` and `docs/protocol-upgrade-guide.md`
regenerate at release; no regeneration is owed here.
- **`skills/objectstack-automation/SKILL.md`** still teaches the date
macros as kept (Tier H, #22585).
- **The six rider positions** keep the single-brace dialect. The carry
line on #19939 stands.
- **PR #22609** (seat 3) edits `lint-flow-patterns.test.ts`'s
`valueFlow` fixture in hunks it measured as disjoint from this PR's.
Whichever lands later merges `main`.
- **`Clause-②`.** The claim line reads `no`; the dispatch prescribes `no
(narrowing)`, the same value with its direction arm stated.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f53f14b commit d85615d
24 files changed
Lines changed: 1351 additions & 271 deletions
File tree
- .changeset
- content/docs
- automation
- references/automation
- packages
- lint/src
- services/service-automation
- src/builtin
- spec/src
- automation
- migrations
- entries/semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
265 | 265 | | |
266 | 266 | | |
267 | 267 | | |
268 | | - | |
| 268 | + | |
269 | 269 | | |
270 | 270 | | |
271 | 271 | | |
272 | 272 | | |
273 | 273 | | |
274 | 274 | | |
275 | 275 | | |
276 | | - | |
277 | | - | |
278 | | - | |
279 | | - | |
280 | | - | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
281 | 300 | | |
282 | 301 | | |
283 | 302 | | |
| |||
327 | 346 | | |
328 | 347 | | |
329 | 348 | | |
330 | | - | |
| 349 | + | |
331 | 350 | | |
332 | 351 | | |
333 | 352 | | |
| |||
347 | 366 | | |
348 | 367 | | |
349 | 368 | | |
350 | | - | |
351 | | - | |
| 369 | + | |
| 370 | + | |
352 | 371 | | |
353 | 372 | | |
354 | 373 | | |
| |||
1994 | 2013 | | |
1995 | 2014 | | |
1996 | 2015 | | |
1997 | | - | |
1998 | | - | |
| 2016 | + | |
1999 | 2017 | | |
2000 | 2018 | | |
2001 | 2019 | | |
2002 | 2020 | | |
2003 | 2021 | | |
2004 | 2022 | | |
2005 | | - | |
| 2023 | + | |
2006 | 2024 | | |
2007 | 2025 | | |
2008 | 2026 | | |
2009 | 2027 | | |
2010 | 2028 | | |
2011 | 2029 | | |
2012 | 2030 | | |
2013 | | - | |
2014 | | - | |
2015 | | - | |
| 2031 | + | |
| 2032 | + | |
| 2033 | + | |
2016 | 2034 | | |
2017 | 2035 | | |
2018 | 2036 | | |
| |||
2029 | 2047 | | |
2030 | 2048 | | |
2031 | 2049 | | |
2032 | | - | |
2033 | | - | |
| 2050 | + | |
| 2051 | + | |
2034 | 2052 | | |
2035 | 2053 | | |
2036 | 2054 | | |
| |||
0 commit comments