name: Validation

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  validate:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Setup Node (pin from .nvmrc)
        uses: actions/setup-node@v5
        with:
          node-version-file: .nvmrc

      - name: Install validator-tooling dependencies (root)
        # Root package.json provides js-yaml for scripts/check-frontmatter-yaml.mjs
        # (used by lint-skills-frontmatter). The docs-site deps install separately in site/.
        run: npm ci

      - name: Install Astro/Starlight dependencies
        working-directory: site
        run: npm ci

      - name: Generate site content
        # Generated content is gitignored and rebuilt each build, so it is absent
        # on a fresh checkout. Generate it here (zero-dependency Node, no node_modules
        # needed) so the content-scanning validators below see the full tree.
        run: node scripts/gen-site.mjs

      - name: Check resource index is current
        # docs/RESOURCES.md is generated by scripts/gen-resource-index.mjs and
        # committed. This fails the build if a resource was added/removed without
        # regenerating the index, or if any live route / repo source link is dead.
        # Cross-platform Node script: runs on both OS legs (also exercises CRLF/LF).
        run: node scripts/gen-resource-index.mjs --check

      - name: Check skill manifest is current (enforcing)
        # skill-manifest.json is generated from skills/*/SKILL.md frontmatter by
        # scripts/gen-skill-manifest.mjs and committed (M-32, spec_derived-surfaces).
        # Enforcing from day one: deterministic, brand-new artifact, resource-index
        # precedent (spec decision D-C). Needs root js-yaml (npm ci above).
        run: node scripts/gen-skill-manifest.mjs --check

      - name: Check AGENTS.md skills catalog is current (enforcing)
        # The block between the skills-catalog markers in AGENTS.md is generated
        # from the same manifest data (--agents). A frontmatter description edit
        # without regeneration fails here; hand edits inside the markers fail here.
        run: node scripts/gen-skill-manifest.mjs --agents --check

      - name: Validate site doc frontmatter YAML (enforcing)
        # Parse-checks frontmatter in tracked Astro doc bodies (skills front matter
        # is covered separately by lint-skills-frontmatter). Catches the unquoted
        # colon-in-value defect early with a clear message, instead of a cryptic
        # Astro build crash. Cross-platform Node; runs on both OS legs.
        run: node scripts/check-frontmatter-yaml.mjs --site-docs

      - name: Run resource-index unit tests
        run: node --test scripts/gen-resource-index.test.mjs scripts/check-emdash-scars.test.mjs

      - name: Check for em-dash-sweep scars in user-facing prose (enforcing)
        # Flags the " . " (space-period-space) scar that an earlier em-dash sweep
        # (commit 8ab0f81) left behind. The no-em-dashes hook blocks em-dash
        # characters but not this swept-to-period residue. Enforcing: the
        # user-facing corpus was swept clean in v2.25.1, and the check skips both
        # fenced and inline code, so any finding is a real new scar. Scope is
        # tracked hand-authored prose; generated + internal docs are excluded.
        # Cross-platform Node; runs on both OS legs.
        run: node scripts/check-emdash-scars.mjs

      - name: Validate commands (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-commands.sh

      - name: Validate commands (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-commands.ps1

      - name: Validate AGENTS.md sync (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-agents-md.sh

      - name: Validate AGENTS.md sync (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-agents-md.ps1

      - name: Lint skills front matter (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/lint-skills-frontmatter.sh

      - name: Lint skills front matter (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/lint-skills-frontmatter.ps1

      - name: Check skill sample coverage (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/check-skill-sample-coverage.sh

      - name: Check skill sample coverage (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-skill-sample-coverage.ps1

      - name: Validate meeting-skills-family contract (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-meeting-skills-family.sh

      - name: Validate meeting-skills-family contract (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-meeting-skills-family.ps1

      - name: Validate foundation-sprint-skills-family contract (bash, --strict)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-foundation-sprint-skills-family.sh --strict

      - name: Validate foundation-sprint-skills-family contract (pwsh, -Strict)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-foundation-sprint-skills-family.ps1 -Strict

      - name: Validate design-sprint-skills-family contract (bash, --strict)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-design-sprint-skills-family.sh --strict

      - name: Validate design-sprint-skills-family contract (pwsh, -Strict)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-design-sprint-skills-family.ps1 -Strict

      - name: Check MCP impact (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/check-mcp-impact.sh
        continue-on-error: true

      - name: Check MCP impact (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-mcp-impact.ps1
        continue-on-error: true

      - name: Check CONTEXT.md currency (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/check-context-currency.sh
        continue-on-error: true

      - name: Check CONTEXT.md currency (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-context-currency.ps1
        continue-on-error: true

      - name: Validate skill HISTORY.md (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-skill-history.sh
        continue-on-error: true

      - name: Validate skill HISTORY.md (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-skill-history.ps1
        continue-on-error: true

      - name: Validate skills manifest (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-skills-manifest.sh
        continue-on-error: true

      - name: Validate skills manifest (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-skills-manifest.ps1
        continue-on-error: true

      - name: Validate version consistency (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-version-consistency.sh

      - name: Validate version consistency (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-version-consistency.ps1
      - name: Validate Codex manifest (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-codex-manifest.sh

      - name: Validate Codex manifest (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-codex-manifest.ps1

      - name: Validate plugin install path (bash, enforcing)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-plugin-install.sh

      - name: Validate plugin install path (pwsh, enforcing)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-plugin-install.ps1

      - name: Validate _pm-skills/ in .gitignore (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-gitignore-pm-skills.sh
        continue-on-error: true

      - name: Validate _pm-skills/ in .gitignore (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-gitignore-pm-skills.ps1
        continue-on-error: true

      - name: Validate script documentation (bash, enforcing)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-script-docs.sh

      - name: Validate script documentation (pwsh, enforcing)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-script-docs.ps1

      - name: Check workflow coverage (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/check-workflow-coverage.sh
        continue-on-error: true

      - name: Check workflow coverage (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-workflow-coverage.ps1
        continue-on-error: true

      - name: Check count consistency (bash)
        if: matrix.os == 'ubuntu-latest'
        run: bash scripts/check-count-consistency.sh

      - name: Check count consistency (pwsh)
        if: matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-count-consistency.ps1

      - name: Check version references (bash)
        if: always() && matrix.os == 'ubuntu-latest'
        run: bash scripts/check-version-references.sh
        continue-on-error: true

      - name: Check version references (pwsh)
        if: always() && matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-version-references.ps1
        continue-on-error: true

      - name: Validate docs frontmatter (bash, enforcing)
        if: always() && matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-docs-frontmatter.sh --strict

      - name: Validate docs frontmatter (pwsh, enforcing)
        if: always() && matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-docs-frontmatter.ps1 -Strict

      - name: Validate skill family registration (bash, enforcing)
        if: always() && matrix.os == 'ubuntu-latest'
        run: bash scripts/validate-skill-family-registration.sh

      - name: Validate skill family registration (pwsh, enforcing)
        if: always() && matrix.os == 'windows-latest'
        run: pwsh -File scripts/validate-skill-family-registration.ps1

      # Filesystem link validators (check-internal-link-validity,
      # validate-references-cross-doc) were retired in the Pattern S migration:
      # they resolve .md links on disk, which cannot represent the cross-target
      # links (../../skills/SKILL.md, ../internal/, _workflows/) that now resolve
      # only at build time via scripts/remark-resolve-links.mjs. The build-aware
      # "Check rendered link resolution" step below validates the real dist and is
      # a strict superset.

      - name: Check no body H1 duplication (bash, enforcing)
        if: always() && matrix.os == 'ubuntu-latest'
        run: bash scripts/check-no-body-h1.sh --strict

      - name: Check no body H1 duplication (pwsh, enforcing)
        if: always() && matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-no-body-h1.ps1 -Strict

      - name: Check skill cross-references (bash, enforcing)
        if: always() && matrix.os == 'ubuntu-latest'
        run: bash scripts/check-skill-cross-references.sh

      - name: Check skill cross-references (pwsh, enforcing)
        if: always() && matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-skill-cross-references.ps1

      # v2.15.1 preventive validators - close audit findings A01/A02/A03/A04
      - name: Check landing-page counts (bash, enforcing, --strict)
        if: always() && matrix.os == 'ubuntu-latest'
        run: bash scripts/check-landing-page-counts.sh --strict

      - name: Check landing-page counts (pwsh, enforcing, -Strict)
        if: always() && matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-landing-page-counts.ps1 -Strict

      - name: Check workflow generator coverage (bash, enforcing)
        if: always() && matrix.os == 'ubuntu-latest'
        run: bash scripts/check-workflow-generator-coverage.sh

      - name: Check workflow generator coverage (pwsh, enforcing)
        if: always() && matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-workflow-generator-coverage.ps1

      - name: Check AGENTS.md command-sync (bash, enforcing)
        if: always() && matrix.os == 'ubuntu-latest'
        run: bash scripts/check-agents-md-command-sync.sh

      - name: Check AGENTS.md command-sync (pwsh, enforcing)
        if: always() && matrix.os == 'windows-latest'
        run: pwsh -File scripts/check-agents-md-command-sync.ps1

      - name: Build site with Astro Starlight (enforcing)
        if: always()
        working-directory: site
        run: npm run build

      - name: Check internal/ exclusion from dist (bash, enforcing)
        if: always() && matrix.os == 'ubuntu-latest'
        run: |
          leaks=$(find site/dist/ -path '*internal*' -type f 2>/dev/null)
          if [ -n "$leaks" ]; then
            echo "FAIL: internal/ paths leaked into site/dist/:"
            echo "$leaks" | head -10
            exit 1
          fi
          echo "PASS: no internal/ paths in site/dist/"

      - name: Check internal/ exclusion from dist (pwsh, enforcing)
        if: always() && matrix.os == 'windows-latest'
        run: |
          $leaks = Get-ChildItem -Path site/dist -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match 'internal' }
          if ($leaks.Count -gt 0) {
            Write-Output "FAIL: internal/ paths leaked into site/dist/: $($leaks.Count) files"
            $leaks | Select-Object -First 10 | ForEach-Object { Write-Output $_.FullName }
            exit 1
          }
          Write-Output "PASS: no internal/ paths in site/dist/"

      - name: Verify edit-link resolution (enforcing)
        if: always()
        run: node scripts/verify-edit-links.mjs site/dist .

      - name: Test build-aware validators (unit; base single-source)
        # Proves the rendered-link checker consumes the base path it is given (a
        # wrong base must FAIL, not pass silently) and that the default wires
        # through to scripts/site-base.mjs - the regression guard for the 14.7
        # single-source fix. Pure node builtins + a temp fixture; runs on both OS.
        if: always()
        run: node --test scripts/check-rendered-links.test.mjs

      - name: Test hooks + eval validators (unit; enforcing)
        # F-43/F-44 hook units (dependency-free) and M-30 eval invariants. Pure
        # node builtins + temp fixtures; cross-OS. These MUST pass.
        if: always()
        run: |
          node --test hooks/lib/frontmatter.test.mjs hooks/lib/local-config.test.mjs hooks/lib/phase-map.test.mjs hooks/lib/signals.test.mjs hooks/guardrails.test.mjs hooks/phase-router.test.mjs
          node --test scripts/check-sample-no-placeholders.test.mjs scripts/check-sample-exact-quote-sourcing.test.mjs scripts/check-sample-no-fabricated-metrics.test.mjs
          node --test scripts/check-sample-counts.test.mjs
          node --test scripts/check-skill-page-sections.test.mjs
          node --test scripts/check-trigger-fixtures.test.mjs
          node --test scripts/gen-skill-manifest.test.mjs
          node --test scripts/run-trigger-evals.test.mjs
          node --test scripts/run-router-evals.test.mjs
          node --test scripts/check-new-skill-collision.test.mjs
          node --test scripts/check-reciprocal-boundary-pointers.test.mjs
          node --test scripts/output-eval.workflow.test.mjs
          node --test scripts/output-eval-batch.workflow.test.mjs
          node --test scripts/output-eval-informed.workflow.test.mjs
          node --test scripts/output-eval-aggregate.test.mjs
          node --test scripts/check-output-eval-assets.test.mjs

      - name: Eval - no placeholders in samples (advisory)
        # M-30 Tier 1. Advisory until promoted to enforcing per invariant; the
        # corpus is currently clean for this invariant (enforcing candidate).
        if: always()
        continue-on-error: true
        run: node scripts/check-sample-no-placeholders.mjs

      - name: Eval - exact-quote sourcing (advisory)
        # Scoped to foundation-prioritized-action-plan (known-clean as of v2.23.0).
        if: always()
        continue-on-error: true
        run: node scripts/check-sample-exact-quote-sourcing.mjs

      - name: Eval - no fabricated metrics (advisory, heuristic)
        # Flags percentages in output not traceable to input. Heuristic; stays
        # advisory (never enforcing soon). Surfaces ~260 findings for human review.
        if: always()
        continue-on-error: true
        run: node scripts/check-sample-no-fabricated-metrics.mjs

      - name: Sample-count reconciliation (enforcing)
        # D8 (v2.29.0): library sample counts on disk must match the headline numbers in
        # README_SAMPLES.md + samples/index.md. check-count-consistency excludes library/,
        # so this closes that gap (the same rot class as the stale showcase). Deterministic.
        if: always()
        run: node scripts/check-sample-counts.mjs

      - name: Skill-page completeness (enforcing)
        # v2.29.1: every SKILL.md section must render on its generated docs page. gen-site used to
        # drop unrecognized sections silently, shipping ~27 hollow pages; the renderSkillPage catch-all
        # fixed it and this gate keeps it fixed. Output-based: it renders each page and checks every section.
        if: always()
        run: node scripts/check-skill-page-sections.mjs

      - name: Eval - trigger-fixture structure + asset presence (enforcing)
        # M-31 Phase 1 (spec_trigger-accuracy-evals.md section 3): schema, composition
        # minimums, 60/40 split, collision near-misses, and roster asset-presence (every
        # Phase 1 roster skill has evals/trigger-fixtures.json). Promoted advisory ->
        # enforcing 2026-06-14 (B-4): the 29-file corpus is stable and the validator
        # exits 0, so a malformed fixture or a roster skill losing its fixture now fails
        # CI. Roster-scoped by design (the 29 measured skills); the other skills are not
        # yet in the trigger-eval program, so they are not asset-gated here.
        if: always()
        run: node scripts/check-trigger-fixtures.mjs

      - name: Eval - reciprocal boundary pointers (enforcing)
        # C-5 (M-31): every declared collision pair must point at each other in "When NOT
        # to Use" - the reciprocity that kept the library collision-clean. Promoted advisory
        # -> enforcing 2026-06-14 once the one known gap closed: foundation-meeting-recap
        # 1.0.2 added the reciprocal back-pointer to discover-interview-synthesis, so the
        # validator now exits 0. Any future collision pair missing a reciprocal pointer fails CI.
        if: always()
        run: node scripts/check-reciprocal-boundary-pointers.mjs

      - name: Eval - output-eval asset presence (advisory)
        # B-7 (M-33): the output-eval analog of B-4. Every skill carrying an
        # evals/output-scenarios/ entry must have well-formed scenario frontmatter, a
        # matching skill name, an existing family rubric, and a non-trivial brief. Advisory
        # on the M-30 ladder until the output-eval roster is pinned, then enforcing. Closes
        # the regression-protection gap from the 2026-06-14 codex adversarial review.
        if: always()
        continue-on-error: true
        run: node scripts/check-output-eval-assets.mjs

      - name: Check rendered link resolution + anchors (enforcing)
        if: always()
        env:
          STRICT_ANCHORS: '1'
        run: node scripts/check-rendered-links.mjs site/dist

      - name: Check route parity (enforcing; no published URL removed without a redirect)
        if: always()
        run: node scripts/check-route-parity.mjs site/dist

      - name: Check root doc links (enforcing; README/CHANGELOG relative + deployed-site URLs)
        # Root markdown (README, CHANGELOG, ...) is rendered by GitHub, not Astro, so
        # the build-aware link guards above never see it. This closes that blind spot:
        # relative links must resolve in the repo and deployed-site URLs must map to a
        # real route in scripts/route-manifest.txt. Reads the committed repo + manifest
        # (no site/dist dependency). Pure node builtins; cross-OS.
        if: always()
        run: |
          node --test scripts/check-root-doc-links.test.mjs
          node scripts/check-root-doc-links.mjs

      - name: Check validator-inventory parity (enforcing; bash/pwsh/CI vs manifest)
        # The release gate has three hand-maintained SHELL-validator inventories:
        # pre-tag-validate.sh, pre-tag-validate.ps1, and this workflow. They drifted
        # before (P0-01/P0-02). scripts/validation-manifest.yaml is now the single
        # source of truth; this referee FAILS if any inventory drifts from it, so the
        # bash and pwsh bundles cannot list different sets and CI cannot add or drop a
        # shell validator without updating the manifest. Pure node builtins + js-yaml
        # (root dep, installed above); cross-OS.
        if: always()
        run: |
          node --test scripts/check-validator-parity.test.mjs
          node scripts/check-validator-parity.mjs
