From c1b15170bcb2df5bd56d81b7090fe679c8be49eb Mon Sep 17 00:00:00 2001 From: Igor Beylin Date: Tue, 28 Jul 2026 16:07:40 +0000 Subject: [PATCH 1/3] chore(repo-hygiene): relocate PR-remediation scripts and drop mis-imported PyPI release workflow Per the L9 repository instantiation audit and remediation plan: - Move the 12 historical PR-remediation helper scripts (fix-*.sh, push-*.sh, s4036-fix.sh, sonar-fix.sh, transplant.sh, push-results.txt) out of the repository root into tools/pr-remediation/, and document them in a new tools/pr-remediation/README.md. These files had no consumer in package.json, workflows, or docs, so their presence at root wrongly implied they were part of the shipped project surface. - Remove .github/workflows/l9-release.yml. It is a Quantum-L9 org starter that publishes to PyPI (python-version 3.12, publish-to-pypi: true), which is inappropriate for this TypeScript/npm package. npm release is already handled by publish.yml. Nothing references l9-release.yml. No production source, build, or published-package files are changed. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01P1xeNSemqdnFScWGvUayHZ --- .github/workflows/l9-release.yml | 13 -------- tools/pr-remediation/README.md | 32 +++++++++++++++++++ .../pr-remediation/fix-boundary-chain.sh | 0 .../pr-remediation/fix-eslint-chain.sh | 0 .../pr-remediation/fix-lockfile-chain.sh | 0 .../pr-remediation/fix-pin-prs.sh | 0 .../pr-remediation/fix-readfile-chain.sh | 0 .../pr-remediation/fix-sha-chain.sh | 0 .../pr-remediation/push-remediation.sh | 0 .../pr-remediation/push-results.txt | 0 .../pr-remediation/push-transplants.sh | 0 .../pr-remediation/s4036-fix.sh | 0 .../pr-remediation/sonar-fix.sh | 0 .../pr-remediation/transplant.sh | 0 14 files changed, 32 insertions(+), 13 deletions(-) delete mode 100644 .github/workflows/l9-release.yml create mode 100644 tools/pr-remediation/README.md rename fix-boundary-chain.sh => tools/pr-remediation/fix-boundary-chain.sh (100%) rename fix-eslint-chain.sh => tools/pr-remediation/fix-eslint-chain.sh (100%) rename fix-lockfile-chain.sh => tools/pr-remediation/fix-lockfile-chain.sh (100%) rename fix-pin-prs.sh => tools/pr-remediation/fix-pin-prs.sh (100%) rename fix-readfile-chain.sh => tools/pr-remediation/fix-readfile-chain.sh (100%) rename fix-sha-chain.sh => tools/pr-remediation/fix-sha-chain.sh (100%) rename push-remediation.sh => tools/pr-remediation/push-remediation.sh (100%) rename push-results.txt => tools/pr-remediation/push-results.txt (100%) rename push-transplants.sh => tools/pr-remediation/push-transplants.sh (100%) rename s4036-fix.sh => tools/pr-remediation/s4036-fix.sh (100%) rename sonar-fix.sh => tools/pr-remediation/sonar-fix.sh (100%) rename transplant.sh => tools/pr-remediation/transplant.sh (100%) diff --git a/.github/workflows/l9-release.yml b/.github/workflows/l9-release.yml deleted file mode 100644 index 579d78d..0000000 --- a/.github/workflows/l9-release.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Quantum-L9 org starter — Versioned release build and PyPI publish. Calls l9-ci-core kernel; do not add logic here. -name: "L9 Release" -on: - push: - tags: ['v*.*.*'] - -jobs: - l9_release: - uses: Quantum-L9/l9-ci-core/.github/workflows/release-publish.yml@v1 - secrets: inherit - with: - python-version: "3.12" - publish-to-pypi: true diff --git a/tools/pr-remediation/README.md b/tools/pr-remediation/README.md new file mode 100644 index 0000000..0d7ec3d --- /dev/null +++ b/tools/pr-remediation/README.md @@ -0,0 +1,32 @@ +# PR Remediation Tooling (Historical) + +These scripts were used to rebuild and push a stacked chain of pack-remediation +commits across a set of open pull requests against `Quantum-L9/LLM-Router` +(referenced PR numbers: 2, 8-17, 18). They fix historical defects such as: + +- `fix-boundary-chain.sh` — adds the provider-boundary ESLint rule to PR stages + that lacked it. +- `fix-eslint-chain.sh` — patches `eslint.config.js` so the required `eslint .` + check passes on affected stages. +- `fix-lockfile-chain.sh` — fixes CI steps on stages missing `package-lock.json`. +- `fix-pin-prs.sh` — maps PR numbers to branch names for the transplant chain. +- `fix-readfile-chain.sh` — removes an unused `readFile` import from + `scripts/verify-package.mjs`. +- `fix-sha-chain.sh` — fixes truncated `actions/upload-artifact` SHA pins in + `ci.yml` / `supply-chain.yml`. +- `push-remediation.sh`, `push-transplants.sh` — push remediation/transplant + branches to their live PR head branches. +- `s4036-fix.sh` — fixes SonarCloud rule `javascript:S4036` in + `scripts/verify-package.mjs`. +- `sonar-fix.sh` — applies SonarCloud remediation per PR branch. +- `transplant.sh` — rebuilds the canonical transplant-commit chain onto `main`. +- `push-results.txt` — recorded outcome of one push run (mixed + success/failure across PR branches). + +Relocated from the repository root (2026-07-28) per the L9 repository +instantiation audit and remediation plan: these files had no consumer in +`package.json`, GitHub Actions workflows, or documentation, and their +presence at root implied they were part of the shipped project surface. +They are retained here, out of the package root, pending confirmation that +all referenced pull requests are merged or closed, at which point they can +be deleted. diff --git a/fix-boundary-chain.sh b/tools/pr-remediation/fix-boundary-chain.sh similarity index 100% rename from fix-boundary-chain.sh rename to tools/pr-remediation/fix-boundary-chain.sh diff --git a/fix-eslint-chain.sh b/tools/pr-remediation/fix-eslint-chain.sh similarity index 100% rename from fix-eslint-chain.sh rename to tools/pr-remediation/fix-eslint-chain.sh diff --git a/fix-lockfile-chain.sh b/tools/pr-remediation/fix-lockfile-chain.sh similarity index 100% rename from fix-lockfile-chain.sh rename to tools/pr-remediation/fix-lockfile-chain.sh diff --git a/fix-pin-prs.sh b/tools/pr-remediation/fix-pin-prs.sh similarity index 100% rename from fix-pin-prs.sh rename to tools/pr-remediation/fix-pin-prs.sh diff --git a/fix-readfile-chain.sh b/tools/pr-remediation/fix-readfile-chain.sh similarity index 100% rename from fix-readfile-chain.sh rename to tools/pr-remediation/fix-readfile-chain.sh diff --git a/fix-sha-chain.sh b/tools/pr-remediation/fix-sha-chain.sh similarity index 100% rename from fix-sha-chain.sh rename to tools/pr-remediation/fix-sha-chain.sh diff --git a/push-remediation.sh b/tools/pr-remediation/push-remediation.sh similarity index 100% rename from push-remediation.sh rename to tools/pr-remediation/push-remediation.sh diff --git a/push-results.txt b/tools/pr-remediation/push-results.txt similarity index 100% rename from push-results.txt rename to tools/pr-remediation/push-results.txt diff --git a/push-transplants.sh b/tools/pr-remediation/push-transplants.sh similarity index 100% rename from push-transplants.sh rename to tools/pr-remediation/push-transplants.sh diff --git a/s4036-fix.sh b/tools/pr-remediation/s4036-fix.sh similarity index 100% rename from s4036-fix.sh rename to tools/pr-remediation/s4036-fix.sh diff --git a/sonar-fix.sh b/tools/pr-remediation/sonar-fix.sh similarity index 100% rename from sonar-fix.sh rename to tools/pr-remediation/sonar-fix.sh diff --git a/transplant.sh b/tools/pr-remediation/transplant.sh similarity index 100% rename from transplant.sh rename to tools/pr-remediation/transplant.sh From 77c43076ec046ac5934a7c38c303f9d00862bdd6 Mon Sep 17 00:00:00 2001 From: Igor Beylin Date: Tue, 28 Jul 2026 16:16:19 +0000 Subject: [PATCH 2/3] ci: converge to L9 shared model (classifier-first routing, canonical gate) Adapt the L9 shared CI target model (authored for Python) to this TypeScript/npm package. Routing architecture adopted verbatim; gates map to the repo's own npm scripts. - Add .github/scripts/classify_pr.py: changed-files-primary PR classifier with TS-aware surfaces (providers/vision/budget/control-plane), full L9 output set plus run_* routing booleans, unknown diffs fail closed. Validated across docs, app, security, workflow, dependency, contract, and unknown scenarios. - Add .github/workflows/pr-pipeline.yml: canonical "PR Pipeline Gate" job that classifies then routes to lint / build+types / test / security (npm scripts), aggregates with if: always(), contents: read, concurrency + cancel-in-progress. - Add governance policies: routing_policy, blocking_policy, comment_protocol. - Add .github/labels.yml namespaced taxonomy (automation/type/area/risk) as the provisioning source of truth. - Upgrade actions/checkout v4 -> v6 across all workflows, preserving SHA pinning (Scorecard hardening): 34e1148... -> d23441a... . - Document file-plane changes and the settings-plane roadmap (label provisioning, branch protection requiring the gate, secrets) in docs/ci/convergence.md. No production source is changed. classify_pr.py and every npm gate run clean locally (eslint, boundary probe, tsc build, type-check, declarations, 76 vitest tests, npm audit, package verify). Branch protection, label creation, and secrets are settings-plane actions and are intentionally NOT applied here. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01P1xeNSemqdnFScWGvUayHZ --- .github/governance/blocking_policy.yaml | 37 ++++ .github/governance/comment_protocol.yaml | 19 ++ .github/governance/routing_policy.yaml | 48 +++++ .github/labels.yml | 59 ++++++ .github/scripts/classify_pr.py | 212 ++++++++++++++++++++++ .github/workflows/ci.yml | 4 +- .github/workflows/l9-node-ts-monorepo.yml | 6 +- .github/workflows/pr-pipeline.yml | 179 ++++++++++++++++++ .github/workflows/publish.yml | 2 +- .github/workflows/supply-chain.yml | 2 +- docs/ci/convergence.md | 61 +++++++ 11 files changed, 622 insertions(+), 7 deletions(-) create mode 100644 .github/governance/blocking_policy.yaml create mode 100644 .github/governance/comment_protocol.yaml create mode 100644 .github/governance/routing_policy.yaml create mode 100644 .github/labels.yml create mode 100644 .github/scripts/classify_pr.py create mode 100644 .github/workflows/pr-pipeline.yml create mode 100644 docs/ci/convergence.md diff --git a/.github/governance/blocking_policy.yaml b/.github/governance/blocking_policy.yaml new file mode 100644 index 0000000..80d7528 --- /dev/null +++ b/.github/governance/blocking_policy.yaml @@ -0,0 +1,37 @@ +# L9 blocking vs advisory policy for @quantum-l9/llm-router. +# "PR Pipeline Gate" is the single canonical merge gate (see pr-pipeline.yml). +# Inherited debt is visible but advisory unless a touched file worsens it. +blocking_policy: + version: 1 + merge_gate: "PR Pipeline Gate" + blocking_jobs: + - lint + - build + - test + - security + advisory_jobs: + - dependency-review + - secret-scan + unknown_class_behavior: block_all # fail closed + inherited_debt_behavior: advisory_unless_touched + + hard_block_if_touched: + - secret_leak + - auth_bypass + - security_boundary_weakening # provider transport boundary (eslint lint:boundary) + - transport_packet_contract_break # OpenAI SDK transport clients + - image_ingress_validation_bypass # src/vision URL/inline-image validation + - budget_reservation_bypass # src/budget pre-dispatch reservation + - contract_bound_change_without_contract_update # src/control-plane contracts + - classifier_failure + - unknown_high_or_critical_security_finding + + advisory_if_untouched: + - inherited_style_debt + - inherited_docstring_findings + - historical_non_touched_high_findings + + fail_closed: + - diff_unknown + - classifier_failure + - missing_required_ci_context diff --git a/.github/governance/comment_protocol.yaml b/.github/governance/comment_protocol.yaml new file mode 100644 index 0000000..0e51cd1 --- /dev/null +++ b/.github/governance/comment_protocol.yaml @@ -0,0 +1,19 @@ +# L9 PR comment protocol. Persistent governance comments MUST use a stable +# marker and update in place — never duplicate. Prepares the agent-review loop +# (not enabled yet). See the l9-github-ci kernel for the canonical layout. +comment_protocol: + version: 1 + bot_identity: "github-actions[bot]" + update_existing_comment: true + duplicate_persistent_comments_allowed: false + max_comment_chars: 65336 + markers: + ci_summary: "" + audit: "" + contract_control: "" + security: "" + future_agent_review: "" + rules: + - "Find existing comment by exact marker; update it in place." + - "Never match by title alone; never create duplicate persistent comments." + - "Truncate to the max length with an explicit truncation notice." diff --git a/.github/governance/routing_policy.yaml b/.github/governance/routing_policy.yaml new file mode 100644 index 0000000..ae63292 --- /dev/null +++ b/.github/governance/routing_policy.yaml @@ -0,0 +1,48 @@ +# L9 CI routing policy for @quantum-l9/llm-router (TypeScript / Node). +# Consumed by .github/scripts/classify_pr.py and .github/workflows/pr-pipeline.yml. +# Primary signal is changed files; labels are secondary hints that may upgrade +# relevance but never downgrade a touched security surface. +routing_policy: + version: 1 + primary_signal: changed_files + secondary_signal: labels + conflict_resolution: evidence_beats_labels + unknown_diff_behavior: fail_closed + classes: + - name: docs_only + patterns: ["*.md", "docs/**", "LICENSE", ".github/ISSUE_TEMPLATE/**"] + gates: [] + advisory: [lint] + - name: ci_workflow + patterns: [".github/workflows/**"] + gates: [lint] + advisory: [security] + - name: tests_only + patterns: ["tests/**", "**/*.test.ts", "**/*.spec.ts"] + gates: [lint, test] + - name: dependency + patterns: ["package.json", "package-lock.json", ".npmrc"] + gates: [lint, build, test, security] + - name: compliance + patterns: + - "src/control-plane/**" + - "src/matrices/**" + - "fixtures/control-plane/**" + - ".github/governance/**" + gates: [lint, build, test] + - name: security + patterns: + - "src/providers/**" + - "src/vision/**" + - "src/budget/**" + - "eslint.config.js" + - "scripts/verify-eslint-boundary.mjs" + gates: [lint, build, test, security] + requires_human_review: true + - name: app_code + patterns: ["src/**", "scripts/**"] + gates: [lint, build, test, security] + - name: unknown + patterns: ["*"] + gates: [lint, build, test, security] + policy: fail_closed diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..45ff888 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,59 @@ +# Canonical namespaced label taxonomy for @quantum-l9/llm-router. +# Source of truth for the L9 label contract (l9-github-ci kernel §9 + +# l9_target_ci_spec labels). Plain labels (ci, security, testing, typing, +# docker, python, dependencies, github-actions) are deprecated and must not be +# created by automation. Provisioning to the GitHub UI is a settings-plane +# action (see docs/ci/convergence.md) — this file does not create labels itself. + +automation: # color: 5319E7 + - automation:dependabot + - automation:coderabbit + - automation:perplexity + - automation:github-actions + - automation:gitguardian + - automation:sonarcloud + - automation:codex + - automation:l9-agent + +type: # color: 0E8A16 + - type:ci + - type:security + - type:docs + - type:test + - type:deps + - type:refactor + - type:governance + - type:bug + - type:feature + - type:release + +area: # color: 1D76DB + - area:workflows + - area:l9 + - area:contracts + - area:api + - area:typing + - area:runtime + - area:transport # provider transport boundary (src/providers) + - area:vision # image ingress validation (src/vision) + - area:budget # budget reservation (src/budget) + - area:infra + +risk: # color: D93F0B + - risk:blocking + - risk:advisory + - risk:secret + - risk:deploy + - risk:contract + - risk:runtime + - risk:security + +deprecated_plain_labels_forbidden: + - ci + - security + - testing + - typing + - docker + - python + - dependencies + - github-actions diff --git a/.github/scripts/classify_pr.py b/.github/scripts/classify_pr.py new file mode 100644 index 0000000..9db62a3 --- /dev/null +++ b/.github/scripts/classify_pr.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""L9 PR classifier for @quantum-l9/llm-router. + +Adapted from the L9 shared CI model for a TypeScript / Node package. The routing +logic is language-agnostic; the file-pattern surfaces are tuned to this repo +(TypeScript source under src/, Vitest tests under tests/, npm dependency +manifests, Control Plane contracts). + +Contract (see .github/governance/routing_policy.yaml and the l9-github-ci kernel): + - Changed files are the primary signal. Labels are secondary hints only and may + never downgrade a touched security surface. + - Unknown diffs fail closed: conservative core gates run. + +Emits both the rich L9 classifier outputs (pr_class + *_changed surfaces) and the +run_* routing booleans consumed by pr-pipeline.yml. Standard library only. +""" + +from __future__ import annotations + +import fnmatch +import os +import subprocess +import sys + +# --- surface patterns (primary signal) ------------------------------------- + +PATTERNS = { + "docs": ["*.md", "docs/**", "LICENSE", "*.txt", ".github/ISSUE_TEMPLATE/**"], + "workflows": [".github/workflows/**"], + "scripts": ["scripts/**", ".github/scripts/**"], + "app": ["src/**"], + "tests": ["tests/**", "*.test.ts", "*.spec.ts", "**/*.test.ts", "**/*.spec.ts"], + "docker": ["Dockerfile", "Dockerfile.*", "*.dockerfile", "docker/**", ".dockerignore"], + "dependency": ["package.json", "package-lock.json", "npm-shrinkwrap.json", ".npmrc"], + "dependency_types": ["**/@types/**"], + "contracts": [ + "src/control-plane/**", + "src/matrices/**", + "fixtures/control-plane/**", + "tests/control-plane/**", + ".github/governance/**", + ], + "security_sensitive": [ + "src/providers/**", + "src/vision/**", + "src/budget/**", + "scripts/verify-eslint-boundary.mjs", + "eslint.config.js", + ".github/workflows/**security*", + ".github/workflows/supply-chain.yml", + ], + "typing_sensitive": ["tsconfig*.json", "**/*.d.ts", "src/**/types.ts", "src/**/*.types.ts"], + "transport_sensitive": ["src/providers/**"], + "ingress_sensitive": ["src/vision/**"], + "python": ["*.py", "**/*.py"], +} + + +def _match(path: str, globs: list[str]) -> bool: + for g in globs: + if fnmatch.fnmatch(path, g): + return True + # support "dir/**" prefix matching for nested paths + if g.endswith("/**") and (path == g[:-3] or path.startswith(g[:-2])): + return True + return False + + +def changed_files() -> list[str]: + """Resolve the PR's changed files. Prefer an explicit CHANGED_FILES env + (newline separated), otherwise diff against the base ref.""" + explicit = os.environ.get("CHANGED_FILES", "").strip() + if explicit: + return [f.strip() for f in explicit.splitlines() if f.strip()] + + base = os.environ.get("GITHUB_BASE_REF") or "main" + for ref in (f"origin/{base}", base): + try: + out = subprocess.run( + ["git", "diff", "--name-only", f"{ref}...HEAD"], + capture_output=True, text=True, check=True, + ).stdout + files = [f.strip() for f in out.splitlines() if f.strip()] + if files: + return files + except subprocess.CalledProcessError: + continue + return [] + + +def classify(files: list[str], labels: list[str]) -> dict: + surfaces = {name: False for name in PATTERNS} + matched_any = {f: False for f in files} + + for f in files: + for name, globs in PATTERNS.items(): + if _match(f, globs): + surfaces[name] = True + matched_any[f] = True + + # A file that matched no known surface makes the diff unknown -> fail closed. + diff_unknown = (not files) or any(not m for m in matched_any.values()) + + # Label hints may UPGRADE relevance but never downgrade a touched security + # surface. Evidence beats labels. + label_set = {l.lower() for l in labels} + if "risk:security" in label_set or "type:security" in label_set: + surfaces["security_sensitive"] = surfaces["security_sensitive"] or True + + # pr_class precedence: most safety-critical wins. + if surfaces["security_sensitive"]: + pr_class = "security" + elif surfaces["contracts"]: + pr_class = "compliance" + elif surfaces["docker"]: + pr_class = "docker" + elif surfaces["dependency_types"] and not (surfaces["app"] or surfaces["contracts"]): + pr_class = "dependency_types" + elif surfaces["dependency"] and not (surfaces["app"] or surfaces["contracts"]): + pr_class = "dependency" + elif surfaces["app"]: + pr_class = "app_code" + elif surfaces["tests"] and not surfaces["app"]: + pr_class = "tests_only" + elif surfaces["workflows"] and not (surfaces["app"] or surfaces["scripts"]): + pr_class = "ci_workflow" + elif surfaces["scripts"]: + pr_class = "app_code" + elif surfaces["docs"] and not diff_unknown: + pr_class = "docs_only" + elif diff_unknown: + pr_class = "unknown_diff" + else: + pr_class = "unknown" + + is_docs_only = pr_class == "docs_only" + + # run_* routing (consumed by pr-pipeline.yml). Fail closed on unknown. + fail_closed = pr_class in ("unknown_diff", "unknown") + run_lint = not is_docs_only or fail_closed + run_build = surfaces["app"] or surfaces["contracts"] or surfaces["typing_sensitive"] \ + or surfaces["dependency"] or surfaces["scripts"] or fail_closed + run_test = surfaces["app"] or surfaces["tests"] or surfaces["contracts"] \ + or surfaces["dependency"] or fail_closed + run_security = surfaces["security_sensitive"] or surfaces["dependency"] \ + or surfaces["app"] or fail_closed + run_infrastructure = surfaces["workflows"] or surfaces["docker"] + requires_human_review = pr_class in ("security", "compliance", "unknown_diff") + + semgrep_relevant = surfaces["app"] or surfaces["scripts"] or surfaces["security_sensitive"] + sbom_relevant = surfaces["dependency"] or surfaces["app"] + scorecard_relevant = surfaces["workflows"] or surfaces["dependency"] + + return { + "pr_class": pr_class, + "all_changed_files": ";".join(files), + "changed_count": str(len(files)), + "diff_unknown": str(diff_unknown).lower(), + "labels": ",".join(labels), + "detected_labels": ",".join(labels), + "python_changed": str(surfaces["python"]).lower(), + "app_changed": str(surfaces["app"]).lower(), + "tests_changed": str(surfaces["tests"]).lower(), + "docs_changed": str(surfaces["docs"]).lower(), + "workflows_changed": str(surfaces["workflows"]).lower(), + "scripts_changed": str(surfaces["scripts"]).lower(), + "docker_changed": str(surfaces["docker"]).lower(), + "dependency_changed": str(surfaces["dependency"]).lower(), + "contracts_changed": str(surfaces["contracts"]).lower(), + "security_sensitive_changed": str(surfaces["security_sensitive"]).lower(), + "typing_sensitive_changed": str(surfaces["typing_sensitive"]).lower(), + "transport_sensitive_changed": str(surfaces["transport_sensitive"]).lower(), + "ingress_sensitive_changed": str(surfaces["ingress_sensitive"]).lower(), + "semgrep_relevant": str(semgrep_relevant).lower(), + "sbom_relevant": str(sbom_relevant).lower(), + "scorecard_relevant": str(scorecard_relevant).lower(), + # routing booleans + "run_lint": str(run_lint).lower(), + "run_build": str(run_build).lower(), + "run_test": str(run_test).lower(), + "run_security": str(run_security).lower(), + "run_infrastructure": str(run_infrastructure).lower(), + "is_docs_only": str(is_docs_only).lower(), + "requires_human_review": str(requires_human_review).lower(), + } + + +def main() -> int: + labels_env = os.environ.get("PR_LABELS", "").strip() + labels = [l.strip() for l in labels_env.replace("\n", ",").split(",") if l.strip()] + files = changed_files() + result = classify(files, labels) + + # Emit to GITHUB_OUTPUT if present, else stdout (local runs). + out_path = os.environ.get("GITHUB_OUTPUT") + lines = [f"{k}={v}" for k, v in result.items()] + if out_path: + with open(out_path, "a", encoding="utf-8") as fh: + fh.write("\n".join(lines) + "\n") + + print("L9 PR classifier") + print(f" changed files : {result['changed_count']}") + print(f" pr_class : {result['pr_class']}") + print(f" diff_unknown : {result['diff_unknown']}") + print(f" run_lint/build/test/security: " + f"{result['run_lint']}/{result['run_build']}/" + f"{result['run_test']}/{result['run_security']}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 039169a..f45eeda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: matrix: node: ["20.19.0", "22.23.1", "24.18.0"] steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ matrix.node }} @@ -43,7 +43,7 @@ jobs: needs: node runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 with: name: package-contract diff --git a/.github/workflows/l9-node-ts-monorepo.yml b/.github/workflows/l9-node-ts-monorepo.yml index 1be6ba5..3d1b2fd 100644 --- a/.github/workflows/l9-node-ts-monorepo.yml +++ b/.github/workflows/l9-node-ts-monorepo.yml @@ -12,7 +12,7 @@ jobs: install-and-typecheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "20" @@ -33,7 +33,7 @@ jobs: needs: install-and-typecheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "20" @@ -47,7 +47,7 @@ jobs: needs: test-by-segment runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "20" diff --git a/.github/workflows/pr-pipeline.yml b/.github/workflows/pr-pipeline.yml new file mode 100644 index 0000000..6d45a01 --- /dev/null +++ b/.github/workflows/pr-pipeline.yml @@ -0,0 +1,179 @@ +# Canonical L9 merge gate for @quantum-l9/llm-router. +# +# CI is a router, not a bouncer: classify the PR from its changed files, run +# only the relevant Node/TS gates, and summarize merge truth in ONE required +# check — "PR Pipeline Gate". This is the intended branch-protection required +# check; enforcement is configured in GitHub Settings (see docs/ci/convergence.md). +# +# Adapted from the L9 shared model (Python) to this TypeScript package: gates map +# to the repo's own npm scripts (eslint, tsc, vitest, npm audit, boundary probe). +# Actions are SHA-pinned (Scorecard hardening) with a version comment. +name: PR Pipeline Gate + +on: + pull_request: + branches: [main, 'release/**'] + +permissions: + contents: read + +concurrency: + group: pr-pipeline-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: "20.19.0" + +jobs: + classify: + name: Classify + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + pr_class: ${{ steps.classify.outputs.pr_class }} + run_lint: ${{ steps.classify.outputs.run_lint }} + run_build: ${{ steps.classify.outputs.run_build }} + run_test: ${{ steps.classify.outputs.run_test }} + run_security: ${{ steps.classify.outputs.run_security }} + is_docs_only: ${{ steps.classify.outputs.is_docs_only }} + diff_unknown: ${{ steps.classify.outputs.diff_unknown }} + requires_human_review: ${{ steps.classify.outputs.requires_human_review }} + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + fetch-depth: 0 + - id: classify + name: Classify PR + env: + GITHUB_BASE_REF: ${{ github.base_ref }} + PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} + run: python3 .github/scripts/classify_pr.py + + lint: + name: Lint + needs: classify + if: needs.classify.outputs.run_lint == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - run: npm ci --ignore-scripts + - run: npm run lint + - run: npm run lint:boundary + + build: + name: Build & Types + needs: classify + if: needs.classify.outputs.run_build == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - run: npm ci --ignore-scripts + - run: npm run build + - run: npm run verify:types + - run: npm run verify:declarations + + test: + name: Test + needs: classify + if: needs.classify.outputs.run_test == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - run: npm ci --ignore-scripts + - run: npm test + + security: + name: Security + needs: classify + if: needs.classify.outputs.run_security == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - run: npm ci --ignore-scripts + - run: npm audit --audit-level=high --omit=dev + - run: npm run verify:package + + gate: + name: PR Pipeline Gate + needs: [classify, lint, build, test, security] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Evaluate canonical merge gate + env: + PR_CLASS: ${{ needs.classify.outputs.pr_class }} + DIFF_UNKNOWN: ${{ needs.classify.outputs.diff_unknown }} + HUMAN_REVIEW: ${{ needs.classify.outputs.requires_human_review }} + R_CLASSIFY: ${{ needs.classify.result }} + R_LINT: ${{ needs.lint.result }} + R_BUILD: ${{ needs.build.result }} + R_TEST: ${{ needs.test.result }} + R_SECURITY: ${{ needs.security.result }} + run: | + set -euo pipefail + fail=0 + summary=".github pr-pipeline gate summary" + note() { echo "$1"; summary="${summary} + - $1"; } + + note "pr_class: ${PR_CLASS:-unknown}" + note "classify: ${R_CLASSIFY}" + + # Classifier itself must succeed (fail closed). + if [ "${R_CLASSIFY}" != "success" ]; then + note "BLOCK: classifier did not succeed"; fail=1 + fi + if [ "${DIFF_UNKNOWN}" = "true" ]; then + note "NOTE: diff_unknown -> conservative core gates ran (fail closed)" + fi + + # A routed gate blocks only when it actually ran and failed. + # 'skipped' is a legitimate route decision, never a failure. + for pair in "lint:${R_LINT}" "build:${R_BUILD}" "test:${R_TEST}" "security:${R_SECURITY}"; do + name="${pair%%:*}"; result="${pair##*:}" + if [ "${result}" = "failure" ]; then + note "BLOCK: ${name} failed"; fail=1 + elif [ "${result}" = "cancelled" ]; then + note "BLOCK: ${name} cancelled"; fail=1 + else + note "ok: ${name} = ${result}" + fi + done + + if [ "${HUMAN_REVIEW}" = "true" ]; then + note "ADVISORY: touched security/compliance/unknown surface — human review recommended" + fi + + { + echo "## PR Pipeline Gate" + echo "" + echo "\`\`\`" + echo "${summary}" + echo "\`\`\`" + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${fail}" -ne 0 ]; then + echo "Gate FAILED"; exit 1 + fi + echo "Gate PASSED" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index abcbdf8..33c033b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24.18.0 diff --git a/.github/workflows/supply-chain.yml b/.github/workflows/supply-chain.yml index 0ae9565..3771136 100644 --- a/.github/workflows/supply-chain.yml +++ b/.github/workflows/supply-chain.yml @@ -9,7 +9,7 @@ jobs: sbom: runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24.18.0 diff --git a/docs/ci/convergence.md b/docs/ci/convergence.md new file mode 100644 index 0000000..70417d3 --- /dev/null +++ b/docs/ci/convergence.md @@ -0,0 +1,61 @@ +# L9 CI Convergence — @quantum-l9/llm-router + +This repo is a **TypeScript / npm** package. The L9 shared CI target model is +authored around Python repos, so convergence here is an *adaptation*: the routing +architecture (classifier-first, one canonical gate, namespaced labels, blocking +vs advisory policy) is adopted verbatim, while the gates themselves map to this +repo's own npm scripts instead of `ruff`/`pytest`/`semgrep`. + +## What was implemented in-repo (file plane) + +| Area | File | Notes | +|---|---|---| +| Classifier | `.github/scripts/classify_pr.py` | Python 3 stdlib; TS-aware surfaces (src/providers, src/vision, src/budget, src/control-plane). Emits the full L9 output set + `run_*` routing booleans. Unknown diffs **fail closed**. | +| Canonical gate | `.github/workflows/pr-pipeline.yml` | Job **`PR Pipeline Gate`** aggregates classify → lint → build → test → security using this repo's npm scripts. `if: always()`, `contents: read`, `concurrency` + `cancel-in-progress`. | +| Routing policy | `.github/governance/routing_policy.yaml` | Changed-files primary, labels secondary, evidence-beats-labels. | +| Blocking policy | `.github/governance/blocking_policy.yaml` | Hard-block-if-touched surfaces (transport boundary, image ingress, budget reservation, control-plane contracts); inherited debt advisory. | +| Comment protocol | `.github/governance/comment_protocol.yaml` | Stable markers, update-in-place, no duplicates. | +| Label taxonomy | `.github/labels.yml` | Namespaced source of truth (`automation:` / `type:` / `area:` / `risk:`). | +| Workflow standard | all `.github/workflows/*.yml` | `actions/checkout` upgraded v4 → **v6** (`d23441a…`), SHA-pinned to preserve Scorecard hardening. | + +The classifier and every npm gate (`lint`, `lint:boundary`, `build`, +`verify:types`, `verify:declarations`, `test`, `npm audit`, `verify:package`) +were run locally and pass. + +## Settings plane — requires GitHub admin, NOT changed by this branch + +These cannot be set from repo files and were **not** modified. Apply with +explicit approval. + +### 1. Provision namespaced labels +Create every label in `.github/labels.yml`; delete the deprecated plain labels +(`ci`, `security`, `testing`, `typing`, `docker`, `python`, `dependencies`, +`github-actions`). Suggested colors are in the manifest. + +### 2. Branch protection on `main` +- Required check: **`PR Pipeline Gate`** (add others after they stabilize: + CodeQL / Gitleaks / GitGuardian / SonarCloud / Supply Chain). +- Require PR before merge; ≥1 approval; dismiss stale approvals; require + CODEOWNERS; require conversation resolution; require branches up to date; + linear history; disallow force-push and deletion. +- Merge strategy: squash-only; auto-delete head branches. + +> Status: **Unknown / unenforced.** Do not claim `PR Pipeline Gate` is enforced +> until GitHub → Settings → Branches confirms it is a required check. + +### 3. Secrets / variables (names only) +- Platform/org preferred: `SONAR_TOKEN`, `GITGUARDIAN_API_KEY`, `CODECOV_TOKEN`. +- Repo publish: `NODE_AUTH_TOKEN` (GitHub Packages, already used by `publish.yml`). + +## Known divergences deferred (roadmap) + +- **`l9-pr-pipeline.yml` / `l9-governance.yml`** call the org `l9-ci-core` + reusable kernels with `python-version: "3.12"` / a Python trio-governance + model. These are **language-mismatched** for a TS package. Left in place to + avoid breaking org wiring; resolving them belongs at the `l9-ci-core` org + level (a Node/TS kernel or a documented no-op path for TS repos). +- **GitGuardian / Gitleaks / SonarCloud / CodeQL** are `recommended_optional` + and deferred until the canonical gate is enforced and their tokens exist. +- Agent-review loop is **prepared, not enabled** — the foundation (stable + classifier outputs, machine-readable gate summary, marker protocol, + namespaced labels) is now in place. From d7dc4ae5e5a533d997ebdc405739054587efee1a Mon Sep 17 00:00:00 2001 From: Igor Beylin Date: Fri, 31 Jul 2026 22:02:04 +0000 Subject: [PATCH 3/3] ci: cut divergent classifier, converge on l9-ci-core@v1 kernels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverse the repo-local classifier-first pipeline (a one-repo snowflake that followed the auditor's candidate spec) and align with the org's actual, live source of truth: l9-ci-core's language-aware @v1 compatibility kernels (verified against l9-ci-core HEAD and the @v1 tag — pr-pipeline.yml@v1 already detects Node and runs ESLint/tsc/Vitest; trio-governance.yml@v1 no-ops on non-tiered repos). Cut (divergent classifier architecture): - .github/scripts/classify_pr.py - .github/workflows/pr-pipeline.yml (repo-local canonical gate) - .github/governance/{routing_policy,blocking_policy,comment_protocol}.yaml - .github/labels.yml - docs/ci/convergence.md Rationalize the lint/typecheck/test overlap (was running 4x): - Remove .github/workflows/l9-lint-test-node.yml (copied template; its lint/typecheck/test are already provided by l9-pr-pipeline.yml@v1). - Remove .github/workflows/l9-node-ts-monorepo.yml (repo-local snowflake that duplicated typecheck/test and double-delegated the security/scorecard kernels already called by l9-security.yml / l9-scorecard.yml). Kept: - actions/checkout v4 -> v6 across the surviving workflows (org standard, SHA-pinned d23441a...; not divergent). - ci.yml as the repo-owned comprehensive contract gate (eslint boundary probe, package tarball contract + sha, declaration-consumer compile, 3-node matrix, npm audit, artifact roundtrip) — coverage no kernel provides. - All l9-* @v1 kernel callers + l9-analysis (v2 governed preset) + publish.yml + supply-chain.yml. Lint/typecheck/test now run in exactly two places with distinct roles: the org l9-pr-pipeline@v1 baseline-hygiene kernel and ci.yml's comprehensive matrix. NOTE (settings plane, not changed here): if branch protection required checks referenced the removed workflows' jobs, update the required-check list after merge or PRs will hang on never-reported checks. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01P1xeNSemqdnFScWGvUayHZ --- .github/governance/blocking_policy.yaml | 37 ---- .github/governance/comment_protocol.yaml | 19 -- .github/governance/routing_policy.yaml | 48 ----- .github/labels.yml | 59 ------ .github/scripts/classify_pr.py | 212 ---------------------- .github/workflows/l9-lint-test-node.yml | 156 ---------------- .github/workflows/l9-node-ts-monorepo.yml | 84 --------- .github/workflows/pr-pipeline.yml | 179 ------------------ docs/ci/convergence.md | 61 ------- 9 files changed, 855 deletions(-) delete mode 100644 .github/governance/blocking_policy.yaml delete mode 100644 .github/governance/comment_protocol.yaml delete mode 100644 .github/governance/routing_policy.yaml delete mode 100644 .github/labels.yml delete mode 100644 .github/scripts/classify_pr.py delete mode 100644 .github/workflows/l9-lint-test-node.yml delete mode 100644 .github/workflows/l9-node-ts-monorepo.yml delete mode 100644 .github/workflows/pr-pipeline.yml delete mode 100644 docs/ci/convergence.md diff --git a/.github/governance/blocking_policy.yaml b/.github/governance/blocking_policy.yaml deleted file mode 100644 index 80d7528..0000000 --- a/.github/governance/blocking_policy.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# L9 blocking vs advisory policy for @quantum-l9/llm-router. -# "PR Pipeline Gate" is the single canonical merge gate (see pr-pipeline.yml). -# Inherited debt is visible but advisory unless a touched file worsens it. -blocking_policy: - version: 1 - merge_gate: "PR Pipeline Gate" - blocking_jobs: - - lint - - build - - test - - security - advisory_jobs: - - dependency-review - - secret-scan - unknown_class_behavior: block_all # fail closed - inherited_debt_behavior: advisory_unless_touched - - hard_block_if_touched: - - secret_leak - - auth_bypass - - security_boundary_weakening # provider transport boundary (eslint lint:boundary) - - transport_packet_contract_break # OpenAI SDK transport clients - - image_ingress_validation_bypass # src/vision URL/inline-image validation - - budget_reservation_bypass # src/budget pre-dispatch reservation - - contract_bound_change_without_contract_update # src/control-plane contracts - - classifier_failure - - unknown_high_or_critical_security_finding - - advisory_if_untouched: - - inherited_style_debt - - inherited_docstring_findings - - historical_non_touched_high_findings - - fail_closed: - - diff_unknown - - classifier_failure - - missing_required_ci_context diff --git a/.github/governance/comment_protocol.yaml b/.github/governance/comment_protocol.yaml deleted file mode 100644 index 0e51cd1..0000000 --- a/.github/governance/comment_protocol.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# L9 PR comment protocol. Persistent governance comments MUST use a stable -# marker and update in place — never duplicate. Prepares the agent-review loop -# (not enabled yet). See the l9-github-ci kernel for the canonical layout. -comment_protocol: - version: 1 - bot_identity: "github-actions[bot]" - update_existing_comment: true - duplicate_persistent_comments_allowed: false - max_comment_chars: 65336 - markers: - ci_summary: "" - audit: "" - contract_control: "" - security: "" - future_agent_review: "" - rules: - - "Find existing comment by exact marker; update it in place." - - "Never match by title alone; never create duplicate persistent comments." - - "Truncate to the max length with an explicit truncation notice." diff --git a/.github/governance/routing_policy.yaml b/.github/governance/routing_policy.yaml deleted file mode 100644 index ae63292..0000000 --- a/.github/governance/routing_policy.yaml +++ /dev/null @@ -1,48 +0,0 @@ -# L9 CI routing policy for @quantum-l9/llm-router (TypeScript / Node). -# Consumed by .github/scripts/classify_pr.py and .github/workflows/pr-pipeline.yml. -# Primary signal is changed files; labels are secondary hints that may upgrade -# relevance but never downgrade a touched security surface. -routing_policy: - version: 1 - primary_signal: changed_files - secondary_signal: labels - conflict_resolution: evidence_beats_labels - unknown_diff_behavior: fail_closed - classes: - - name: docs_only - patterns: ["*.md", "docs/**", "LICENSE", ".github/ISSUE_TEMPLATE/**"] - gates: [] - advisory: [lint] - - name: ci_workflow - patterns: [".github/workflows/**"] - gates: [lint] - advisory: [security] - - name: tests_only - patterns: ["tests/**", "**/*.test.ts", "**/*.spec.ts"] - gates: [lint, test] - - name: dependency - patterns: ["package.json", "package-lock.json", ".npmrc"] - gates: [lint, build, test, security] - - name: compliance - patterns: - - "src/control-plane/**" - - "src/matrices/**" - - "fixtures/control-plane/**" - - ".github/governance/**" - gates: [lint, build, test] - - name: security - patterns: - - "src/providers/**" - - "src/vision/**" - - "src/budget/**" - - "eslint.config.js" - - "scripts/verify-eslint-boundary.mjs" - gates: [lint, build, test, security] - requires_human_review: true - - name: app_code - patterns: ["src/**", "scripts/**"] - gates: [lint, build, test, security] - - name: unknown - patterns: ["*"] - gates: [lint, build, test, security] - policy: fail_closed diff --git a/.github/labels.yml b/.github/labels.yml deleted file mode 100644 index 45ff888..0000000 --- a/.github/labels.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Canonical namespaced label taxonomy for @quantum-l9/llm-router. -# Source of truth for the L9 label contract (l9-github-ci kernel §9 + -# l9_target_ci_spec labels). Plain labels (ci, security, testing, typing, -# docker, python, dependencies, github-actions) are deprecated and must not be -# created by automation. Provisioning to the GitHub UI is a settings-plane -# action (see docs/ci/convergence.md) — this file does not create labels itself. - -automation: # color: 5319E7 - - automation:dependabot - - automation:coderabbit - - automation:perplexity - - automation:github-actions - - automation:gitguardian - - automation:sonarcloud - - automation:codex - - automation:l9-agent - -type: # color: 0E8A16 - - type:ci - - type:security - - type:docs - - type:test - - type:deps - - type:refactor - - type:governance - - type:bug - - type:feature - - type:release - -area: # color: 1D76DB - - area:workflows - - area:l9 - - area:contracts - - area:api - - area:typing - - area:runtime - - area:transport # provider transport boundary (src/providers) - - area:vision # image ingress validation (src/vision) - - area:budget # budget reservation (src/budget) - - area:infra - -risk: # color: D93F0B - - risk:blocking - - risk:advisory - - risk:secret - - risk:deploy - - risk:contract - - risk:runtime - - risk:security - -deprecated_plain_labels_forbidden: - - ci - - security - - testing - - typing - - docker - - python - - dependencies - - github-actions diff --git a/.github/scripts/classify_pr.py b/.github/scripts/classify_pr.py deleted file mode 100644 index 9db62a3..0000000 --- a/.github/scripts/classify_pr.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python3 -"""L9 PR classifier for @quantum-l9/llm-router. - -Adapted from the L9 shared CI model for a TypeScript / Node package. The routing -logic is language-agnostic; the file-pattern surfaces are tuned to this repo -(TypeScript source under src/, Vitest tests under tests/, npm dependency -manifests, Control Plane contracts). - -Contract (see .github/governance/routing_policy.yaml and the l9-github-ci kernel): - - Changed files are the primary signal. Labels are secondary hints only and may - never downgrade a touched security surface. - - Unknown diffs fail closed: conservative core gates run. - -Emits both the rich L9 classifier outputs (pr_class + *_changed surfaces) and the -run_* routing booleans consumed by pr-pipeline.yml. Standard library only. -""" - -from __future__ import annotations - -import fnmatch -import os -import subprocess -import sys - -# --- surface patterns (primary signal) ------------------------------------- - -PATTERNS = { - "docs": ["*.md", "docs/**", "LICENSE", "*.txt", ".github/ISSUE_TEMPLATE/**"], - "workflows": [".github/workflows/**"], - "scripts": ["scripts/**", ".github/scripts/**"], - "app": ["src/**"], - "tests": ["tests/**", "*.test.ts", "*.spec.ts", "**/*.test.ts", "**/*.spec.ts"], - "docker": ["Dockerfile", "Dockerfile.*", "*.dockerfile", "docker/**", ".dockerignore"], - "dependency": ["package.json", "package-lock.json", "npm-shrinkwrap.json", ".npmrc"], - "dependency_types": ["**/@types/**"], - "contracts": [ - "src/control-plane/**", - "src/matrices/**", - "fixtures/control-plane/**", - "tests/control-plane/**", - ".github/governance/**", - ], - "security_sensitive": [ - "src/providers/**", - "src/vision/**", - "src/budget/**", - "scripts/verify-eslint-boundary.mjs", - "eslint.config.js", - ".github/workflows/**security*", - ".github/workflows/supply-chain.yml", - ], - "typing_sensitive": ["tsconfig*.json", "**/*.d.ts", "src/**/types.ts", "src/**/*.types.ts"], - "transport_sensitive": ["src/providers/**"], - "ingress_sensitive": ["src/vision/**"], - "python": ["*.py", "**/*.py"], -} - - -def _match(path: str, globs: list[str]) -> bool: - for g in globs: - if fnmatch.fnmatch(path, g): - return True - # support "dir/**" prefix matching for nested paths - if g.endswith("/**") and (path == g[:-3] or path.startswith(g[:-2])): - return True - return False - - -def changed_files() -> list[str]: - """Resolve the PR's changed files. Prefer an explicit CHANGED_FILES env - (newline separated), otherwise diff against the base ref.""" - explicit = os.environ.get("CHANGED_FILES", "").strip() - if explicit: - return [f.strip() for f in explicit.splitlines() if f.strip()] - - base = os.environ.get("GITHUB_BASE_REF") or "main" - for ref in (f"origin/{base}", base): - try: - out = subprocess.run( - ["git", "diff", "--name-only", f"{ref}...HEAD"], - capture_output=True, text=True, check=True, - ).stdout - files = [f.strip() for f in out.splitlines() if f.strip()] - if files: - return files - except subprocess.CalledProcessError: - continue - return [] - - -def classify(files: list[str], labels: list[str]) -> dict: - surfaces = {name: False for name in PATTERNS} - matched_any = {f: False for f in files} - - for f in files: - for name, globs in PATTERNS.items(): - if _match(f, globs): - surfaces[name] = True - matched_any[f] = True - - # A file that matched no known surface makes the diff unknown -> fail closed. - diff_unknown = (not files) or any(not m for m in matched_any.values()) - - # Label hints may UPGRADE relevance but never downgrade a touched security - # surface. Evidence beats labels. - label_set = {l.lower() for l in labels} - if "risk:security" in label_set or "type:security" in label_set: - surfaces["security_sensitive"] = surfaces["security_sensitive"] or True - - # pr_class precedence: most safety-critical wins. - if surfaces["security_sensitive"]: - pr_class = "security" - elif surfaces["contracts"]: - pr_class = "compliance" - elif surfaces["docker"]: - pr_class = "docker" - elif surfaces["dependency_types"] and not (surfaces["app"] or surfaces["contracts"]): - pr_class = "dependency_types" - elif surfaces["dependency"] and not (surfaces["app"] or surfaces["contracts"]): - pr_class = "dependency" - elif surfaces["app"]: - pr_class = "app_code" - elif surfaces["tests"] and not surfaces["app"]: - pr_class = "tests_only" - elif surfaces["workflows"] and not (surfaces["app"] or surfaces["scripts"]): - pr_class = "ci_workflow" - elif surfaces["scripts"]: - pr_class = "app_code" - elif surfaces["docs"] and not diff_unknown: - pr_class = "docs_only" - elif diff_unknown: - pr_class = "unknown_diff" - else: - pr_class = "unknown" - - is_docs_only = pr_class == "docs_only" - - # run_* routing (consumed by pr-pipeline.yml). Fail closed on unknown. - fail_closed = pr_class in ("unknown_diff", "unknown") - run_lint = not is_docs_only or fail_closed - run_build = surfaces["app"] or surfaces["contracts"] or surfaces["typing_sensitive"] \ - or surfaces["dependency"] or surfaces["scripts"] or fail_closed - run_test = surfaces["app"] or surfaces["tests"] or surfaces["contracts"] \ - or surfaces["dependency"] or fail_closed - run_security = surfaces["security_sensitive"] or surfaces["dependency"] \ - or surfaces["app"] or fail_closed - run_infrastructure = surfaces["workflows"] or surfaces["docker"] - requires_human_review = pr_class in ("security", "compliance", "unknown_diff") - - semgrep_relevant = surfaces["app"] or surfaces["scripts"] or surfaces["security_sensitive"] - sbom_relevant = surfaces["dependency"] or surfaces["app"] - scorecard_relevant = surfaces["workflows"] or surfaces["dependency"] - - return { - "pr_class": pr_class, - "all_changed_files": ";".join(files), - "changed_count": str(len(files)), - "diff_unknown": str(diff_unknown).lower(), - "labels": ",".join(labels), - "detected_labels": ",".join(labels), - "python_changed": str(surfaces["python"]).lower(), - "app_changed": str(surfaces["app"]).lower(), - "tests_changed": str(surfaces["tests"]).lower(), - "docs_changed": str(surfaces["docs"]).lower(), - "workflows_changed": str(surfaces["workflows"]).lower(), - "scripts_changed": str(surfaces["scripts"]).lower(), - "docker_changed": str(surfaces["docker"]).lower(), - "dependency_changed": str(surfaces["dependency"]).lower(), - "contracts_changed": str(surfaces["contracts"]).lower(), - "security_sensitive_changed": str(surfaces["security_sensitive"]).lower(), - "typing_sensitive_changed": str(surfaces["typing_sensitive"]).lower(), - "transport_sensitive_changed": str(surfaces["transport_sensitive"]).lower(), - "ingress_sensitive_changed": str(surfaces["ingress_sensitive"]).lower(), - "semgrep_relevant": str(semgrep_relevant).lower(), - "sbom_relevant": str(sbom_relevant).lower(), - "scorecard_relevant": str(scorecard_relevant).lower(), - # routing booleans - "run_lint": str(run_lint).lower(), - "run_build": str(run_build).lower(), - "run_test": str(run_test).lower(), - "run_security": str(run_security).lower(), - "run_infrastructure": str(run_infrastructure).lower(), - "is_docs_only": str(is_docs_only).lower(), - "requires_human_review": str(requires_human_review).lower(), - } - - -def main() -> int: - labels_env = os.environ.get("PR_LABELS", "").strip() - labels = [l.strip() for l in labels_env.replace("\n", ",").split(",") if l.strip()] - files = changed_files() - result = classify(files, labels) - - # Emit to GITHUB_OUTPUT if present, else stdout (local runs). - out_path = os.environ.get("GITHUB_OUTPUT") - lines = [f"{k}={v}" for k, v in result.items()] - if out_path: - with open(out_path, "a", encoding="utf-8") as fh: - fh.write("\n".join(lines) + "\n") - - print("L9 PR classifier") - print(f" changed files : {result['changed_count']}") - print(f" pr_class : {result['pr_class']}") - print(f" diff_unknown : {result['diff_unknown']}") - print(f" run_lint/build/test/security: " - f"{result['run_lint']}/{result['run_build']}/" - f"{result['run_test']}/{result['run_security']}") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.github/workflows/l9-lint-test-node.yml b/.github/workflows/l9-lint-test-node.yml deleted file mode 100644 index 5efa2e4..0000000 --- a/.github/workflows/l9-lint-test-node.yml +++ /dev/null @@ -1,156 +0,0 @@ -# L9 consumer lint + test workflow for Node.js / TypeScript -# -# Copied from Quantum-L9/l9-ci-core docs/templates/l9-lint-test-node.yml. -# Node counterpart to l9-lint-test.yml (Python). Provides the TypeScript/Node -# hygiene half that v1's pr-pipeline never covered. -# -# Three independent gates (each surfaces as its own required check): -# lint -> eslint . lint / style / correctness (your eslint config) -# typecheck -> tsc --noEmit type-check the whole tree against the type -# system (honors tsconfig, e.g. strict: true). -# NO compiled output; it proves the types are -# sound, it does not run the code. -# test -> vitest run one-shot, non-interactive run of *.test.ts. -# `run` is mandatory in CI — bare `vitest` is -# watch mode and would hang the job forever. -# -# v2 conventions: immutable event-revision checkout, contents: read only (no -# write scopes). Node comes preinstalled on ubuntu-latest; see the optional -# setup-node block to pin a specific version. Package manager auto-detected -# from the lockfile (npm / pnpm / yarn). -name: L9 Lint and Test (Node) - -on: - pull_request: - push: - branches: - - main - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: l9-lint-test-node-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# ── Reusable anchors kept inline per job (templates must stay copy-paste-able). -# Each job: immutable checkout -> install deps -> run one gate. - -jobs: - lint: - name: ESLint - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - steps: - - name: Checkout immutable event revision - env: - REPOSITORY: ${{ github.repository }} - REVISION: ${{ github.sha }} - TOKEN: ${{ github.token }} - run: | - set -euo pipefail - git init . - git remote add origin \ - "https://x-access-token:${TOKEN}@github.com/${REPOSITORY}.git" - git -c protocol.version=2 fetch --depth=1 origin "${REVISION}" - git checkout --detach FETCH_HEAD - git remote set-url origin "https://github.com/${REPOSITORY}.git" - # OPTIONAL — pin Node. ubuntu-latest ships Node 20+, used as-is. To pin, - # uncomment and set a full 40-char SHA you trust for actions/setup-node: - # - uses: actions/setup-node@ # v4.x - # with: - # node-version-file: package.json # or .nvmrc, or node-version: "20" - - name: Install dependencies - run: | - set -euo pipefail - corepack enable || true - if [ -f pnpm-lock.yaml ]; then - corepack prepare pnpm@latest --activate && pnpm install --frozen-lockfile - elif [ -f yarn.lock ]; then - corepack prepare yarn@stable --activate && yarn install --immutable - elif [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then - npm ci - else - npm install - fi - - name: ESLint - run: npx --no-install eslint . || npx eslint . - - typecheck: - name: tsc --noEmit - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: read - steps: - - name: Checkout immutable event revision - env: - REPOSITORY: ${{ github.repository }} - REVISION: ${{ github.sha }} - TOKEN: ${{ github.token }} - run: | - set -euo pipefail - git init . - git remote add origin \ - "https://x-access-token:${TOKEN}@github.com/${REPOSITORY}.git" - git -c protocol.version=2 fetch --depth=1 origin "${REVISION}" - git checkout --detach FETCH_HEAD - git remote set-url origin "https://github.com/${REPOSITORY}.git" - - name: Install dependencies - run: | - set -euo pipefail - corepack enable || true - if [ -f pnpm-lock.yaml ]; then - corepack prepare pnpm@latest --activate && pnpm install --frozen-lockfile - elif [ -f yarn.lock ]; then - corepack prepare yarn@stable --activate && yarn install --immutable - elif [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then - npm ci - else - npm install - fi - - name: Type-check (tsc --noEmit) - run: npx --no-install tsc --noEmit || npx tsc --noEmit - - test: - name: Vitest - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: read - steps: - - name: Checkout immutable event revision - env: - REPOSITORY: ${{ github.repository }} - REVISION: ${{ github.sha }} - TOKEN: ${{ github.token }} - run: | - set -euo pipefail - git init . - git remote add origin \ - "https://x-access-token:${TOKEN}@github.com/${REPOSITORY}.git" - git -c protocol.version=2 fetch --depth=1 origin "${REVISION}" - git checkout --detach FETCH_HEAD - git remote set-url origin "https://github.com/${REPOSITORY}.git" - - name: Install dependencies - run: | - set -euo pipefail - corepack enable || true - if [ -f pnpm-lock.yaml ]; then - corepack prepare pnpm@latest --activate && pnpm install --frozen-lockfile - elif [ -f yarn.lock ]; then - corepack prepare yarn@stable --activate && yarn install --immutable - elif [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then - npm ci - else - npm install - fi - - name: Vitest (one-shot, non-interactive) - env: - CI: "true" - run: npx --no-install vitest run || npx vitest run - # For coverage, install @vitest/coverage-v8 and use: - # npx vitest run --coverage diff --git a/.github/workflows/l9-node-ts-monorepo.yml b/.github/workflows/l9-node-ts-monorepo.yml deleted file mode 100644 index 3d1b2fd..0000000 --- a/.github/workflows/l9-node-ts-monorepo.yml +++ /dev/null @@ -1,84 +0,0 @@ -# Quantum-L9 org starter — Node/TypeScript monorepo pipeline. -# Inline jobs for install/typecheck/test/publish-dry-run. -# Security and Scorecard delegate to l9-ci-core kernels; do not add logic to those jobs. -name: "L9 Node/TS Monorepo Pipeline" -on: - pull_request: - branches: [main, 'release/**'] - push: - branches: [main] - -jobs: - install-and-typecheck: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: "20" - cache: "npm" - - name: Install dependencies - run: npm ci - - name: Type-check - run: | - if node -e "const s=(require('./package.json').scripts||{});process.exit(s.typecheck?0:1)"; then - npm run typecheck - elif node -e "const s=(require('./package.json').scripts||{});process.exit(s['verify:types']?0:1)"; then - npm run verify:types - else - npx tsc --noEmit - fi - - test-by-segment: - needs: install-and-typecheck - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: "20" - cache: "npm" - - name: Install dependencies - run: npm ci - - name: Run tests (Vitest) - run: npx vitest run - - publish-dry-run: - needs: test-by-segment - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: "20" - cache: "npm" - - name: Install dependencies - run: npm ci - - name: Publish dry-run (all public packages) - run: | - dirs="." - if [ -d "packages" ]; then - dirs="packages/*/" - fi - for pkg_dir in $dirs; do - pkg_json="${pkg_dir%/}/package.json" - if [ ! -f "$pkg_json" ]; then continue; fi - private=$(node -e "const p=require('./$pkg_json'); console.log(p.private === true)") - if [ "$private" = "false" ]; then - echo "--- Dry-run: $pkg_dir ---" - (cd "$pkg_dir" && npm publish --dry-run) - else - echo "--- Skipping private package: $pkg_dir ---" - fi - done - - security: - uses: Quantum-L9/l9-ci-core/.github/workflows/security.yml@v1 - secrets: inherit - with: - python-version: "3.12" - run-npm-audit: true - - scorecard: - uses: Quantum-L9/l9-ci-core/.github/workflows/scorecard.yml@v1 - secrets: inherit diff --git a/.github/workflows/pr-pipeline.yml b/.github/workflows/pr-pipeline.yml deleted file mode 100644 index 6d45a01..0000000 --- a/.github/workflows/pr-pipeline.yml +++ /dev/null @@ -1,179 +0,0 @@ -# Canonical L9 merge gate for @quantum-l9/llm-router. -# -# CI is a router, not a bouncer: classify the PR from its changed files, run -# only the relevant Node/TS gates, and summarize merge truth in ONE required -# check — "PR Pipeline Gate". This is the intended branch-protection required -# check; enforcement is configured in GitHub Settings (see docs/ci/convergence.md). -# -# Adapted from the L9 shared model (Python) to this TypeScript package: gates map -# to the repo's own npm scripts (eslint, tsc, vitest, npm audit, boundary probe). -# Actions are SHA-pinned (Scorecard hardening) with a version comment. -name: PR Pipeline Gate - -on: - pull_request: - branches: [main, 'release/**'] - -permissions: - contents: read - -concurrency: - group: pr-pipeline-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - NODE_VERSION: "20.19.0" - -jobs: - classify: - name: Classify - runs-on: ubuntu-latest - timeout-minutes: 10 - outputs: - pr_class: ${{ steps.classify.outputs.pr_class }} - run_lint: ${{ steps.classify.outputs.run_lint }} - run_build: ${{ steps.classify.outputs.run_build }} - run_test: ${{ steps.classify.outputs.run_test }} - run_security: ${{ steps.classify.outputs.run_security }} - is_docs_only: ${{ steps.classify.outputs.is_docs_only }} - diff_unknown: ${{ steps.classify.outputs.diff_unknown }} - requires_human_review: ${{ steps.classify.outputs.requires_human_review }} - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - with: - fetch-depth: 0 - - id: classify - name: Classify PR - env: - GITHUB_BASE_REF: ${{ github.base_ref }} - PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} - run: python3 .github/scripts/classify_pr.py - - lint: - name: Lint - needs: classify - if: needs.classify.outputs.run_lint == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: npm - - run: npm ci --ignore-scripts - - run: npm run lint - - run: npm run lint:boundary - - build: - name: Build & Types - needs: classify - if: needs.classify.outputs.run_build == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: npm - - run: npm ci --ignore-scripts - - run: npm run build - - run: npm run verify:types - - run: npm run verify:declarations - - test: - name: Test - needs: classify - if: needs.classify.outputs.run_test == 'true' - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: npm - - run: npm ci --ignore-scripts - - run: npm test - - security: - name: Security - needs: classify - if: needs.classify.outputs.run_security == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 - with: - node-version: ${{ env.NODE_VERSION }} - cache: npm - - run: npm ci --ignore-scripts - - run: npm audit --audit-level=high --omit=dev - - run: npm run verify:package - - gate: - name: PR Pipeline Gate - needs: [classify, lint, build, test, security] - if: always() - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Evaluate canonical merge gate - env: - PR_CLASS: ${{ needs.classify.outputs.pr_class }} - DIFF_UNKNOWN: ${{ needs.classify.outputs.diff_unknown }} - HUMAN_REVIEW: ${{ needs.classify.outputs.requires_human_review }} - R_CLASSIFY: ${{ needs.classify.result }} - R_LINT: ${{ needs.lint.result }} - R_BUILD: ${{ needs.build.result }} - R_TEST: ${{ needs.test.result }} - R_SECURITY: ${{ needs.security.result }} - run: | - set -euo pipefail - fail=0 - summary=".github pr-pipeline gate summary" - note() { echo "$1"; summary="${summary} - - $1"; } - - note "pr_class: ${PR_CLASS:-unknown}" - note "classify: ${R_CLASSIFY}" - - # Classifier itself must succeed (fail closed). - if [ "${R_CLASSIFY}" != "success" ]; then - note "BLOCK: classifier did not succeed"; fail=1 - fi - if [ "${DIFF_UNKNOWN}" = "true" ]; then - note "NOTE: diff_unknown -> conservative core gates ran (fail closed)" - fi - - # A routed gate blocks only when it actually ran and failed. - # 'skipped' is a legitimate route decision, never a failure. - for pair in "lint:${R_LINT}" "build:${R_BUILD}" "test:${R_TEST}" "security:${R_SECURITY}"; do - name="${pair%%:*}"; result="${pair##*:}" - if [ "${result}" = "failure" ]; then - note "BLOCK: ${name} failed"; fail=1 - elif [ "${result}" = "cancelled" ]; then - note "BLOCK: ${name} cancelled"; fail=1 - else - note "ok: ${name} = ${result}" - fi - done - - if [ "${HUMAN_REVIEW}" = "true" ]; then - note "ADVISORY: touched security/compliance/unknown surface — human review recommended" - fi - - { - echo "## PR Pipeline Gate" - echo "" - echo "\`\`\`" - echo "${summary}" - echo "\`\`\`" - } >> "${GITHUB_STEP_SUMMARY}" - - if [ "${fail}" -ne 0 ]; then - echo "Gate FAILED"; exit 1 - fi - echo "Gate PASSED" diff --git a/docs/ci/convergence.md b/docs/ci/convergence.md deleted file mode 100644 index 70417d3..0000000 --- a/docs/ci/convergence.md +++ /dev/null @@ -1,61 +0,0 @@ -# L9 CI Convergence — @quantum-l9/llm-router - -This repo is a **TypeScript / npm** package. The L9 shared CI target model is -authored around Python repos, so convergence here is an *adaptation*: the routing -architecture (classifier-first, one canonical gate, namespaced labels, blocking -vs advisory policy) is adopted verbatim, while the gates themselves map to this -repo's own npm scripts instead of `ruff`/`pytest`/`semgrep`. - -## What was implemented in-repo (file plane) - -| Area | File | Notes | -|---|---|---| -| Classifier | `.github/scripts/classify_pr.py` | Python 3 stdlib; TS-aware surfaces (src/providers, src/vision, src/budget, src/control-plane). Emits the full L9 output set + `run_*` routing booleans. Unknown diffs **fail closed**. | -| Canonical gate | `.github/workflows/pr-pipeline.yml` | Job **`PR Pipeline Gate`** aggregates classify → lint → build → test → security using this repo's npm scripts. `if: always()`, `contents: read`, `concurrency` + `cancel-in-progress`. | -| Routing policy | `.github/governance/routing_policy.yaml` | Changed-files primary, labels secondary, evidence-beats-labels. | -| Blocking policy | `.github/governance/blocking_policy.yaml` | Hard-block-if-touched surfaces (transport boundary, image ingress, budget reservation, control-plane contracts); inherited debt advisory. | -| Comment protocol | `.github/governance/comment_protocol.yaml` | Stable markers, update-in-place, no duplicates. | -| Label taxonomy | `.github/labels.yml` | Namespaced source of truth (`automation:` / `type:` / `area:` / `risk:`). | -| Workflow standard | all `.github/workflows/*.yml` | `actions/checkout` upgraded v4 → **v6** (`d23441a…`), SHA-pinned to preserve Scorecard hardening. | - -The classifier and every npm gate (`lint`, `lint:boundary`, `build`, -`verify:types`, `verify:declarations`, `test`, `npm audit`, `verify:package`) -were run locally and pass. - -## Settings plane — requires GitHub admin, NOT changed by this branch - -These cannot be set from repo files and were **not** modified. Apply with -explicit approval. - -### 1. Provision namespaced labels -Create every label in `.github/labels.yml`; delete the deprecated plain labels -(`ci`, `security`, `testing`, `typing`, `docker`, `python`, `dependencies`, -`github-actions`). Suggested colors are in the manifest. - -### 2. Branch protection on `main` -- Required check: **`PR Pipeline Gate`** (add others after they stabilize: - CodeQL / Gitleaks / GitGuardian / SonarCloud / Supply Chain). -- Require PR before merge; ≥1 approval; dismiss stale approvals; require - CODEOWNERS; require conversation resolution; require branches up to date; - linear history; disallow force-push and deletion. -- Merge strategy: squash-only; auto-delete head branches. - -> Status: **Unknown / unenforced.** Do not claim `PR Pipeline Gate` is enforced -> until GitHub → Settings → Branches confirms it is a required check. - -### 3. Secrets / variables (names only) -- Platform/org preferred: `SONAR_TOKEN`, `GITGUARDIAN_API_KEY`, `CODECOV_TOKEN`. -- Repo publish: `NODE_AUTH_TOKEN` (GitHub Packages, already used by `publish.yml`). - -## Known divergences deferred (roadmap) - -- **`l9-pr-pipeline.yml` / `l9-governance.yml`** call the org `l9-ci-core` - reusable kernels with `python-version: "3.12"` / a Python trio-governance - model. These are **language-mismatched** for a TS package. Left in place to - avoid breaking org wiring; resolving them belongs at the `l9-ci-core` org - level (a Node/TS kernel or a documented no-op path for TS repos). -- **GitGuardian / Gitleaks / SonarCloud / CodeQL** are `recommended_optional` - and deferred until the canonical gate is enforced and their tokens exist. -- Agent-review loop is **prepared, not enabled** — the foundation (stable - classifier outputs, machine-readable gate summary, marker protocol, - namespaced labels) is now in place.