Skip to content

feat(intent): cross-model schedule source (model: on schedules + forEach) - #6532

Merged
delchev merged 1 commit into
masterfrom
feat/intent-cross-model-schedule-source
Aug 6, 2026
Merged

feat(intent): cross-model schedule source (model: on schedules + forEach)#6532
delchev merged 1 commit into
masterfrom
feat/intent-cross-model-schedule-source

Conversation

@delchev

@delchev delchev commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

What

A P6 schedule's source entity — and, inside a scheduled generate, a forEach collection — may now live in another intent model, referenced by a new model: <uses alias> key. Omitting model: keeps today's behaviour (local source), and existing generations stay byte-identical.

uses:
  - { model: projects }
schedules:
  - name: monthlyProjectTimesheets
    cron: "0 0 2 1 * ?"
    entity: Project
    model: projects                # NEW: the source entity's model alias
    where: [ { field: Status, op: eq, value: 2 } ]
    generate:
      to: ProjectTimesheet         # LOCAL — no uses: needed
      map: { Project: id }
      children:
        - to: EmployeeTimesheet
          parent: ProjectTimesheet
          forEach:
            entity: EmployeeProjectAssignment
            model: projects        # NEW: a cross-model forEach collection
            match: { Project: id }
          map: { Employee: Employee }

Why

Today a schedule's source must be a local entity, so the module that owns the created rows cannot own the schedule — it is forced into the source's module with a uses: back-edge, a dependency in the wrong direction (and a leaf-first-generation / bootstrap headache for any pair of mutually-referencing models). Letting the consumer own the schedule and read the source cross-model removes that inversion. This is the existing consumer→owner uses: direction — a consumer's generated code already binds to an owner (PROJECTION entities, leafOnly validation), so no new coupling is introduced.

How

  • ScheduleIntent.model — new field (Gson-mapped YAML key).
  • Parsermodel: must name a declared uses: alias (parse); for a cross-model source the local entity/field checks are skipped and validated later; a cross-model source with notify is rejected (v1 scope — notify needs the source's relation metadata, which only a local entity carries); forEach.model handled the same way.
  • Glue generator — resolves the source through CrossModelSupport (workspace .model first, registry fallback, loud failure) — the same two-tier resolution relations / dependsOn / leafOnly already use. Every where / map / match reference is validated at generation against the owner's properties (skipped on the convention fallback, like dependsOn); a miss drops that schedule with a warning rather than emitting code that cannot compile. Emits sourceCrossModel/sourceModel (+ forEachCrossModel/forEachModel), every existing glue key preserved.
  • generateUtils.js + Job.java.template — the source (and cross-model forEach) imports resolve against the owner's gen.<owner>.data… package (the leafOnly precedent — the job only READS the source). sourceGenFolder defaults to the local folder, so local schedules regenerate byte-identically.
  • Intent editor — a cross-model schedule card renders with the qualified label and no edge.
  • Docsintent-assistant-guide.md; plus the two doc sites (see below).

Validation split (mirrors relations / dependsOn / leafOnly)

Check When
model: names a declared uses: alias parse
notify + cross-model source (rejected, v1) parse
source entity exists in the owner's .model generation (loud drop + warning)
where/map/match fields exist on the owner generation (skipped on convention fallback)

Scope

v1 is generate-action schedules only; the cross-model source is read-only (a schedule never writes it — one-writer-per-state preserved). Out of scope: notify over a cross-model source (needs NotificationSupport over TargetInfo, no driving case), and any write to the source.

Tests

  • UnitIntentParserTest (declared-alias / undeclared-alias / notify-rejected / cross-model forEach) and GlueSchedulesTest (local stays not-cross-model; cross-model source emits the source + forEach keys and the Criteria). Full engine-intent suite green (308).
  • ITIntentCrossModelScheduleSourceIT (two mutually-referencing modules, HTTP-only): asserts the generated Job tokens (owner-package source import, Criteria, owner-package forEach FQNs) and the OUTERMOST layer — publish both, force-sync, seed the source over REST, and assert the cron-fired job created the target + child rows over REST — plus both loud-failure paths (owner .model absent; a where-field typo reported naming the owner model).
  • Regression: IntentEngineIT (36) + IntentEmissionCoverageIT (1) green — local-source Job emission unchanged.

Docs

Vendor-neutral spec (intentfile.org) and branded (dirigible.io /help/intent/) updated in the same effort.

Notes / deviations

The /parse endpoint receives only raw YAML, so — exactly as for relations, dependsOn and leafOnly — cross-model reference resolution happens at generation, not parse. No /parse signature change was needed.

…ach)

A schedule's source entity - and a generate forEach collection - may live in
another intent model, referenced by `model: <uses alias>`. This lets a schedule
live with the module that owns the CREATED rows instead of being forced into the
source's module with a dependency in the wrong direction.

- Omitted `model:` = local, fully backward compatible (existing generations stay
  byte-identical: `sourceGenFolder` defaults to the local gen folder).
- The generated JobHandler imports the OWNER's `gen.<owner>.data...` classes and
  only READS them (the leafOnly precedent); one writer per state is preserved.
- v1 scope: the generate action only. A cross-model source with notify is
  rejected at parse (notify needs the source's relation metadata).
- Validation split (mirrors relations / dependsOn / leafOnly): the `model:` alias
  must be a declared `uses:` (parse); the source entity and every where/map/match
  field reference are validated at generation against the owner's .model, via
  CrossModelSupport (workspace first, registry fallback). An unresolvable owner or
  a mistyped field drops the schedule with a warning - never code that can't compile.

Includes IntentParser + GlueIntentGenerator unit tests and a two-module HTTP-only
IT (IntentCrossModelScheduleSourceIT) asserting the generated Job tokens, the
running-instance row creation over REST, and both loud-failure paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@delchev
delchev merged commit 39602c5 into master Aug 6, 2026
10 checks passed
@delchev
delchev deleted the feat/intent-cross-model-schedule-source branch August 6, 2026 05:14
delchev added a commit that referenced this pull request Aug 9, 2026
A `generates` create-from could only be authored on the module owning the
SOURCE, with the target optionally foreign (`uses:`). `fromUses:` adds the
mirror: the source is foreign and the target is local, so the same button can
be authored on the module owning the TARGET.

Why it matters beyond symmetry: "A generates into B" authored in A puts a
reference to B's entities inside A's generated controller. When B already holds
a foreign key back into A - which is usually WHY the create-from exists - the
two modules' generated Java is mutually dependent and neither can be compiled,
or packaged as a jar, before the other. Authoring the create-from in B with
`fromUses: A` leaves exactly one edge, B -> A, and the pair is a DAG again.
Same reasoning that made a schedule's source addressable across models (#6532).

- GeneratesIntent: `fromUses` + isCrossModelSource().
- IntentParser: the alias must be declared under uses:; the source, its items
  and its status relation are no longer checked against this model (they are
  resolved from the owner .model, as a cross-model target already is);
  forEntity must BE the source - the button is contributed onto the owner's
  view and no local view carries the source id the endpoint needs. The
  unknown-local-source message now names the new key.
- CrossModelSupport.TargetInfo: statusProperty, read off the owner .model's
  DOCUMENT_STATUS widget (the same fact isEntityStatus() carries locally), so
  the sourceStatus completion hook works for a cross-model source. Never
  guessed - a wrong guess would emit updateProperty against a column that does
  not exist, failing only on the button's first click.
- Glue + generateUtils + Generate.java.template: fromGenFolder / fromProjectName
  target the source's gen folder and the "-transitioned" topic of the project
  that OWNS the source. Both default to the local values, so a local generate
  renders byte-identically.
- GeneratesIntentGenerator: the button registers on the OWNER's
  <owner>-custom-action point - the one that view actually reads - while the
  descriptor file and the controller stay in the authoring project.

Guide section + parser/glue coverage; engine-intent suite 347 green.

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.

1 participant