# History-anatomy pipeline — daily/ad-hoc diagnostic report over already-imported history
# (feature I8, HA-S1 0660 / ADR-079).
#
# Orchestration is configuration (ADR-022): a state machine over the existing dual-workflow
# engine. The YAML owns ONLY the cache branch, deterministic stage ordering, executor dispatch,
# bounded correction, and atomic publication sequencing. The deterministic cache/digest/
# structure/publish work is 0659's dependency-free helper script; the judgment (mode validity,
# what counts as evidence, the report contract, enrich/validate rubrics) is 0658's skill. Per
# ADR-069 R1, cache-decide / digest / structure-check / publish must NOT be inline shell — every
# shell action here is glue length (a single helper invocation), because each of those programs
# would exceed the shell composition threshold and be flagged as an owned-capability candidate.
#
# Shape:
#   start -> resolve-scope -> resolve-paths -> analyze -> cache-probe
#            (hit  -> refresh-provenance -> publish -> published)
#            (miss -> render -> enrich -> structure-gate -> validate -> stamp -> publish)
#            (structure-gate FAIL -> correct (max 2 total) -> structure-gate; exhausted -> failed)
#            (validate FAIL -> correct (max 2 total) -> structure-gate ...; exhausted -> failed)
#            (any all-paths failure -> failed)
#
# `analyze` precedes `cache-probe` deliberately: ADR-079 makes cache validity a DERIVED fact, so
# the semantic digest must come from a fresh analyze, never from the cached report being judged.
# The deterministic half is therefore never cached — only the model-authored half is reused.
#
# terminalStates: [published, failed]. Publication is reachable ONLY via `stamp` (guarded on a
# passing validation) or `refresh-provenance` (whose model half was itself published through a
# passing validation) — there is no edge into `publish` from structure-gate or enrich directly.
#
# Vars (all must be declared here — the vars: block is the only safe shape; `spur workflow
# validate` and the skill-structure test enforce it):
#   mode      — "daily" (default) | "ad-hoc" (validated in resolve-scope)
#   date      — YYYY-MM-DD for --date (daily); empty for ad-hoc
#   since     — inclusive ISO lower bound (ad-hoc; also normalizes daily via the DST-aware rule)
#   until     — inclusive ISO upper bound (ad-hoc)
#   baselineSince — declared home for the baseline-leg lower bound the analyze stage references;
#               actual values flow through the run-scoped env file emitted by resolve-paths
#               (0674 R2/R5 — a referenced var must have a declared home, never an implicit one)
#   baselineUntil — baseline-leg upper bound, same contract as baselineSince
#   focus     — ad-hoc focus string (required in ad-hoc mode, rejected in daily)
#   recompute — "true" forces the full analyze/render/enrich/validate path (cache disposition
#               forced-recompute)
#   output    — explicit report output path; default run directory
#   agent     — executor for the agent.run stages (enrich/validate)
#   spurBin   — PATH-independent spur invocation (overridden by CLI at run start)
#   __runId   — run-scoped id for explicit artifact paths (allocated in start)
#   stepTimeoutMs — agent.run budget for enrich/validate

"$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
kind: state-machine
name: history-anatomy
version: "1"
description: "Daily/ad-hoc history-anatomy report: cache branch, deterministic analyze/render, skill enrichment, deterministic structure gate, independent evidence validation, bounded correction, atomic publication"
iterationBound: 20
initialState: start
terminalStates:
  - published
  - failed
failureStates:
  - failed
vars:
  mode: "daily"
  date: ""
  since: ""
  until: ""
  baselineSince: ""
  baselineUntil: ""
  focus: ""
  recompute: "false"
  output: ""
  # 0676 R5: omp went quota-dead (HTTP 429) in the 2026-08-25 dogfood. The literal names a
  # currently-reachable executor; `agent.default` in config still overrides via the precedence chain.
  agent: "claude"
  spurBin: "spur"
  __runId: ""
  # 2026-09-12: raised 30m -> 60m. Observed runs on zai/glm-5.3-flash butt against the
  # 30m ceiling (correct exited at exactly 30m00s; a prior run passed at 29m52s) — the
  # enrich/validate/correct stages legitimately need >30m for the 60KB candidate report.
  stepTimeoutMs: "3600000"
  workflowFile: "config/workflows/history-anatomy.yaml"
  contractVersion: "1"
  reportDir: "docs/report"

states:
  - id: start
    description: >
      Allocate the run-scoped id and root. Every analyze/render path in this workflow writes to
      an explicit unique path under this run id — no stage reads the mutable latest.json pointer.
    onEnter:
      - kind: shell
        options:
          command: >-
            mkdir -p .spur/run;
            if [ -z "$__runId" ]; then __runId=$(uuidgen | tr 'A-Z' 'a-z' | cut -c1-8); fi;
            echo "$__runId" > .spur/run/history-anatomy-run.id

  - id: resolve-scope
    description: >
      Dispatch the skill's mode validation (0658 references/modes.md): daily is the default and
      rejects focus/since/until/output; ad-hoc requires a non-empty focus plus two ordered
      inclusive bounds and rejects --date/--recompute. Writes the normalized selector artifact.
    onEnter:
      - kind: agent.run
        options:
          agent: ${vars.agent}
          input: "Validate the mode arguments per sp:history-anatomy references/modes.md and write the normalized selector to .spur/run/${vars.__runId}-selector.json. mode=${vars.mode} date=${vars.date} since=${vars.since} until=${vars.until} focus=${vars.focus}. Reject conflicting arguments, naming the offending one; a rejection marks the run failed."
          # Declared Layer-1 role (0538 R2): mode validation is a reviewer-standard judgment.
          role: reviewer
          expectFile: .spur/run/${vars.__runId}-selector.json
          timeoutMs: ${vars.stepTimeoutMs}

  - id: resolve-paths
    description: >
      Resolve the helper, the skill/contract logic paths, and the publication target once, into a
      run-scoped env file every later stage sources. Keeps each downstream shell action at glue
      length (ADR-069 R1) instead of repeating path arithmetic per stage.
    onEnter:
      - kind: shell
        options:
          # Path arithmetic lives in the helper (ADR-069 R1); the shell only locates it. Avoid
          # `${...}` here — that syntax collides with the engine's own template interpolation.
          command: >-
            h=$(superskill script path sp history-anatomy-cache.mjs);
            node "$h" paths --helper "$h" --report-dir "$reportDir" --date "$date"
            --output "$output" --mode "$mode" --since "$since" --until "$until"
            --out .spur/run/$__runId-paths.txt

  - id: analyze
    description: >
      Deterministic analyze of the current window and the immediately preceding comparable
      window, each to an explicit run-scoped path. Always reruns — the deterministic half is
      never cached (ADR-079).
    onEnter:
      - kind: shell
        options:
          command: >-
            mkdir -p .spur/run;
            . .spur/run/$__runId-paths.txt;
            $spurBin history analyze --out .spur/run/$__runId-history-anatomy-current.json --since "$HA_SINCE" --until "$HA_UNTIL" --json
      - kind: shell
        options:
          command: >-
            . .spur/run/$__runId-paths.txt;
            $spurBin history analyze --out .spur/run/$__runId-history-anatomy-baseline.json --since "$HA_BASELINE_SINCE" --until "$HA_BASELINE_UNTIL" --json 2>/dev/null || true

  - id: cache-probe
    description: >
      Deterministic cache probe via the helper (0659). Runs AFTER analyze because ADR-079 makes
      validity a derived fact: the semantic digest must come from the fresh artifact, never from
      the cached report being judged. A hit skips only enrichment, never the probe. Daily only;
      ad-hoc always reports miss (`ad-hoc-never-cached`). Also writes the run's full provenance.
    onEnter:
      - kind: shell
        options:
          command: >-
            . .spur/run/$__runId-paths.txt;
            node "$HA_HELPER" probe --artifact .spur/run/$__runId-history-anatomy-current.json
            --baseline .spur/run/$__runId-history-anatomy-baseline.json --target "$HA_TARGET"
            --mode "$mode" --date "$HA_DATE" --recompute "$recompute" --executor "$agent"
            --skill-dir "$HA_SKILL" --contract "$HA_SKILL/references/report-contract.md"
            --workflow "$workflowFile" --helper "$HA_HELPER" --contract-version "$contractVersion" --run-id "$__runId"
            --out .spur/run/$__runId-provenance.json
            > .spur/run/$__runId-cache-disposition.txt

  - id: render
    description: >
      Render both artifacts with report --mode forensics, naming the exact analyze paths.
    onEnter:
      - kind: shell
        options:
          command: >-
            mkdir -p .spur/run;
            $spurBin history report .spur/run/$__runId-history-anatomy-current.json --mode forensics > .spur/run/$__runId-history-anatomy-current.md;
            $spurBin history report .spur/run/$__runId-history-anatomy-baseline.json --mode forensics > .spur/run/$__runId-history-anatomy-baseline.md || true

  - id: enrich
    description: >
      Model enrichment via the skill operation sp:history-anatomy enrich (0658 operations.md).
      Consumes the two artifacts; authors the model half of the report. A porcelain baseline
      is captured before dispatch and asserted after: any working-tree file created outside the
      declared output fails the run (0676 R3).
    onEnter:
      - kind: shell
        options:
          command: >-
            git status --porcelain > .spur/run/$__runId-baseline-enrich.txt || true
      - kind: agent.run
        options:
          agent: ${vars.agent}
          input: "Run sp:history-anatomy enrich: given .spur/run/${vars.__runId}-history-anatomy-current.json and -baseline.json, author the model half of the report (Baseline comparison, Findings, Recurrence ledger, Remediation options, Performance analysis, Workflow/process improvements, Positive patterns) to .spur/run/${vars.__runId}-candidate.md per references/report-contract.md. This operation never launches a workflow."
          # Declared Layer-1 role (0538 R2): enrichment is model judgment.
          role: reviewer
          expectFile: .spur/run/${vars.__runId}-candidate.md
          timeoutMs: ${vars.stepTimeoutMs}
      - kind: shell
        options:
          command: >-
            node "$(superskill script path sp history-anatomy-cache.mjs)" assert-clean --baseline .spur/run/$__runId-baseline-enrich.txt --expect .spur/run/$__runId-candidate.md

  - id: structure-gate
    description: >
      Deterministic structure gate via the helper (0659). Asserts the twelve sections in order,
      per-finding fields, no placeholders, evidence anchors. Not reachable on the hit path from
      enrich — always gated before validation.
    onEnter:
      - kind: shell
        options:
          command: >-
            node "$(superskill script path sp history-anatomy-cache.mjs)" check \
              .spur/run/$__runId-candidate.md > .spur/run/$__runId-structure-gate.txt 2>&1 || true

  - id: validate
    description: >
      Independent evidence validation via the skill operation sp:history-anatomy validate.
      Publication is reachable only from a PASS here. Same undeclared-write assertion as enrich (0676 R3).
      The publish guard reads the ANCHORED FINAL LINE (0771): only a validation artifact whose last
      line is exactly `Verdict: PASS` publishes — a leading PASS under a later FAIL or a
      `not Verdict: PASS` line can never satisfy it.
    onEnter:
      - kind: shell
        options:
          command: >-
            git status --porcelain > .spur/run/$__runId-baseline-validate.txt || true
      - kind: agent.run
        options:
          agent: ${vars.agent}
          input: "Run sp:history-anatomy validate: independently check .spur/run/${vars.__runId}-candidate.md against the two artifacts per references/operations.md; write Verdict: PASS or FAIL to .spur/run/${vars.__runId}-validation.txt. This operation never launches a workflow."
          # Declared Layer-1 role (0538 R2): independent verification.
          role: reviewer
          expectFile: .spur/run/${vars.__runId}-validation.txt
          timeoutMs: ${vars.stepTimeoutMs}
      - kind: shell
        options:
          # 2026-09-13 verdict-placement normalization: the 0771 guard reads ONLY the
          # final line, but models sometimes lead with `Verdict: PASS` and close with
          # prose — a substantive PASS must not die as a format phantom (observed
          # 2026-09-13 run 94c4d6dc). Moves the verdict line to the end ONLY when
          # exactly one exact-line verdict exists and it is PASS. Any `Verdict: FAIL`
          # line anywhere (or multiple/ambiguous verdict lines) suppresses
          # normalization, so the guard still fails and a real FAIL can never be
          # laundered into a PASS.
          command: >-
            f=.spur/run/$__runId-validation.txt;
            if [ -f "$f" ] && ! tail -n 1 "$f" | grep -qx 'Verdict: PASS'; then
              p=$(grep -cx 'Verdict: PASS' "$f"); q=$(grep -cx 'Verdict: FAIL' "$f");
              if [ "$p" = 1 ] && [ "$q" = 0 ]; then
                grep -vx 'Verdict: PASS' "$f" > "$f.tmp" && printf 'Verdict: PASS\n' >> "$f.tmp" && mv "$f.tmp" "$f";
              fi;
            fi
      - kind: shell
        options:
          command: >-
            node "$(superskill script path sp history-anatomy-cache.mjs)" assert-clean --baseline .spur/run/$__runId-baseline-validate.txt --expect .spur/run/$__runId-validation.txt

  - id: correct
    description: >
      A two-pass correction budget (0690). onEnter increments the counter; retry edges guard on
      the run-scoped correction-count file
      `.spur/run/$__runId-correction-count` < 2 (the live bound — the
      former `vars.correctionCount` declared-but-unread var was removed,
      0702 R3), and a failure after the second repair takes the -> failed
      edge. The
      counter is shared by the structure-gate and validate FAIL edges, so a structure repair can
      still be followed by one validation-driven repair. The
      model half re-authors the candidate in place from the gate findings and validation notes —
      re-anchoring evidence rows to a backticked .md/.ts/.json path or path:line, stripping
      placeholders, restoring the canonical twelve-section order, and reconciling quantitative
      claims against the current/baseline artifacts — before the deterministic gate re-runs.
    onEnter:
      - kind: shell
        options:
          command: >-
            n=$(cat .spur/run/$__runId-correction-count 2>/dev/null || echo 0) &&
            printf '%s\n' "$((n + 1))" > .spur/run/$__runId-correction-count
      - kind: shell
        options:
          command: >-
            git status --porcelain > .spur/run/$__runId-baseline-correct.txt || true
      - kind: agent.run
        options:
          agent: ${vars.agent}
          input: "Repair .spur/run/${vars.__runId}-candidate.md in place. Read its structure-gate findings, optional validation notes, references/report-contract.md, and the references/operations.md validate rubric; fix every violation. Use a backticked `.md`/`.ts`/`.json` path or a `path:line` anchor, never `current #/...`; remove placeholders and empty rows; restore the twelve-section order; ensure every problem and positive finding carries the full field set; verify every quantitative claim (counts, durations, percentages, distributions) and every current/baseline full-digest match against .spur/run/${vars.__runId}-history-anatomy-current.json and -baseline.json. Overwrite only the candidate; never launch a workflow."
          # Declared Layer-1 role (0538 R2): repair reuses enrich-class judgment.
          role: reviewer
          expectFile: .spur/run/${vars.__runId}-candidate.md
          timeoutMs: ${vars.stepTimeoutMs}
      - kind: shell
        options:
          command: >-
            node "$(superskill script path sp history-anatomy-cache.mjs)" assert-clean --baseline .spur/run/$__runId-baseline-correct.txt --expect .spur/run/$__runId-candidate.md

  - id: refresh-provenance
    description: >
      Cache-hit path. Keeps the published model half verbatim and refreshes only validated_at,
      the cache disposition, and the imported-snapshot banner — which is the EARLIEST per-source
      lastImportedAt, so the report never claims a source was imported after its own recorded
      timestamp. Emits the publishable artifact the shared publish state consumes.
    onEnter:
      - kind: shell
        options:
          command: >-
            . .spur/run/$__runId-paths.txt;
            node "$HA_HELPER" refresh --report "$HA_TARGET"
            --out .spur/run/$__runId-publishable.md --disposition hit

  - id: stamp
    description: >
      Cache-miss path. Attaches the full R7 frontmatter provenance block (identity tuple, window
      state, generated/validated timestamps, artifact paths + digests, contract/skill/workflow
      digests, per-source coverage + last_imported_at, spur/schema version, executor, run id,
      cache disposition) and the freshness banner to the validated candidate.
    onEnter:
      - kind: shell
        options:
          command: >-
            . .spur/run/$__runId-paths.txt;
            node "$HA_HELPER" stamp --candidate .spur/run/$__runId-candidate.md
            --provenance .spur/run/$__runId-provenance.json
            --out .spur/run/$__runId-publishable.md

  - id: publish
    description: >
      Atomic publication via the helper (0659). Reachable only from `stamp` (which is reachable
      only from a passing validate) or from `refresh-provenance` on the cache-hit path — the
      model half a hit reuses was itself published through a passing validation.
    onEnter:
      - kind: shell
        options:
          command: >-
            . .spur/run/$__runId-paths.txt;
            mkdir -p "$(dirname "$HA_TARGET")";
            node "$HA_HELPER" publish .spur/run/$__runId-publishable.md "$HA_TARGET"

  - id: published
    description: Terminal — report published atomically after a passing validation.

  - id: failed
    description: Terminal — mode rejection, gate failure, or validation failure not corrected.

transitions:
  - from: start
    to: resolve-scope
    description: Scope resolved.
    guard:
      kind: always

  - from: resolve-scope
    to: resolve-paths
    description: Mode valid — resolve helper/skill/target paths once.
    guard:
      kind: always

  - from: resolve-paths
    to: analyze
    description: Paths resolved — always rerun the deterministic half (ADR-079).
    guard:
      kind: always

  - from: analyze
    to: cache-probe
    description: Fresh artifacts on disk — derive the digest and decide reuse.
    guard:
      kind: always

  # Daily only takes the hit branch; ad-hoc always regenerates.
  - from: cache-probe
    to: refresh-provenance
    description: Cache hit (daily) — refresh provenance and publish without re-enrichment.
    guard:
      kind: shell
      options:
        command: 'test "$mode" = daily && grep -q "^hit$" .spur/run/$__runId-cache-disposition.txt 2>/dev/null'
  - from: cache-probe
    to: render
    description: Cache miss, forced recompute, or ad-hoc — render and re-enrich.
    guard:
      kind: always

  - from: refresh-provenance
    to: publish
    description: Provenance refreshed — publish the cached model half.
    guard:
      kind: always

  - from: render
    to: enrich
    description: Both artifacts rendered — enrich.
    guard:
      kind: always

  - from: enrich
    to: structure-gate
    description: Candidate authored — deterministic structure gate.
    guard:
      kind: always

  - from: structure-gate
    to: validate
    description: Candidate structurally sound — independent evidence validation.
    guard:
      kind: shell
      options:
        command: 'grep -q "^PASS$" .spur/run/$__runId-structure-gate.txt 2>/dev/null'
  - from: structure-gate
    to: failed
    description: Structure gate FAIL with the correction cap exhausted — never publish a malformed candidate.
    guard:
      kind: shell
      options:
        command: '! grep -q "^PASS$" .spur/run/$__runId-structure-gate.txt 2>/dev/null && test "$(cat .spur/run/$__runId-correction-count 2>/dev/null || echo 0)" -ge 2'
  - from: structure-gate
    to: correct
    description: Structure gate FAIL under the shared two-pass correction cap (0690).
    guard:
      kind: always

  - from: validate
    to: stamp
    description: Independent validation PASS (anchored final line, 0771) — stamp provenance, then publish atomically.
    guard:
      kind: shell
      options:
        command: 'tail -n 1 .spur/run/$__runId-validation.txt 2>/dev/null | grep -qx "Verdict: PASS"'
  - from: validate
    to: correct
    description: Validation FAIL under the shared two-pass correction cap.
    guard:
      kind: shell
      options:
        command: '! tail -n 1 .spur/run/$__runId-validation.txt 2>/dev/null | grep -qx "Verdict: PASS" && test "$(cat .spur/run/$__runId-correction-count 2>/dev/null || echo 0)" -lt 2'
  - from: validate
    to: failed
    description: Validation FAIL with the correction cap exhausted — terminate without publishing.
    guard:
      kind: always

  - from: correct
    to: structure-gate
    description: Corrected candidate re-enters the deterministic gate, then re-validates.
    guard:
      kind: always

  - from: stamp
    to: publish
    description: Provenance stamped — publish atomically.
    guard:
      kind: always

  - from: publish
    to: published
    description: Report published.
    guard:
      kind: always
