Skip to content

feat: trigger.type continuous (0.7.6 preview) is never scheduled; --strict fails on a retargeted overlay patch - #734

Merged
fas89 merged 12 commits into
mainfrom
feat/continuous-trigger-strict-overlays
Oct 21, 2026
Merged

fas89 merged 12 commits into
mainfrom
feat/continuous-trigger-strict-overlays

Conversation

@fas89

@fas89 fas89 commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

Two things #731 and #732 left open.

trigger.type: continuous (0.7.6 preview). A long-running Flink job, or a Kafka Connect or Debezium connector, had no trigger type: schedule, event and the rest all start runs. #731 answered the requester's question with "omit trigger", which still let scheduler synthesis give such a build a nightly 0 2 * * * task. Now:

  • Schema (0.7.6 only; 0.7.5 unchanged). continuous joins $defs.trigger.type. A continuous trigger takes no other trigger: cron, schedule, datasets, event and timetable are refused beside it (the rule's if is type: object, so a blank trigger: gets one error, not six).
  • One predicate. util/contract.is_continuous_build, and is_unscheduled_build (= runs outside forge-cli, or continuous). Scheduler synthesis (fluid generate schedule, the template-mode schedule), fluid generate schedule's "nothing to schedule" short-circuit, and the acquisition schedule-sync all use it. The fluid apply DAGs already render only schedule triggers.
  • Readable refusals. format_schema_error renders any not: {required: [field]} error as the rule's description, or <field> is not allowed here, instead of quoting the whole object: cron is not allowed with type continuous: a continuous job runs all the time, so its trigger takes no other trigger. Remove cron, or set type: schedule. AGENTS.md records the convention.

--strict fails on a retargeted overlay patch. The loader logged overlay_patch_retargeted, and no --strict reads the log. loader.collect_overlay_warnings() (a context-variable collector) hands it to the command, which reports it as its own warning and logs it at DEBUG only, so it shows once:

  • fluid validate --env (one contract, and workspace mode), fluid contract-validation --env, fluid test --env (native engine; an "Environment overlay" row): listed as a warning, and --strict fails on it.
  • --quiet still shows it (WARNING:), and --quiet --strict names it (ERROR (--strict): …), also beside schema errors.
  • fluid validate --strict says Validation failed: warnings treated as errors (--strict) when warnings alone fail it after a summary that reads valid.
  • overlay_entry_added stays a log line: an entry only prod adds is often intended, and --strict could not be silenced on it.
  • Tables escape the warning, which quotes the contract's ids (an id of [/] raised MarkupError).

Borrowed, not built

  • Databricks Jobs continuous trigger (docs): the name, and "a continuous trigger cannot be combined with any other trigger".
  • Diverged on purpose from Airflow's @continuous (ContinuousTimetable, which starts batch runs back to back): for an always-on job it would loop a DAG that runs nothing.
  • The repo's own context-variable collector (build_runners/kafka_connect/iceberg_sink_validation.py, _AT_RUN), in the shape of warnings.catch_warnings(record=True) under CPython 3.14's context_aware_warnings.
  • Strategy: adapt the pattern; no new dependency.

Validation

  • New tests: tests/test_continuous_trigger.py (26) and 16 more in tests/test_overlay_keyed_merge.py. Each was red on the commit before its fix (25 on main, 3 after round 1, 6 after round 2, 1 after round 3).
  • Mutations (test-suite audit): 17 of 19 killed. The survivor, the fluid_apply type check, now has a test that kills it.
  • Gates (local): ruff, black 24.10.0, scripts/mypy_strict.py, import-linter (6 kept), license headers, perf budgets (38 passed), bandit. 236 affected test files: 6811 passed. The 20 failures + 2 errors also fail on clean main in this venv (environmental; reproduced id-for-id).
  • Live CLI: fluid validate on the requester's Flink contract with type: continuous passes; with cron beside it, one error naming the fix; on 0.7.5, the enum error. fluid generate schedule on a Flink job and on a continuous connector: "nothing to schedule", no files; the same connector with type: schedule still gets its DAG. A 0.7.5 positional overlay: fluid validate --env prod exit 0 with the warning once; --strict exit 1 with the reason; --quiet / --quiet --strict / workspace mode / fluid test / contract-validation as above.
  • ripwire --quality-delta: the gating rows left are pytest functions as "dead code", per-module test helpers as duplication (house style), the schema's JSON verbosity, and one test class over 60 lines.

Reviews

  • /security-review: no finding at the bar, in four passes (every round).
  • /world-class-inspection: PASS on all four dimensions after round 1 (documentation, codebase, tests, architecture); persistence and web n/a. Round 1 fixed: fluid test --strict failing silently, workspace mode skipping the check, the help example wrapping at 80 columns, docs/schedule.md, one vacuous test.
  • /defect-scan: three hunters (logic, runtime, cross-file contract). Rounds 2 and 3 fixed: a blank trigger: drawing five continuous refusals, --quiet hiding the warning, the markup crash, --quiet --strict beside schema errors. Final re-verify: all fixed.

Documentation

Not in this PR

  • fluid bundle --env / fluid validate <bundle> --strict (generated pipelines' stage 2) cannot see the warning: the merge is frozen in the bundle. Follow-up: record the collected events in the MANIFEST source block.
  • An overlay cannot turn a scheduled build continuous (overlays cannot delete cron); documented.
  • Pre-existing, documented or follow-up: fluid scaffold-composer writes a nightly DAG for any contract; the dbt and python runners refuse continuous as they refuse event; a DAG deployed before a product went all-continuous is not retired; fluid test's _detail() escape is unused by every other row; --engine soda ignores --strict.
  • Re-vendoring the 0.7.6 schema into open-data-protocol/fluid follows this PR, as fix(marketplace): add API timeouts and resilient errors #52 did for 0.21.0.

🤖 Generated with Claude Code

fas89 added 12 commits October 21, 2026 10:06
…ld, or the rule's description

A `not: {required: [field]}` rule quoted the whole object and the subschema it
must not match: "{'type': 'continuous', 'cron': '0 2 * * *'} should not be valid
under {'required': ['cron']}". It now reads "<field> is not allowed here", as it
already did under sink.catalogAuth, or the rule's own `description` when it
carries one, so a schema can say why and what to do instead.
…eduled

A long-running Flink job, or a Kafka Connect or Debezium connector, had no
trigger type: every one of them starts runs. `continuous` says the platform
keeps the job running, so no scheduler starts it: scheduler synthesis
(`fluid generate schedule`, the template-mode schedule) and the acquisition
schedule-sync leave the build out, where a connector used to get a nightly
0 2 * * * task, and a contract with no other build has nothing to schedule.
The `fluid apply` DAGs already render only schedule triggers.

The schema refuses cron, schedule, datasets, event and timetable next to it,
and each refusal names the field and the trigger type that takes it. 0.7.5 is
unchanged.

Borrowed: the name and the "no other trigger" rule from Databricks Jobs'
continuous trigger (docs.databricks.com/jobs/triggers.html). Diverged on
purpose from Airflow's @continuous (ContinuousTimetable), which starts batch
runs back to back: for an always-on job that would loop a no-op DAG.
…her entry

The loader logged overlay_patch_retargeted, and a command's --strict reads its
own findings, never the log, so `fluid validate --env prod --strict` passed a
0.7.5 overlay that patched the wrong build. `collect_overlay_warnings()` hands
the warning to `fluid validate` and `fluid contract-validation`, which report
it as their own warning: shown without --strict, failing with it, and listed in
`--format json`. The loader logs a warning a block takes at DEBUG only, so the
terminal shows it once, and does not mark it as said, so a later load outside a
block still logs it. overlay_entry_added stays a log line: an entry only prod
adds is often intended, and --strict could not be silenced on it.

Borrowed: the context-variable collector already used by
build_runners/kafka_connect/iceberg_sink_validation.py (_AT_RUN), and the
shape of warnings.catch_warnings(record=True) under CPython 3.14's
context_aware_warnings, which keeps one context's warnings out of another's.
…h shows it

`fluid test --strict` reads ContractValidator's report, so it failed on the
overlay warning without printing it: its table had no row for the category,
and the loader now logs a collected warning at DEBUG only. It shows as an
"Environment overlay" row. Workspace mode (`fluid validate --env prod
--strict` with no contract) loaded each product outside the collector and
passed; it now lists the warning under the product and fails it under
--strict. `fluid validate --strict` says "Validation failed: warnings treated
as errors (--strict)" when warnings alone fail it, after a summary that reads
valid. The --help example fits 80 columns.
…hrough one predicate

The acquisition schedule-sync used is_continuous_build while synthesis and
`fluid generate schedule` use is_unscheduled_build; widening the rule would
have missed it. The fluid-apply DAG test now gives the continuous trigger a
cron, with a schedule-trigger control, so it can fail.
The rule's `if` had no `type: object`, and `required` and `properties` pass on
anything else, so a blank `trigger:` (null), a string or a list drew five
refusals of a continuous trigger nobody declared beside the real error. The
sibling catalogAuth rules already say `type: object`.
…bles escape its ids

Under --quiet, which prints errors only, the warning the loader always printed
had vanished, and --quiet --strict failed with a count alone. --quiet prints it
as a WARNING: line, and under --strict as "ERROR (--strict): <warning>".

The warning quotes the contract's ids, which `fluid test`'s "Environment
overlay" row and `fluid contract-validation`'s issue table passed to Rich as
markup: an id of "[/]" raised MarkupError. Both escape it, the row through
test.py's own _detail().
…de schema errors too

The ERROR (--strict) lines printed only when warnings alone failed the run, so
a contract that also had a schema error showed less under --strict than under
--quiet alone, and the second failure came as a surprise after the fix.
@github-actions github-actions Bot added cli Changes to the CLI surface or implementation docs Documentation changes tests Test coverage or test infrastructure changes needs-docs Pull request needs a linked docs update or justification labels Oct 21, 2026
@github-actions

Copy link
Copy Markdown

📄 Documentation Reminder

This PR appears to be missing a documentation reference. Our docs live in a separate repo.

Please update the PR description with one of:

  • Link a docs PR — check the "Docs PR linked" box and paste the URL
  • Mark as no docs needed — check "No docs needed" with a justification
  • Acknowledge docs TODO — check "Docs TODO" and create the docs PR before merge

See the Contributing Guide for details.

@github-actions github-actions Bot removed the needs-docs Pull request needs a linked docs update or justification label Oct 21, 2026
@fas89
fas89 merged commit d7fd06c into main Oct 21, 2026
34 checks passed
@fas89
fas89 deleted the feat/continuous-trigger-strict-overlays branch October 21, 2026 14:41
fas89 added a commit that referenced this pull request Oct 21, 2026
…test.py leaks

tests/cli/conftest.py sets FLUID_LOG_LEVEL=DEBUG, and some xdist workers keep
it. The CLI subprocesses here inherited it, so the loader's DEBUG copy of a
warning the command reports was printed too. The tests that count the warning
once then failed with `assert 2 == 1`, as #734's TestValidateStrict did on
main's CI run for 1320619. The subprocesses now drop FLUID_LOG_LEVEL,
FLUID_DEBUG and FLUID_SHOW_REGISTRATION, as tests/output_ports already do,
with API keys as before.
fas89 added a commit that referenced this pull request Oct 22, 2026
… text literally (#738)

* fix(console): error lines print exception and contract text literally

console_error wrapped its message in [red]...[/red], so Rich parsed the
message as markup: a lowercase [word] in it vanished, and a [/] raised
MarkupError. The CLI's last-resort handler reports through console_error,
so that raise escaped main() as a traceback. main()'s generic handler had
the same flaw one step earlier: "[red]Unexpected error: {e}[/red]" raised
on the same text and handed it to the fallback.

- New _console.literal_text(): a rich Text, never markup, with control
  characters spelled \xNN. ESC opens an escape sequence and the C1 range
  holds the one-byte CSI and OSC; printed raw, a message could clear the
  screen or plant a link. Rich's highlighter broke such sequences up on a
  top-level str by accident; a Text is not highlighted.
- _console.error prints that way, still redacted. None of its 130 call
  sites passes intended markup (AST scan).
- validate.py drops the rich.markup.escape it applied to six messages to
  survive the old wrapper; kept, it would print a stray backslash.
- main(): the Provider error and Unexpected error lines use literal_text,
  and the --debug traceback prints with markup=False.

Text rather than escape: escape doubles a trailing backslash (C:\work\
prints as C:\work\\) when no tag follows it.

* fix(cli): fluid test and contract-validation print contract text literally

Both reports quote the contract back, and Rich parsed it as markup. A
JSON Schema pattern lost its character classes (^[a-z0-9][a-z0-9-]*[a-z0-9]$
printed as ^*$), exposes[orders] lost its id, and an id or expose id
holding [/] raised MarkupError, so both commands died with a traceback.
Only the overlay row of fluid test and the issue table of
contract-validation were escaped (#734).

- fluid test: the header panel and every table row go through
  literal_text; the _detail() escape helper is gone.
- contract-validation: the summary panel, each issue's message and path,
  and a severity outside the styled three go through literal_text.
- The Rich-less plain reports, and the soda engine's failed check names
  and stderr, print with markup=False (the plain fallback strips [word]).

JSON, JUnit and the plain report file are unchanged.

* fix(console): redact before spelling controls; print soda and --debug text as written

Follow-ups from the security review and defect hunt of the two commits
before this one.

- error() redacts the raw text before it spells control characters, and
  again after. A spelled control ends in a hex digit, a word character,
  so "\x00PASSWORD=hunter2" no longer matched the redaction pattern and
  the secret printed.
- New _console.cprint_literal(): cprint with no markup and no :emoji:
  codes, control characters spelled, still redacted. The soda engine's
  check names and stderr and the Rich-less plain reports use it; Rich had
  printed "fe80:🅰️b:c" with a 🅱️ emoji.
- main(): the Provider error and Unexpected error lines and the --debug
  traceback are redacted. The MarkupError used to keep a secret beside a
  "[/]" off the screen; now that the text prints, it must be redacted. The
  traceback goes through literal_text as well.
- _show_controls handles CR itself, with or without Rich: a CRLF reads as
  a line feed and a lone CR is spelled.
- The CHANGELOG entry now matches the change: contract-validation's issue
  table was already literal, and only the shared error helper's lines are
  covered among error output.

* fix(console): cprint_literal ignores markup=; pin redaction, CRLF and plain-report spelling

Round-2 review follow-ups to the commit before: each of these protections
could be removed without a test failing.

- main()'s Provider error line is redacted (test with a ProviderError).
- error() keeps a CRLF as one line break, with and without Rich.
- Both Rich-less plain reports spell control characters in the
  contract's id and in issue messages.
- cprint_literal always turns markup and emoji off, so a caller passing
  markup= no longer raises TypeError for a duplicate keyword.
- The plain-report test in test_contract_validation_ext2 patches
  cprint_literal, which that report now calls.
- The CHANGELOG names the Provider error line and no longer says the
  reports are redacted.

* chore(secrets): refresh .secrets.baseline line numbers

Two entries were already stale on main: the Secret Keyword findings in
tests/test_cli_test_branches.py (413, now 424) and
tests/test_contract_validation_ext2.py (574, now 586) moved when those
files grew. CI's detect-secrets step scans only changed files, so it
asked for the refresh once this branch touched them.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Changes to the CLI surface or implementation docs Documentation changes tests Test coverage or test infrastructure changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant