Skip to content

intent: resolve a seeded status name in schedules[].where (#7251) - #7269

Merged
delchev merged 1 commit into
masterfrom
issue-7251-schedules-where-status-symbol
Sep 10, 2026
Merged

intent: resolve a seeded status name in schedules[].where (#7251)#7269
delchev merged 1 commit into
masterfrom
issue-7251-schedules-where-status-symbol

Conversation

@delchev

@delchev delchev commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Cause

#7164 taught StatusSymbolResolver to resolve a seeded status NAME in a create-from's items: where: (rewriteGeneratesItemsWhere). The construct that shape was explicitly modelled on - a schedules[].where row query, "the same { field, op, value } triples" - never got the rewrite, and the parser did not refuse a non-numeric value against a function: EntityStatus relation there either. So

schedules:
  - name: dunning
    cron: "0 0 8 * * ?"
    entity: SalesInvoice
    where:
      - { field: Status, op: eq, value: OVERDUE }

parsed, generated .eq("Status", "OVERDUE") into the job and matched nothing for as long as the schedule kept ticking - the silent failure #6645 removed for every other status site, at the site a status guard is written most often (dunning, staleness sweeps, month-end runs), and the one where replacing a positional id with a name is most tempting.

Change

  • StatusSymbolResolver.rewriteSchedules resolves the condition naming the source's own function: EntityStatus relation, on that source's nomenclature. It shares rewriteConditions with the items rule (which now delegates to it), so the two constructs cannot drift apart again by having one taught and the other not.
  • Same-model source only. A cross-model source (model: <uses alias>) is not in this file's entities, so neither its nomenclature nor even WHICH of its conditions names its status is knowable in the parser (its where field references are resolved at generation time against the owner's .model); it keeps the numeric seed id, exactly as every other cross-model status site does. This is the one place the issue's wording could not be followed literally - there is nothing identifiable to "say so" about, so the boundary is stated in the guide and the javadoc instead of as a refusal.
  • IntentParser.validateWhereStatusValue is the invariant at the consuming site, independent of the resolver's site list: a where condition on the queried entity's own status relation must carry an integer by the time validation runs. A value no status can equal - a stage word, a blank, a moment token - is refused by name instead of rendering into a query that matches nothing. Applied to both where sites (schedules[] and items:).
  • Docs: the schedules section of the assistant guide gains the dunning example and the rule (incl. the cross-model boundary); schedules[].where and items: where: join the site list of "Statuses may be named, not numbered"; engine-intent/CLAUDE.md and .claude/docs/intent-layer.md record the drift and the backstop.

No behaviour change for an authored numeric id, which is what the fleet uses today (base-timesheets: { field: Status, op: eq, value: 2 } # 2 = ACTIVE - the comment this makes unnecessary).

Verification

  • mvn -pl components/engine/engine-intent test: 1190 tests green, formatter cache wiped + formatter:validate green.
  • New: StatusSymbolIntentTest.everySiteResolvesTheNameToItsSeedId covers the schedule query alongside every other site, and anUnknownStatusNameInAScheduleQueryIsRejected pins the mistyped name; GlueSchedulesTest.aSeededStatusNameInTheQueryRendersAsItsSeedId pins the rendered criteria (Criteria.create().eq("Status", 3).lt("DueOn", java.time.LocalDate.now())) and aValueThatIsNoStatusAtAllIsRefused the backstop.
  • Both halves were checked against the pre-fix state: with rewriteSchedules commented out, the new tests fail, and the parser guard reports the authored name (schedule [dunning] where-condition on the status relation [Status] compares it with [OVERDUE], which is not a status ...) - i.e. the guard alone would have caught this drift.
  • Not run: no integration test. The change is parse-time only - no template and no generator was touched, and the emitted criteria shape (.eq("Status", <int>)) is the one existing schedules already emit and IntentEmissionCoverageIT already compiles and runs.

Fixes #7251

🤖 Generated with Claude Code

#7164 taught `StatusSymbolResolver` to resolve a seeded status NAME in a
create-from's `items: where:`. The construct that shape was modelled on - a
`schedules[].where` row query, "the same { field, op, value } triples" - had no
rewrite, and nothing refused a non-numeric value against a `function:
EntityStatus` relation there either. So

    schedules:
      - name: dunning
        cron: "0 0 8 * * ?"
        entity: SalesInvoice
        where:
          - { field: Status, op: eq, value: OVERDUE }

parsed, generated `.eq("Status", "OVERDUE")` into the job, and matched nothing
for as long as the schedule kept ticking - the exact silent failure #6645
removed for every other status site, at the site a status guard is written most
often (dunning, staleness sweeps, month-end runs), where replacing a positional
id with a name is most tempting.

`rewriteSchedules` resolves the condition naming the source's own status
relation, on that source's nomenclature. It shares `rewriteConditions` with the
items rule, so the two constructs cannot drift apart again by having one of
them taught and the other not. Same-model source only: a cross-model source
(`model: <uses alias>`) is not in this file's entities, so neither its
nomenclature nor even WHICH of its conditions names its status is knowable
here, and it keeps the numeric seed id exactly as every other cross-model
status site does.

The second half is the invariant at the consuming site, independent of the
resolver's site list: a `where` condition on the queried entity's own
`function: EntityStatus` relation must carry an integer by the time validation
runs (`validateWhereStatusValue`, on both where sites). A value no status can
equal - a stage word, a blank, a moment token - is refused by name instead of
rendering into a query that matches nothing. That check alone catches the drift
this issue is about: with the rewrite disabled, it reports the authored name.

Covered by `StatusSymbolIntentTest` (the schedule query joins the every-site
case; a mistyped name is refused) and `GlueSchedulesTest` (the rendered
criteria - `Criteria.create().eq("Status", 3).lt("DueOn", ...)`; a value that is
no status refused). Documented in the `schedules` section of the assistant
guide and in the site list of "Statuses may be named, not numbered".

Fixes #7251

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

intent: a seeded status NAME in schedules[].where is not resolved - renders .eq("Status", "OVERDUE") and matches nothing, while items.where resolves it

1 participant