name: CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
  # Merge-skew guard (issue #366): also run the required gates against the merge queue's
  # PROSPECTIVE merged commit (GitHub Actions `merge_group` event). Several gates assert a whole-repo invariant (migration prefixes,
  # BPMN DI freshness, committed generated artifacts) that two PRs can each satisfy in isolation yet
  # violate once BOTH land on `main`. Validating the speculative merge commit the queue builds — not
  # the stale PR head — blocks such a merge categorically instead of letting it poison the next PR.
  merge_group:

permissions:
  contents: read

jobs:
  typecheck:
    name: typecheck + test (Node)
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          # Full history + tags: the migration immutability gate (check:migrations) diffs against the
          # merge-base with origin/main, and the upgrade smoke test materialises the previous release
          # tag's migration set — both need history a shallow clone doesn't have (issue #357).
          fetch-depth: 0

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "24"

      - name: Install dependencies
        run: npm ci

      - name: Lint (Biome)
        run: npm run lint

      - name: Typecheck (Node / tsc)
        run: npm run typecheck

      - name: Validate manifest (urban check)
        run: npm run check

      # DI-freshness gate: the `.bpmn` semantic model is authoritative and the
      # `bpmndi:BPMNDiagram` is GENERATED (scripts/layout-bpmn.ts → layoutBpmn). This regenerates
      # the DI in memory and fails if any committed diagram is stale — i.e. a flow was changed
      # without re-running `npm run layout`. Keeps the diagram from drifting off the model.
      - name: Check BPMN diagram freshness (layout)
        run: npm run layout:check

      # Deploy-safety gate: every model-authored `{{template}}` agent-prompt header must resolve
      # to a declared, non-blank prompt (and no agent prompt may ship blank). A broken/blank token
      # means a prompt-less agent — the root of the empty escalations on nano-bpm #597/#599.
      - name: Check agent prompt templates
        run: npm run check:prompts

      # Merge-safety gate: migrations are applied in numeric-prefix order, but each fan-out branch
      # picks "the next" prefix independently, so parallel siblings silently collide on one slot
      # (the historical 004/005/006/007 pairs; recurred on epic #142). This fails the build on any
      # new duplicate prefix so the collision can't merge unnoticed.
      - name: Check migration prefixes (no collisions)
        run: npm run check:migrations

      # Contract-registry gate (issue #227): every config-family env key must be declared in the ONE
      # typed schema (app/contracts.ts), no retired synonym (e.g. NANO_PR_BASE_URL, #223) may reappear
      # in code, and the registry must reconcile against itself (no synonyms/contradictions). This is
      # what makes a duplicate/synonymous contract a build failure instead of a silent runtime fallback.
      - name: Check contract registry (no synonyms / undeclared keys)
        run: npm run check:contracts

      # MCP tool-schema gate (epic #605, S0): every projected (non-`x-mcp`) request-body operation
      # must carry a self-contained, `$ref`-free inline `body` schema — the runtime projector copies
      # it VERBATIM into the tool `inputSchema`, so a leaked `$ref` is unresolvable in a standard MCP
      # client (nano-ide#502). The inline bodies are DERIVED from `components.schemas` by
      # scripts/inline-mcp-bodies.ts; this fails if a source component changed without regenerating.
      - name: Check MCP tool-body schemas (inline, $ref-free)
        run: npm run check:mcp-bodies

      # Runs the full *.test.ts suite under Node's built-in test runner (node:test), which strips
      # TypeScript types on the fly (Node >= 22.6) — no build step.
      - name: Test (Node)
        run: npm test

      # End-to-end pilot (nano-ide issue #157, S3): boots the whole app in-process against the WASM
      # engine + a virtual clock via @nanobpm/urban-testkit and drives the real /app/api operations.
      # Hermetic (no socket, no GitHub network), so it runs on every push like the unit suite.
      - name: E2E (urban-testkit)
        run: npm run e2e

  # FINAL regression guard for epic nano-ide#314 (S6, #321): the compounding oracle that keeps the
  # code-first (`defineFlow`) and model-first (`.bpmn`) representations of the nano-workforce corpus
  # in lockstep. For every model it DERIVES the BPMN from its defineFlow port, structurally DIFFS it
  # against the checked-in golden via the S0 harness (@nanobpm/workflow/test-support), and DEPLOYS
  # the derived model to the in-process @nanobpm/engine-wasm engine — failing on any structural drift
  # OR deploy rejection. Parked models (awaiting an upstream construct) must each carry a documented
  # blocker, and a self-proving canary asserts the oracle's red path genuinely fires so the gate can
  # never rot into a vacuous green while the corpus is parked. Hermetic (in-process wasm engine, no
  # sockets), so it runs on every PR/push as its own job.
  # install.sh onboarding script (issues #576 + #583): POSIX-sh cleanliness + a
  # hermetic smoke test asserting the emitted command sequence (phase 1) and the
  # phase-2 console call flow (dry-run + a stubbed-console live run). The script
  # is served from `main` for `curl … | sh`, so a broken change is live the
  # instant it merges — this gate keeps that surface honest.
  install-script:
    name: install.sh (shellcheck + dry-run smoke)
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "24"

      - name: shellcheck (POSIX sh)
        run: shellcheck -s sh install.sh test/install-smoke.sh

      - name: Smoke test (dry-run sequence + stubbed-console phase 2)
        run: sh test/install-smoke.sh

  derivation-parity:
    name: derivation parity (nwf corpus regression guard)
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "24"

      - name: Install dependencies
        run: npm ci

      - name: Derive + diff + deploy the full nwf corpus
        run: npm run check:derivation-parity

