Skip to content

CR-010.R6: scheduler-grade pipeline contract (--headless / --json / exit codes) - #244

Merged
harishquantamix merged 1 commit into
masterfrom
cr-010-r6-scheduler-public
Jul 29, 2026
Merged

harishquantamix merged 1 commit into
masterfrom
cr-010-r6-scheduler-public

Conversation

@quantamixsol

Copy link
Copy Markdown
Owner

CR-010.R6 — Scheduler-grade pipeline contract

Public cherry-pick of private PR #322 (merged 0d6bda41, feature commit 95813097). Cherry-picked as eedc4175 onto the current origin/master (c51d1f26), with the suite and trade-secret screen re-run on that base.

Closes leg 4 of the CR-010 enterprise adoption test: pip install → config file → secret path → scheduler entry. Legs 1–3 already hold via R4 + R5 (both live in 0.81.0).


The defect

graq rebuild is annotated -> int and returns a node count, but main.py:122 registers it directly with Typer — which discards return values. Measured on typer 0.24.1: a command returning 7 exits 0.

$ graq rebuild --graph-path missing.json
Graph file not found: missing.json
$ echo $?
0          # ← a hard failure a scheduler cannot detect

Separately, rebuild_chunks(force=False) skipped any node that already had chunks, so a node whose source file was edited kept stale evidence — a correctness gap, not just a performance one.

The contract

Exit Meaning
0 Success — work performed, nothing failed
1 Failure — including any node failure
2 Usage error
3 Empty delta — ran fine, nothing to do

EMPTY_DELTA is deliberately distinct from SUCCESS: collapsing both onto 0 leaves "did work actually happen?" unanswerable without parsing stdout, so a scheduler could not gate a downstream deploy on whether the graph changed.

New graqle/cli/headless.py:

  • RunReport is frozen, and exit_code is a derived property — a serialized report can never carry a status/exit_code pair that disagree.
  • errors carries exception type names only, inheriting the PII rule already enforced on .graqle/govern.health.json (reports get archived as scheduler artifacts).
  • --report-json reuses the existing atomic tempfile + os.replace pattern from govern_serve.py, including orphan-tempfile cleanup.
  • --headless and --json stay orthogonal — interactivity vs format. Coupling them would contradict the standalone --json convention at 15+ existing sites.
  • --incremental is content-hash based (SHA-256 in _chunk_content_hash), not mtime: git checkouts, CI clones and Docker layer caching all rewrite mtime, which is exactly where a scheduler runs. Graphs with no stored hash fall back to the previous behaviour.

Also ships cron / Airflow / GitHub Actions reference recipes (docs/scheduler-recipes.md).

Backwards compatibility

The corrected exit codes apply only when --headless, --json, or --report-json is passed — those callers are new by definition. Bare graq rebuild behaves byte-for-byte as before, with a stderr-only DeprecationWarning naming the future change. govern serve --once without --json is untouched (38 existing tests unchanged).

A follow-up release may make bare invocation adopt the corrected exit code as an explicitly-flagged breaking change. Not in this PR.


Verified on this base

Scenario Result
missing graph + --headless --json exit 1, errors:["GraphFileNotFound"]
unchanged + --incremental exit 3 empty_delta
modified + --incremental exit 0, updated=1 ← change-based
re-run (idempotency) exit 3
bare graq rebuild, missing graph exit 0 ← legacy contract preserved

914 tests pass across tests/test_core/ + tests/test_cli/. The 10 failures in test_gate_install.py / test_g5_vscode_gate_install.py are pre-existing — verified by checking out c51d1f26 (this PR's base, without the commit) and reproducing the identical 10. They reference none of these modules.

Trade-secret screen: 0 hits.

Verified that the R6 changes are byte-identical to the merged private commit; the only graph.py differences between the two repos are the monetisation W3 reasoning-quota lines, which are private-only and untouched here.

Rollback

Revert the single commit. headless.py is a new module and all wiring is flag-gated, so reverting restores exact prior behaviour. _chunk_content_hash is an additive node property ignored by every prior version — no data-loss risk.


🤖 Generated with Claude Code

`graq rebuild` returns an int that Typer discards, so every invocation exits 0
— including "Graph file not found". Measured: a missing graph prints an error
in red and exits 0, so an unattended scheduler cannot detect the failure at
all. R6's acceptance criterion is that a failed step be distinguishable from an
empty delta by exit code alone; today they are both 0.

Adds graqle/cli/headless.py as the one place the machine contract is defined:
0 success, 1 failure, 2 usage, 3 empty delta. EMPTY_DELTA is deliberately
distinct from SUCCESS so a scheduler can also gate downstream work on whether
anything actually changed. RunReport is frozen and derives exit_code from
status, so a serialized report can never carry a pair that disagree; errors
carry exception TYPE names only, inheriting the PII rule already enforced on
.graqle/govern.health.json.

--headless and --json stay orthogonal: --headless is about interactivity,
--json about format. Coupling them would contradict the standalone --json
convention already used at 15+ sites and hand JSON to scripts that never asked
for it.

Incremental rebuild becomes change-based. rebuild_chunks previously skipped any
node that already had chunks, so a node whose source was edited kept stale
evidence — a correctness gap, not just a perf one. --incremental compares a
stored SHA-256 of the source content. Content, not mtime: git checkouts, CI
clones and Docker layer caching all rewrite mtime, which is exactly where a
scheduler runs. Graphs with no stored hash fall back to the old behaviour.

Backwards compatibility: the corrected exit codes apply only when a machine
flag is passed. Bare `graq rebuild` behaves byte-for-byte as before and gets a
stderr DeprecationWarning naming the future change. `govern serve --once`
without --json is untouched.

Sentinel (graq_reason, 2 passes; graq_review is down). Pass 1 raised 4
BLOCKERs; each was reproduced before being fixed:
  - BLOCKER-1 REAL: a run where every node failed reported empty_delta/exit 3
    with errors:[] — "healthy, nothing to do" while nothing worked. Fixed:
    rebuild_chunks counts failures into Graqle.last_rebuild_failed_nodes and
    any failure now yields FAILURE.
  - BLOCKER-2 REFUTED: the two property writes are adjacent in-memory dict
    assignments with no I/O between them, and the worst case (hash missing)
    re-chunks next run rather than skipping stale evidence.
  - BLOCKER-3 REFUTED by measurement: bare rebuild updating 3 nodes exits 0,
    not 3 — Typer 0.24.1 discards the return value. That IS the defect here.
  - BLOCKER-4 REFUTED by measurement on win32: NamedTemporaryFile is used as a
    context manager, so the handle closes before os.replace. Report written,
    zero orphan tempfiles.
Pass 2 found a further real BLOCKER: partial failure (some nodes rebuilt, some
failed) fell into the "updated" branch and exited 0. A scheduler routes on the
exit code, so that is the same silent success in a smaller costume — any
failure is now a FAILURE.

graq_predict then surfaced a defect neither pass caught: rebuild_command is
also called programmatically by `graq init`, where unfilled typer.Option
defaults arrive as truthy OptionInfo sentinels. Those wrongly selected machine
mode and then raised TypeError on Path(OptionInfo), which would have broken
init's auto-rebuild. Flags are now normalised before use. Verified the direct
call returns a plain int again.

Also ships cron / Airflow / GitHub Actions reference recipes, as the
requirement asks.

Tests: 913 passed across tests/test_core + tests/test_cli. The 10 failures in
test_gate_install.py / test_g5_vscode_gate_install.py are pre-existing —
verified by stashing this branch and reproducing them on untouched
private/master; they reference none of these modules.

TS-screen: 0 hits.
Plan: plan_b48cef6f
CR: .gsm/external/Change Requests/CR-010.R6-scheduler-pipeline-contract.md
(cherry picked from commit 95813097d9a15fbcf538a8881df6e40af4d13268)
@github-actions

Copy link
Copy Markdown

🛡️ GraQle PR Guardian

💥 Blast Radius: 7 modules affected

Module Files Changed Risk Level Impact Radius
docs 1 🟡 T2 1
graqle 4 🟡 T2 4
tests 2 🟠 T3 2

Total blast radius: 7


🏛️ Governance Verdict

🚫 FAIL

  • T3: 1 file(s) require explicit approval.

🔍 SHACL Violations

No SHACL violations detected.


🔐 Approval Requirements

This PR requires approval from: T3

  • A registered Tech Lead or Governance Admin must approve.

Approval requirement NOT yet satisfied.


Metric Value
Blast Radius 7
Files Analyzed 7
Blocked 1
SHACL Violations 0
Verdict FAIL

🔬 Powered by GraQle PR Guardian v0.1.0 · Scan completed 2026-07-29T20:18:28.085511+00:00

@harishquantamix
harishquantamix merged commit 21c9191 into master Jul 29, 2026
19 of 20 checks passed
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.

2 participants