@getarbiter/cli 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +713 -0
- package/LICENSE +202 -0
- package/NOTICE +31 -0
- package/PRIVACY.md +111 -0
- package/README.md +237 -0
- package/THIRD_PARTY_LICENSES.md +472 -0
- package/dist/.src-manifest.json +1 -0
- package/dist/capabilities/host-probe.js +3 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +87 -0
- package/dist/commands/adopt-policy.js +3 -0
- package/dist/commands/configure-interactive.js +3 -0
- package/dist/commands/configure.js +5 -0
- package/dist/commands/cross-model-review.js +3 -0
- package/dist/commands/diff.js +20 -0
- package/dist/commands/doc-set.js +3 -0
- package/dist/commands/doctor/clean.js +2 -0
- package/dist/commands/doctor/fail-open-census.js +12 -0
- package/dist/commands/doctor/health.js +11 -0
- package/dist/commands/doctor/prove-gates.js +13 -0
- package/dist/commands/doctor/recover-lock.js +12 -0
- package/dist/commands/doctor/repair-state.js +6 -0
- package/dist/commands/doctor/tool-pin-extract.js +4 -0
- package/dist/commands/doctor/tool-pins.js +9 -0
- package/dist/commands/doctor-interactive.js +2 -0
- package/dist/commands/doctor.js +2 -0
- package/dist/commands/explain.js +19 -0
- package/dist/commands/gate-exec.js +4 -0
- package/dist/commands/gold-audit.js +14 -0
- package/dist/commands/graph.js +5 -0
- package/dist/commands/init/build-arbiter-config.js +1 -0
- package/dist/commands/init/generate.js +42 -0
- package/dist/commands/init/github-setup.js +5 -0
- package/dist/commands/init/maturity-gates.js +10 -0
- package/dist/commands/init/resolve-config.js +3 -0
- package/dist/commands/init/toolchain.js +10 -0
- package/dist/commands/init/types.js +1 -0
- package/dist/commands/init.js +17 -0
- package/dist/commands/method-interactive.js +4 -0
- package/dist/commands/method.js +11 -0
- package/dist/commands/obsidian.js +2 -0
- package/dist/commands/plugin.js +8 -0
- package/dist/commands/pr-merged.js +2 -0
- package/dist/commands/retire-policy.js +3 -0
- package/dist/commands/review-diff.js +3 -0
- package/dist/commands/settings.js +7 -0
- package/dist/commands/ship-config.js +2 -0
- package/dist/commands/ship-profile.js +2 -0
- package/dist/commands/ship-review.js +1 -0
- package/dist/commands/ship-tier.js +2 -0
- package/dist/commands/ship-train.js +2 -0
- package/dist/commands/task-mark.js +2 -0
- package/dist/commands/task-note.js +3 -0
- package/dist/commands/task-record-red.js +2 -0
- package/dist/commands/task-record-tech-debt.js +4 -0
- package/dist/commands/task-ship.js +3 -0
- package/dist/commands/task-state.js +6 -0
- package/dist/commands/task.js +59 -0
- package/dist/commands/update.js +60 -0
- package/dist/commands/upgrade-level-interactive.js +2 -0
- package/dist/commands/upgrade-level.js +7 -0
- package/dist/commands/verify-plan.js +8 -0
- package/dist/commands/verify-tdd.js +2 -0
- package/dist/commands/verify.js +7 -0
- package/dist/commands/wizard/coherence.js +1 -0
- package/dist/commands/worktree-prune.js +4 -0
- package/dist/commands/worktree.js +39 -0
- package/dist/compatibility/cross-language-matrix.json +540 -0
- package/dist/compatibility/index.d.ts +4 -0
- package/dist/compatibility/index.js +2 -0
- package/dist/compatibility/matcher.js +1 -0
- package/dist/compatibility/matrix.json +27 -0
- package/dist/compatibility/parsers.d.ts +38 -0
- package/dist/compatibility/parsers.js +1 -0
- package/dist/compatibility/probe.d.ts +41 -0
- package/dist/compatibility/probe.js +4 -0
- package/dist/compatibility/report.js +2 -0
- package/dist/compatibility/schema.d.ts +80 -0
- package/dist/compatibility/schema.js +1 -0
- package/dist/compatibility/skills-matrix.json +98 -0
- package/dist/compatibility/skills-validator.d.ts +30 -0
- package/dist/compatibility/skills-validator.js +4 -0
- package/dist/compatibility/tooling-proposals.json +5 -0
- package/dist/config/arbiter-ignore.js +3 -0
- package/dist/config/collaboration-mode-defaults.js +1 -0
- package/dist/config/diff.js +1 -0
- package/dist/config/env-overrides.js +3 -0
- package/dist/config/env-registry.js +2 -0
- package/dist/config/levels.js +1 -0
- package/dist/config/migrations/index.js +2 -0
- package/dist/config/migrations/v0-to-v1.js +2 -0
- package/dist/config/migrations/v1-to-v2.js +2 -0
- package/dist/config/migrations/v2-to-v3.js +2 -0
- package/dist/config/migrations/v3-to-v4.js +2 -0
- package/dist/config/override-resolver.js +2 -0
- package/dist/config/resolve-project-config.js +2 -0
- package/dist/config/resolve-project-name.js +2 -0
- package/dist/config/schema.d.ts +480 -0
- package/dist/config/schema.js +1 -0
- package/dist/config/test-pyramid-profiles.js +1 -0
- package/dist/config/thresholds.js +2 -0
- package/dist/conformance/dimensions.js +3 -0
- package/dist/conformance/engine.js +7 -0
- package/dist/conformance/gate-proofs.js +4 -0
- package/dist/conformance/shared.js +2 -0
- package/dist/detectors/axis.js +1 -0
- package/dist/detectors/build.js +2 -0
- package/dist/detectors/existing.js +2 -0
- package/dist/detectors/external-model.js +2 -0
- package/dist/detectors/framework.js +2 -0
- package/dist/detectors/git.js +11 -0
- package/dist/detectors/github.js +2 -0
- package/dist/detectors/lanes.js +5 -0
- package/dist/detectors/language-hooks.js +117 -0
- package/dist/detectors/language.js +2 -0
- package/dist/detectors/package-manager.js +2 -0
- package/dist/detectors/package.js +2 -0
- package/dist/detectors/scaffold-wiring.js +2 -0
- package/dist/detectors/workflow-collision.js +4 -0
- package/dist/evidence/gate-binding.js +4 -0
- package/dist/evidence/git-checks.js +3 -0
- package/dist/evidence/load.js +2 -0
- package/dist/evidence/provenance.js +2 -0
- package/dist/evidence/summary.js +2 -0
- package/dist/evidence/tdd-reexecute.js +2 -0
- package/dist/evidence/tdd.js +4 -0
- package/dist/experimental/index.js +2 -0
- package/dist/experimental/parse-argv.js +2 -0
- package/dist/experimental/registry.js +2 -0
- package/dist/generators/agents-claude.js +2 -0
- package/dist/generators/agents-md.js +2 -0
- package/dist/generators/anti-drift-validators.js +3 -0
- package/dist/generators/anti-proforma.js +2 -0
- package/dist/generators/api-e2e.js +2 -0
- package/dist/generators/api-middleware.js +2 -0
- package/dist/generators/archunit.js +2 -0
- package/dist/generators/audit-toolchain.js +2 -0
- package/dist/generators/auth.js +2 -0
- package/dist/generators/behavioral-tests.js +2 -0
- package/dist/generators/boundaries.js +2 -0
- package/dist/generators/check-all.js +2 -0
- package/dist/generators/check-stack-conformity.js +2 -0
- package/dist/generators/ci-five-lane.js +2 -0
- package/dist/generators/ci-tier.js +2 -0
- package/dist/generators/claude.js +3 -0
- package/dist/generators/codex-hooks.js +2 -0
- package/dist/generators/codex-known-limitations.js +2 -0
- package/dist/generators/codex.js +15 -0
- package/dist/generators/commit-footer.js +2 -0
- package/dist/generators/compliance-menu.js +2 -0
- package/dist/generators/compliance.js +2 -0
- package/dist/generators/conformance.js +2 -0
- package/dist/generators/contract-testing.js +2 -0
- package/dist/generators/coverage.js +2 -0
- package/dist/generators/debt-gates.js +24 -0
- package/dist/generators/debt-ratchet.js +2 -0
- package/dist/generators/derived-class.js +2 -0
- package/dist/generators/doc-set.js +4 -0
- package/dist/generators/docs.js +2 -0
- package/dist/generators/duplication.js +2 -0
- package/dist/generators/e2e-constitution.js +2 -0
- package/dist/generators/env-template.js +2 -0
- package/dist/generators/evidence-retention.js +2 -0
- package/dist/generators/feature-matrix.js +2 -0
- package/dist/generators/frontend-governance.js +2 -0
- package/dist/generators/frontend-quality.js +2 -0
- package/dist/generators/gap.js +2 -0
- package/dist/generators/gdpr.js +2 -0
- package/dist/generators/githooks.js +2 -0
- package/dist/generators/github-setup.js +2 -0
- package/dist/generators/github.js +2 -0
- package/dist/generators/gitignore.js +2 -0
- package/dist/generators/global-invariants.js +3 -0
- package/dist/generators/go-boundaries.js +2 -0
- package/dist/generators/gold-kit.js +2 -0
- package/dist/generators/infra.js +2 -0
- package/dist/generators/integration-testing.js +7 -0
- package/dist/generators/iso27001.js +2 -0
- package/dist/generators/iso9001.js +2 -0
- package/dist/generators/labels.js +2 -0
- package/dist/generators/local-wrapper.js +2 -0
- package/dist/generators/modulith.js +2 -0
- package/dist/generators/mutation.js +2 -0
- package/dist/generators/observability.js +2 -0
- package/dist/generators/operations.js +2 -0
- package/dist/generators/perf-k6.js +2 -0
- package/dist/generators/pharma.js +2 -0
- package/dist/generators/playwright-python.js +2 -0
- package/dist/generators/playwright-ts.js +2 -0
- package/dist/generators/pr-tooling.js +2 -0
- package/dist/generators/python-boundaries.js +2 -0
- package/dist/generators/quality.js +2 -0
- package/dist/generators/registry.js +2 -0
- package/dist/generators/regulated.js +2 -0
- package/dist/generators/resilience.js +2 -0
- package/dist/generators/risk-register.js +2 -0
- package/dist/generators/root.js +2 -0
- package/dist/generators/rust-boundaries.js +2 -0
- package/dist/generators/safety-class.js +2 -0
- package/dist/generators/security.js +2 -0
- package/dist/generators/seed.js +2 -0
- package/dist/generators/self-validation.js +2 -0
- package/dist/generators/ship-driver.js +2 -0
- package/dist/generators/skill-names.json +17 -0
- package/dist/generators/skills.js +2 -0
- package/dist/generators/smoke-journeys.js +2 -0
- package/dist/generators/solo-exception.js +2 -0
- package/dist/generators/ssot.js +2 -0
- package/dist/generators/stride-enforcement.js +2 -0
- package/dist/generators/suppressions.js +2 -0
- package/dist/generators/test-pyramid-manifest.js +3 -0
- package/dist/generators/test-taxonomy.js +2 -0
- package/dist/generators/wiki.js +2 -0
- package/dist/github/branch-protection.js +2 -0
- package/dist/github/classify-gh-error.js +2 -0
- package/dist/github/labels.js +2 -0
- package/dist/github/project-board.js +2 -0
- package/dist/graph/builders/adr.js +4 -0
- package/dist/graph/builders/ast.js +3 -0
- package/dist/graph/builders/canon.js +2 -0
- package/dist/graph/builders/evidence.js +2 -0
- package/dist/graph/builders/inv.js +1 -0
- package/dist/graph/builders/req.js +3 -0
- package/dist/graph/builders/test-nodes.js +3 -0
- package/dist/graph/builders/utils.js +2 -0
- package/dist/graph/load.js +2 -0
- package/dist/graph/model.js +2 -0
- package/dist/graph/query.js +3 -0
- package/dist/graph/store.js +1 -0
- package/dist/i18n/en.json +293 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/types.js +2 -0
- package/dist/integrations/companions.js +2 -0
- package/dist/integrations/external-review.js +10 -0
- package/dist/integrations/plugin-schema.js +2 -0
- package/dist/integrations/skill-detector.js +7 -0
- package/dist/integrations/skills-matrix.js +2 -0
- package/dist/integrations/types.d.ts +23 -0
- package/dist/integrations/types.js +2 -0
- package/dist/internal/cli-deprecation-registry.js +2 -0
- package/dist/internal/deprecate.js +3 -0
- package/dist/internal/experimental-warn.js +3 -0
- package/dist/invariants/catalog.d.ts +2 -0
- package/dist/invariants/catalog.js +1 -0
- package/dist/invariants/filter.d.ts +28 -0
- package/dist/invariants/filter.js +1 -0
- package/dist/invariants/index.d.ts +4 -0
- package/dist/invariants/index.js +2 -0
- package/dist/invariants/tiers.js +1 -0
- package/dist/invariants/types.d.ts +1 -0
- package/dist/invariants/types.js +1 -0
- package/dist/kit/applicability.js +3 -0
- package/dist/kit/audit-report.js +3 -0
- package/dist/kit/brownfield-detect.js +4 -0
- package/dist/kit/canonical-mapping.json +1 -0
- package/dist/kit/catalog.js +2 -0
- package/dist/kit/catalog.json +1 -0
- package/dist/kit/derived.json +1 -0
- package/dist/kit/emit-issues.js +8 -0
- package/dist/kit/measure.js +5 -0
- package/dist/kit/redaction.js +3 -0
- package/dist/kit/schema.js +2 -0
- package/dist/kit/taxonomy.js +2 -0
- package/dist/kit/thresholds.d.ts +12 -0
- package/dist/kit/thresholds.js +2 -0
- package/dist/kit/wave-engine.js +1 -0
- package/dist/recipes/loader.js +6 -0
- package/dist/recipes/schema.js +2 -0
- package/dist/risk/classifier.js +2 -0
- package/dist/risk/sha-check.js +2 -0
- package/dist/state/backups.js +2 -0
- package/dist/state/envelope.js +2 -0
- package/dist/state/generated-manifest.js +3 -0
- package/dist/state/manifest-key.js +2 -0
- package/dist/state/migrations/index.js +1 -0
- package/dist/state/migrations/v0-to-v1.js +2 -0
- package/dist/state/retired-renders.js +2 -0
- package/dist/templates/.nvmrc.ejs +1 -0
- package/dist/templates/HANDOFF.template.md +45 -0
- package/dist/templates/__tests__/contract/error-shape.contract.test.ts.ejs +47 -0
- package/dist/templates/agents-md/AGENTS.md.ejs +517 -0
- package/dist/templates/api-e2e/README.md.ejs +33 -0
- package/dist/templates/api-e2e/go-httptest.go.ejs +43 -0
- package/dist/templates/api-e2e/java-restassured.java.ejs +28 -0
- package/dist/templates/api-e2e/kotlin-restassured.kt.ejs +30 -0
- package/dist/templates/api-e2e/manifest.json.ejs +8 -0
- package/dist/templates/api-e2e/postman.collection.json.ejs +30 -0
- package/dist/templates/api-e2e/python-httpx.py.ejs +17 -0
- package/dist/templates/api-e2e/run.sh.ejs +61 -0
- package/dist/templates/api-e2e/ts-supertest.test.ts.ejs +18 -0
- package/dist/templates/archunit/AntiCyclicTest.java.ejs +29 -0
- package/dist/templates/archunit/AntiProformaExempt.java.ejs +35 -0
- package/dist/templates/archunit/AntiProformaTest.java.ejs +104 -0
- package/dist/templates/archunit/ArchitectureTest.java.ejs +87 -0
- package/dist/templates/archunit/DependencyFlowTest.java.ejs +55 -0
- package/dist/templates/archunit/DomainPurityTest.java.ejs +47 -0
- package/dist/templates/archunit/NamingConventionsTest.java.ejs +54 -0
- package/dist/templates/archunit/NoH2ArchTest.java.ejs +30 -0
- package/dist/templates/archunit/NoMockMvcTest.java.ejs +71 -0
- package/dist/templates/archunit/PortsIndependenceTest.java.ejs +55 -0
- package/dist/templates/archunit/RestAssuredArchTest.java.ejs +32 -0
- package/dist/templates/archunit/RestAssuredBaseIT.java.ejs +66 -0
- package/dist/templates/archunit/TestCoverageArchTest.java.ejs +29 -0
- package/dist/templates/archunit/arch-test-deps-maven.md.ejs +42 -0
- package/dist/templates/archunit/arch-test-deps.gradle.ejs +14 -0
- package/dist/templates/audit/gdpr/check-gdpr-controls.mjs.ejs +102 -0
- package/dist/templates/audit/gdpr/controls-to-gates.md.ejs +42 -0
- package/dist/templates/audit/gdpr/data-flow-map.md.ejs +30 -0
- package/dist/templates/audit/gdpr/dpia.md.ejs +45 -0
- package/dist/templates/audit/generic/audit-gate-rules.md.ejs +24 -0
- package/dist/templates/audit/generic/audit-trail-policy.md.ejs +38 -0
- package/dist/templates/audit/iso27001/iso27001-controls-gate-map.md.ejs +60 -0
- package/dist/templates/auth/setup.md.ejs +661 -0
- package/dist/templates/behavioral-tests/ExampleBehavioralTest.java.ejs +67 -0
- package/dist/templates/behavioral-tests/TESTING_POLICY.md.ejs +116 -0
- package/dist/templates/behavioral-tests/bdd/ExampleBddIT.java.ejs +19 -0
- package/dist/templates/behavioral-tests/bdd/ExampleSteps.java.ejs +61 -0
- package/dist/templates/behavioral-tests/bdd/example.feature.ejs +14 -0
- package/dist/templates/behavioral-tests/bdd/example.steps.ts.ejs +37 -0
- package/dist/templates/behavioral-tests/bdd/example_bdd_test.rs.ejs +64 -0
- package/dist/templates/behavioral-tests/bdd/example_test.go.ejs +80 -0
- package/dist/templates/behavioral-tests/bdd/test_example_bdd.py.ejs +47 -0
- package/dist/templates/behavioral-tests/eslint-playwright.json.ejs +10 -0
- package/dist/templates/behavioral-tests/eslint.config.playwright.mjs.ejs +29 -0
- package/dist/templates/behavioral-tests/example.behavioral.test.ts.ejs +47 -0
- package/dist/templates/behavioral-tests/example_behavioral_test.go.ejs +51 -0
- package/dist/templates/behavioral-tests/example_behavioral_test.rs.ejs +47 -0
- package/dist/templates/behavioral-tests/test_example_behavioral.py.ejs +40 -0
- package/dist/templates/boundaries/.eslintrc-boundaries.cjs.ejs +81 -0
- package/dist/templates/boundaries/.eslintrc-frontend-spa.cjs.ejs +79 -0
- package/dist/templates/boundaries/.importlinter.ejs +48 -0
- package/dist/templates/boundaries/check-boundaries-go.mjs.ejs +94 -0
- package/dist/templates/boundaries/check-boundaries-python.mjs.ejs +118 -0
- package/dist/templates/boundaries/check-boundaries-rust.mjs.ejs +111 -0
- package/dist/templates/boundaries/check-boundaries.mjs.ejs +34 -0
- package/dist/templates/boundaries/clippy.toml.ejs +50 -0
- package/dist/templates/boundaries/deny.toml.ejs +56 -0
- package/dist/templates/boundaries/eslint.config.boundaries.mjs.ejs +112 -0
- package/dist/templates/boundaries/eslint.config.frontend-spa.mjs.ejs +93 -0
- package/dist/templates/boundaries/golangci-boundaries.yml.ejs +70 -0
- package/dist/templates/boundaries/ruff-boundaries.toml.ejs +32 -0
- package/dist/templates/claude/CLAUDE.md.ejs +78 -0
- package/dist/templates/claude/agents/agent-write-classes.json +30 -0
- package/dist/templates/claude/agents/bridge-reviewer.md.ejs +68 -0
- package/dist/templates/claude/agents/codebase-scanner.md.ejs +69 -0
- package/dist/templates/claude/agents/context-checker.md.ejs +73 -0
- package/dist/templates/claude/agents/red-team.md.ejs +134 -0
- package/dist/templates/claude/commands/close-gold-gap.md.ejs +43 -0
- package/dist/templates/claude/commands/drain.md.ejs +81 -0
- package/dist/templates/claude/commands/gold-audit.md.ejs +43 -0
- package/dist/templates/claude/commands/impact.md.ejs +39 -0
- package/dist/templates/claude/commands/levelup.md.ejs +61 -0
- package/dist/templates/claude/commands/ship.md.ejs +902 -0
- package/dist/templates/claude/commands/tabletop.md.ejs +46 -0
- package/dist/templates/claude/commands/task.md.ejs +87 -0
- package/dist/templates/claude/commands/wt-close.md.ejs +58 -0
- package/dist/templates/claude/commands/wt-list.md.ejs +48 -0
- package/dist/templates/claude/commands/wt-open.md.ejs +61 -0
- package/dist/templates/claude/commands/wt-prune.md.ejs +38 -0
- package/dist/templates/claude/hooks/check-circular-deps.mjs.ejs +56 -0
- package/dist/templates/claude/hooks/check-no-orphan-todo.mjs.ejs +45 -0
- package/dist/templates/claude/hooks/check-no-pii.mjs.ejs +121 -0
- package/dist/templates/claude/hooks/check-no-placeholders.mjs.ejs +71 -0
- package/dist/templates/claude/hooks/check-no-skipped-tests.mjs +80 -0
- package/dist/templates/claude/hooks/check-no-unused-exports.mjs +62 -0
- package/dist/templates/claude/hooks/closer-mode-guard.mjs.ejs +102 -0
- package/dist/templates/claude/hooks/debug-state-on-failure.mjs.ejs +98 -0
- package/dist/templates/claude/hooks/enforce-gate-before-pr.mjs +142 -0
- package/dist/templates/claude/hooks/enforce-read-only.mjs +28 -0
- package/dist/templates/claude/hooks/exitplanmode-banner.mjs.ejs +43 -0
- package/dist/templates/claude/hooks/guard-done-evidence.mjs.ejs +85 -0
- package/dist/templates/claude/hooks/guard-task-completion.mjs.ejs +56 -0
- package/dist/templates/claude/hooks/hooks.mjs.ejs +127 -0
- package/dist/templates/claude/hooks/lib.mjs.ejs +523 -0
- package/dist/templates/claude/hooks/post-brainstorm-stop.mjs +48 -0
- package/dist/templates/claude/hooks/post-commit-check.mjs.ejs +65 -0
- package/dist/templates/claude/hooks/post-commit-checklists/go/backend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/go/docs.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/go/frontend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/java/backend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/java/docs.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/java/frontend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/python/backend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/python/docs.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/python/frontend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/rust/backend.ejs +5 -0
- package/dist/templates/claude/hooks/post-commit-checklists/rust/docs.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/rust/frontend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/ts/backend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/ts/docs.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/ts/frontend.ejs +5 -0
- package/dist/templates/claude/hooks/post-edit-dispatch.mjs.ejs +73 -0
- package/dist/templates/claude/hooks/post-subagent-release.mjs +80 -0
- package/dist/templates/claude/hooks/pre-compact.mjs.ejs +79 -0
- package/dist/templates/claude/hooks/pre-edit-plan-anchor.mjs.ejs +225 -0
- package/dist/templates/claude/hooks/pre-edit-ssot-guard.mjs +204 -0
- package/dist/templates/claude/hooks/pre-spawn-worktree-guard.mjs +129 -0
- package/dist/templates/claude/hooks/skill-forced-eval.mjs.ejs +281 -0
- package/dist/templates/claude/hooks/stop-dangerous.mjs +86 -0
- package/dist/templates/claude/hooks/stop-evidence-guard.mjs.ejs +245 -0
- package/dist/templates/claude/hooks/stop-finding-loss.mjs.ejs +168 -0
- package/dist/templates/claude/hooks/wiki-on-commit.mjs.ejs +38 -0
- package/dist/templates/claude/knowledge-map.json.ejs +26 -0
- package/dist/templates/claude/rules/05-agent-lifecycle.md +7 -0
- package/dist/templates/claude/rules/25-todo-folder-policy.md +5 -0
- package/dist/templates/claude/rules/40-context-economy.md +32 -0
- package/dist/templates/claude/rules/45-mcp-fallback.md +25 -0
- package/dist/templates/claude/rules/50-batch-execution.md +75 -0
- package/dist/templates/claude/rules/55-brainstorm-terminal-state.md +44 -0
- package/dist/templates/claude/rules/60-incidental-capture.md +43 -0
- package/dist/templates/claude/rules/75-impact-vault-reading.md +43 -0
- package/dist/templates/claude/rules/90-exec-protocol.md.ejs +40 -0
- package/dist/templates/claude/rules/95-closer-mode.md +45 -0
- package/dist/templates/claude/settings.json.ejs +165 -0
- package/dist/templates/claude/skills/architect-review/SKILL.md.ejs +82 -0
- package/dist/templates/claude/skills/brainstorming/SKILL.md.ejs +86 -0
- package/dist/templates/claude/skills/clean-code/SKILL.md.ejs +39 -0
- package/dist/templates/claude/skills/close-gold-gap/SKILL.md.ejs +58 -0
- package/dist/templates/claude/skills/codebase-audit/SKILL.md.ejs +88 -0
- package/dist/templates/claude/skills/configure/SKILL.md.ejs +72 -0
- package/dist/templates/claude/skills/epic-decompose/SKILL.md.ejs +86 -0
- package/dist/templates/claude/skills/gold-audit/SKILL.md.ejs +91 -0
- package/dist/templates/claude/skills/impact/SKILL.md.ejs +104 -0
- package/dist/templates/claude/skills/levelup/SKILL.md.ejs +146 -0
- package/dist/templates/claude/skills/tabletop/SKILL.md.ejs +118 -0
- package/dist/templates/claude/skills/tdd/SKILL.md.ejs +157 -0
- package/dist/templates/claude/skills/understand-code/SKILL.md.ejs +93 -0
- package/dist/templates/claude/skills/verification/SKILL.md.ejs +68 -0
- package/dist/templates/claude/skills/wave-drain/SKILL.md.ejs +491 -0
- package/dist/templates/codex/CODEX.md.ejs +140 -0
- package/dist/templates/codex/codex-adapter.mjs +132 -0
- package/dist/templates/codex/config.toml.ejs +189 -0
- package/dist/templates/compliance/compliance-mapping.md.ejs +124 -0
- package/dist/templates/compliance/compliance-menu.md.ejs +89 -0
- package/dist/templates/contract-testing/CONTRACTS_POLICY.md.ejs +30 -0
- package/dist/templates/contract-testing/api-snapshots/config-keys-baseline.json.ejs +1 -0
- package/dist/templates/contract-testing/api-snapshots/config-response-baseline.json.ejs +1 -0
- package/dist/templates/contract-testing/api-snapshots/enum-values-baseline.json.ejs +1 -0
- package/dist/templates/contract-testing/api-snapshots/error-shape-baseline.json.ejs +6 -0
- package/dist/templates/contract-testing/api-snapshots/openapi-baseline.json.ejs +12 -0
- package/dist/templates/contract-testing/api-snapshots/openapi-content-types-baseline.json.ejs +3 -0
- package/dist/templates/contract-testing/api-snapshots/openapi-paths-baseline.json.ejs +1 -0
- package/dist/templates/contract-testing/api-snapshots/openapi-required-fields-baseline.json.ejs +1 -0
- package/dist/templates/contract-testing/api-snapshots/openapi-response-status-baseline.json.ejs +3 -0
- package/dist/templates/contract-testing/api-snapshots/test-snapshot.json.ejs +4 -0
- package/dist/templates/contract-testing/env/.env.pact.ejs +4 -0
- package/dist/templates/contract-testing/graphql/GraphqlSchemaTest.java.ejs +29 -0
- package/dist/templates/contract-testing/graphql/graphql-inspector.test.ts.ejs +20 -0
- package/dist/templates/contract-testing/graphql/graphql_schema_test.go.ejs +27 -0
- package/dist/templates/contract-testing/graphql/graphql_schema_test.rs.ejs +27 -0
- package/dist/templates/contract-testing/graphql/test_graphql_schema.py.ejs +21 -0
- package/dist/templates/contract-testing/grpc/GrpcContractTest.java.ejs +23 -0
- package/dist/templates/contract-testing/grpc/buf-breaking.yml.ejs +6 -0
- package/dist/templates/contract-testing/grpc/buf.yaml.ejs +11 -0
- package/dist/templates/contract-testing/grpc/grpc-contract.test.ts.ejs +16 -0
- package/dist/templates/contract-testing/grpc/grpc_contract_test.go.ejs +22 -0
- package/dist/templates/contract-testing/grpc/grpc_contract_test.rs.ejs +22 -0
- package/dist/templates/contract-testing/grpc/test_grpc_contract.py.ejs +18 -0
- package/dist/templates/contract-testing/message-queue/SchemaRegistryCheckIT.java.ejs +44 -0
- package/dist/templates/contract-testing/message-queue/schema-registry-check.ts.ejs +42 -0
- package/dist/templates/contract-testing/message-queue/schema_registry_test.go.ejs +71 -0
- package/dist/templates/contract-testing/message-queue/schema_registry_test.rs.ejs +61 -0
- package/dist/templates/contract-testing/message-queue/test_schema_registry.py.ejs +31 -0
- package/dist/templates/contract-testing/pact-samples/assignment-response.json.ejs +7 -0
- package/dist/templates/contract-testing/pact-samples/availability-response.json.ejs +13 -0
- package/dist/templates/contract-testing/pact-samples/availability-rule-response.json.ejs +8 -0
- package/dist/templates/contract-testing/pact-samples/capacity-response.json.ejs +11 -0
- package/dist/templates/contract-testing/pact-samples/fully-booked-response.json.ejs +6 -0
- package/dist/templates/contract-testing/pact-samples/schedule-override-response.json.ejs +7 -0
- package/dist/templates/contract-testing/rest-owned/PactVerificationIT.java.ejs +31 -0
- package/dist/templates/contract-testing/rest-owned/pact-consumer.test.ts.ejs +36 -0
- package/dist/templates/contract-testing/rest-owned/pact-deps.gradle.ejs +15 -0
- package/dist/templates/contract-testing/rest-owned/pact_consumer_test.go.ejs +48 -0
- package/dist/templates/contract-testing/rest-owned/pact_consumer_test.rs.ejs +23 -0
- package/dist/templates/contract-testing/rest-owned/test_pact_consumer.py.ejs +35 -0
- package/dist/templates/contract-testing/rest-public/OpenApiDiffIT.java.ejs +44 -0
- package/dist/templates/contract-testing/rest-public/export-openapi-java.gradle.ejs +13 -0
- package/dist/templates/contract-testing/rest-public/export-openapi.mjs.ejs +21 -0
- package/dist/templates/contract-testing/rest-public/export_openapi.go.ejs +30 -0
- package/dist/templates/contract-testing/rest-public/export_openapi.py.ejs +27 -0
- package/dist/templates/contract-testing/rest-public/export_openapi.rs.ejs +32 -0
- package/dist/templates/contract-testing/rest-public/openapi-diff.ts.ejs +42 -0
- package/dist/templates/contract-testing/rest-public/openapi_diff_test.go.ejs +35 -0
- package/dist/templates/contract-testing/rest-public/openapi_diff_test.rs.ejs +36 -0
- package/dist/templates/contract-testing/rest-public/test_openapi_diff.py.ejs +32 -0
- package/dist/templates/coverage/.coveragerc.ejs +26 -0
- package/dist/templates/coverage/.tarpaulin.toml.ejs +30 -0
- package/dist/templates/coverage/Cargo.toml.profile.release.ejs +24 -0
- package/dist/templates/coverage/jacoco-maven-setup.md.ejs +72 -0
- package/dist/templates/coverage/jacoco.gradle.ejs +49 -0
- package/dist/templates/coverage/kover.gradle.ejs +35 -0
- package/dist/templates/coverage/vitest.browser.config.ts.ejs +47 -0
- package/dist/templates/coverage/vitest.config.ts.ejs +47 -0
- package/dist/templates/docs/CODING_STANDARDS.md.ejs +81 -0
- package/dist/templates/docs/FEATURE_MATRIX.md.ejs +84 -0
- package/dist/templates/docs/GAP.md.ejs +58 -0
- package/dist/templates/docs/ISO27001_ANNEX_A.md.ejs +144 -0
- package/dist/templates/docs/MASTER_TEST_PLAN.md.ejs +52 -0
- package/dist/templates/docs/POST_MERGE_REVIEW_TEMPLATE.md.ejs +63 -0
- package/dist/templates/docs/SECURE_CODING_CHECKLIST.md.ejs +61 -0
- package/dist/templates/docs/adr/ADR-000_template.md.ejs +28 -0
- package/dist/templates/docs/bugs/bug-analysis.md.ejs +47 -0
- package/dist/templates/docs/bugs/bug-report.md.ejs +51 -0
- package/dist/templates/docs/bugs/bug-verification.md.ejs +42 -0
- package/dist/templates/docs/runbooks/deployment.md.ejs +119 -0
- package/dist/templates/docs/runbooks/prod-checklist.md.ejs +90 -0
- package/dist/templates/docs/runbooks/rollback.md.ejs +124 -0
- package/dist/templates/docs/runbooks/troubleshooting.md.ejs +130 -0
- package/dist/templates/docs/skeletons/arc42-canvas.md.ejs +46 -0
- package/dist/templates/docs/skeletons/arc42-full.md.ejs +59 -0
- package/dist/templates/docs/skeletons/decision-registry.md.ejs +38 -0
- package/dist/templates/docs/skeletons/er-model.md.ejs +19 -0
- package/dist/templates/docs/skeletons/glossary.md.ejs +8 -0
- package/dist/templates/docs/skeletons/governance.md.ejs +13 -0
- package/dist/templates/docs/skeletons/slo.md.ejs +21 -0
- package/dist/templates/docs/skeletons/technical-debt.md.ejs +8 -0
- package/dist/templates/docs/skeletons/test-strategy.md.ejs +14 -0
- package/dist/templates/docs/skeletons/threat-model-4q.md.ejs +20 -0
- package/dist/templates/docs/skeletons/threat-model-stride.md.ejs +22 -0
- package/dist/templates/docs/specs/atomic-task-list.md.ejs +57 -0
- package/dist/templates/docs/steering/product.md.ejs +44 -0
- package/dist/templates/docs/steering/structure.md.ejs +49 -0
- package/dist/templates/docs/steering/tech.md.ejs +53 -0
- package/dist/templates/documentation/cli-catalog.md.ejs +21 -0
- package/dist/templates/e2e/E2E_CONSTITUTION.md.ejs +77 -0
- package/dist/templates/e2e/playwright-python/conftest.py.ejs +22 -0
- package/dist/templates/e2e/playwright-python/run_axe.py.ejs +99 -0
- package/dist/templates/e2e/playwright-python/test_a11y.py.ejs +38 -0
- package/dist/templates/e2e/playwright-python/test_smoke.py.ejs +28 -0
- package/dist/templates/e2e/playwright-ts/a11y.spec.ts.ejs +20 -0
- package/dist/templates/e2e/playwright-ts/render-smoke.spec.ts.ejs +90 -0
- package/dist/templates/e2e/playwright-ts/run-axe.ts.ejs +74 -0
- package/dist/templates/e2e/smoke-journeys/journeys.spec.ts.ejs +53 -0
- package/dist/templates/evidence-files.json.ejs +13 -0
- package/dist/templates/frontend/FE_DESIGN_PRINCIPLES.md.ejs +115 -0
- package/dist/templates/frontend/FRONTEND_CONSTITUTION.md.ejs +170 -0
- package/dist/templates/frontend/design-tokens.json.ejs +107 -0
- package/dist/templates/frontend/vrt-setup.md.ejs +83 -0
- package/dist/templates/githooks/commit-msg.ejs +60 -0
- package/dist/templates/githooks/pre-commit.ejs +95 -0
- package/dist/templates/githooks/pre-push.ejs +251 -0
- package/dist/templates/githooks/setup-hooks.sh.ejs +8 -0
- package/dist/templates/github/PULL_REQUEST_TEMPLATE.md +37 -0
- package/dist/templates/github/PULL_REQUEST_TEMPLATE.md.ejs +52 -0
- package/dist/templates/github/actions/build-cache/action.yml.ejs +84 -0
- package/dist/templates/github/actions/nas-ssh/action.yml.ejs +53 -0
- package/dist/templates/github/actions/setup-java-maven/action.yml.ejs +53 -0
- package/dist/templates/github/actions/setup-node-pnpm/action.yml.ejs +39 -0
- package/dist/templates/github/actions/sign-and-attest/action.yml.ejs +53 -0
- package/dist/templates/github/dependabot.yml.ejs +66 -0
- package/dist/templates/github/extended-ci-paths.txt.ejs +38 -0
- package/dist/templates/github/issue-templates/bug-report.yml +72 -0
- package/dist/templates/github/issue-templates/compliance-item.yml.ejs +75 -0
- package/dist/templates/github/issue-templates/config.yml +2 -0
- package/dist/templates/github/issue-templates/epic.yml +66 -0
- package/dist/templates/github/issue-templates/feature-request.yml +24 -0
- package/dist/templates/github/issue-templates/task-brief.yml.ejs +125 -0
- package/dist/templates/github/labels.yml.ejs +161 -0
- package/dist/templates/github/scripts/sticky-failure-issue.sh.ejs +61 -0
- package/dist/templates/github/workflows/01-pr-fast.yml.ejs +1089 -0
- package/dist/templates/github/workflows/02-pr-extended.yml.ejs +512 -0
- package/dist/templates/github/workflows/03-human-approval.yml.ejs +76 -0
- package/dist/templates/github/workflows/04-deploy-test.yml.ejs +425 -0
- package/dist/templates/github/workflows/05-release.yml.ejs +813 -0
- package/dist/templates/github/workflows/06-nightly-lite.yml.ejs +125 -0
- package/dist/templates/github/workflows/06-nightly.yml.ejs +25 -0
- package/dist/templates/github/workflows/07-weekly-lite.yml.ejs +221 -0
- package/dist/templates/github/workflows/07-weekly.yml.ejs +23 -0
- package/dist/templates/github/workflows/08-monthly.yml.ejs +23 -0
- package/dist/templates/github/workflows/09-heartbeat.yml.ejs +268 -0
- package/dist/templates/github/workflows/10-deploy-prod.yml.ejs +144 -0
- package/dist/templates/github/workflows/11-k6-on-demand.yml.ejs +43 -0
- package/dist/templates/github/workflows/12-mutation-scheduled.yml.ejs +100 -0
- package/dist/templates/github/workflows/13-archunit-extended.yml.ejs +33 -0
- package/dist/templates/github/workflows/14-license-scan.yml.ejs +45 -0
- package/dist/templates/github/workflows/15-codeql.yml.ejs +117 -0
- package/dist/templates/github/workflows/16-frontend-quality.yml.ejs +154 -0
- package/dist/templates/github/workflows/17-ossf-scorecard.yml.ejs +46 -0
- package/dist/templates/github/workflows/18-frontend-lane.yml.ejs +50 -0
- package/dist/templates/github/workflows/_ai-draft-check.yml.ejs +44 -0
- package/dist/templates/github/workflows/_contract-postman.yml.ejs +93 -0
- package/dist/templates/github/workflows/_cosign-copy/aws-ecs.ejs +90 -0
- package/dist/templates/github/workflows/_cosign-copy/azure-container-app.ejs +97 -0
- package/dist/templates/github/workflows/_cosign-copy/gcp-cloud-run.ejs +102 -0
- package/dist/templates/github/workflows/_cosign-copy/ghcr.ejs +76 -0
- package/dist/templates/github/workflows/_cosign-copy/nas-compose.ejs +69 -0
- package/dist/templates/github/workflows/_deploy/aws-ecs.ejs +78 -0
- package/dist/templates/github/workflows/_deploy/azure-container-app.ejs +59 -0
- package/dist/templates/github/workflows/_deploy/gcp-cloud-run.ejs +71 -0
- package/dist/templates/github/workflows/_deploy/ghcr.ejs +19 -0
- package/dist/templates/github/workflows/_deploy/nas-compose.ejs +45 -0
- package/dist/templates/github/workflows/_k6-runner.yml.ejs +107 -0
- package/dist/templates/github/workflows/_label-on-approve.yml.ejs +44 -0
- package/dist/templates/github/workflows/_label-sync.yml.ejs +58 -0
- package/dist/templates/github/workflows/_monthly.yml.ejs +343 -0
- package/dist/templates/github/workflows/_nightly.yml.ejs +826 -0
- package/dist/templates/github/workflows/_notify.yml.ejs +96 -0
- package/dist/templates/github/workflows/_partials/macos-keychain-bypass.ejs +6 -0
- package/dist/templates/github/workflows/_partials/macos-keychain-restore.ejs +6 -0
- package/dist/templates/github/workflows/_partials/scheduled-heavy-jobs.ejs +117 -0
- package/dist/templates/github/workflows/_partials/sigstore-preflight.ejs +8 -0
- package/dist/templates/github/workflows/_partials/trivy-fs-java.ejs +27 -0
- package/dist/templates/github/workflows/_post-merge-notify.yml.ejs +228 -0
- package/dist/templates/github/workflows/_pr-staleness.yml.ejs +85 -0
- package/dist/templates/github/workflows/_shared-security.yml.ejs +132 -0
- package/dist/templates/github/workflows/_sigstore-retry-sign.yml.ejs +102 -0
- package/dist/templates/github/workflows/_weekly.yml.ejs +313 -0
- package/dist/templates/github/workflows/drift-shadow.yml.ejs +51 -0
- package/dist/templates/github/workflows/five-lane/ci.yml.ejs +54 -0
- package/dist/templates/github/workflows/five-lane/nightly.yml.ejs +70 -0
- package/dist/templates/github/workflows/five-lane/release.yml.ejs +53 -0
- package/dist/templates/github/workflows/five-lane/weekly.yml.ejs +72 -0
- package/dist/templates/github/workflows/issue-state.yml.ejs +93 -0
- package/dist/templates/global-invariants/GLOBAL_INVARIANTS.md.ejs +34 -0
- package/dist/templates/governance/RACI.md.ejs +21 -0
- package/dist/templates/governance/ci-mental-model.md.ejs +268 -0
- package/dist/templates/governance/contract-integrity-policy.md.ejs +116 -0
- package/dist/templates/governance/enterprise-compliance-baseline.md.ejs +111 -0
- package/dist/templates/governance/evidence-retention.md.ejs +43 -0
- package/dist/templates/governance/gdpr-erasure-runbook.md.ejs +165 -0
- package/dist/templates/governance/qa-audit-phases.md.ejs +129 -0
- package/dist/templates/governance/risk-assessment-template.md.ejs +105 -0
- package/dist/templates/governance/risk-register.md.ejs +76 -0
- package/dist/templates/governance/solo-dev-exception.md.ejs +96 -0
- package/dist/templates/governance/validation-evidence.md.ejs +131 -0
- package/dist/templates/infra/azure/containerapp.tpl.yaml.ejs +74 -0
- package/dist/templates/infra/nas-compose/deploy.sh.ejs +114 -0
- package/dist/templates/integration-testing/AbstractIntegrationTest.java.ejs +36 -0
- package/dist/templates/integration-testing/NoH2ArchTest.java.ejs +24 -0
- package/dist/templates/integration-testing/conftest.py.ejs +20 -0
- package/dist/templates/integration-testing/db_fixture.rs.ejs +48 -0
- package/dist/templates/integration-testing/eslint-no-fake-db.json.ejs +19 -0
- package/dist/templates/integration-testing/eslint.config.no-fake-db.mjs.ejs +46 -0
- package/dist/templates/integration-testing/main_test.go.ejs +53 -0
- package/dist/templates/integration-testing/main_test_sqlite.go.ejs +37 -0
- package/dist/templates/integration-testing/test-setup.ts.ejs +26 -0
- package/dist/templates/integration-testing/testcontainers-deps.gradle.ejs +9 -0
- package/dist/templates/java/DeprecationInterceptor.java.ejs +50 -0
- package/dist/templates/java/archunit-hexagonal.ejs +69 -0
- package/dist/templates/java/modulith/ApplicationModulesTest.java.ejs +21 -0
- package/dist/templates/java/modulith/modulith-deps.gradle.ejs +13 -0
- package/dist/templates/java/modulith/modulith-maven-setup.md.ejs +21 -0
- package/dist/templates/java/pharma/AuditEvent.java.ejs +138 -0
- package/dist/templates/java/pharma/AuditMapper.java.ejs +60 -0
- package/dist/templates/java/pharma/archunit-pharma.ejs +128 -0
- package/dist/templates/kit/GLOBAL_KIT.md.ejs +11 -0
- package/dist/templates/kit/dim.md.ejs +33 -0
- package/dist/templates/llms-txt.config.json.ejs +22 -0
- package/dist/templates/local-wrapper/.env.example.ejs +15 -0
- package/dist/templates/local-wrapper/Makefile.ejs +44 -0
- package/dist/templates/local-wrapper/run.sh.ejs +5 -0
- package/dist/templates/middleware/410-gone-handler.ts.ejs +41 -0
- package/dist/templates/middleware/correlation-id.ts.ejs +32 -0
- package/dist/templates/middleware/deprecation.ts.ejs +45 -0
- package/dist/templates/middleware/error-handler.ts.ejs +73 -0
- package/dist/templates/middleware/payload-size-limit.ts.ejs +51 -0
- package/dist/templates/mutation/README.md.ejs +67 -0
- package/dist/templates/mutation/cargo-mutants.toml.ejs +9 -0
- package/dist/templates/mutation/mutmut-config.toml.ejs +7 -0
- package/dist/templates/mutation/parse-mutants.mjs.ejs +44 -0
- package/dist/templates/mutation/parse-mutmut.py.ejs +44 -0
- package/dist/templates/mutation/pitest-l2-setup.md.ejs +66 -0
- package/dist/templates/mutation/pitest-maven-setup.md.ejs +64 -0
- package/dist/templates/mutation/pitest.gradle.ejs +23 -0
- package/dist/templates/mutation/stryker.conf.json.ejs +17 -0
- package/dist/templates/observability/setup.md.ejs +495 -0
- package/dist/templates/operations/handbook.md.ejs +217 -0
- package/dist/templates/perf/bundle-budget.json.ejs +29 -0
- package/dist/templates/perf/k6/reports/csv-report.py.ejs +91 -0
- package/dist/templates/perf/k6/reports/html-report.py.ejs +114 -0
- package/dist/templates/perf/k6/reports/json-report.py.ejs +74 -0
- package/dist/templates/perf/k6/scenarios/breakpoint.js.ejs +26 -0
- package/dist/templates/perf/k6/scenarios/burst.js.ejs +43 -0
- package/dist/templates/perf/k6/scenarios/endurance.js.ejs +36 -0
- package/dist/templates/perf/k6/scenarios/load.js.ejs +26 -0
- package/dist/templates/perf/k6/scenarios/ramp-down.js.ejs +29 -0
- package/dist/templates/perf/k6/scenarios/ramp-up.js.ejs +32 -0
- package/dist/templates/perf/k6/scenarios/smoke.js.ejs +23 -0
- package/dist/templates/perf/k6/scenarios/soak.js.ejs +26 -0
- package/dist/templates/perf/k6/scenarios/spike.js.ejs +29 -0
- package/dist/templates/perf/k6/scenarios/steady-state.js.ejs +31 -0
- package/dist/templates/perf/k6/scenarios/stress.js.ejs +29 -0
- package/dist/templates/perf/k6/scenarios/volume.js.ejs +36 -0
- package/dist/templates/perf/k6/seed/test-data.sql.ejs +59 -0
- package/dist/templates/perf/lighthouserc.json.ejs +41 -0
- package/dist/templates/quality/iso9001/CAPA_LOG.md.ejs +36 -0
- package/dist/templates/quality/iso9001/DOCUMENT_CONTROL.md.ejs +36 -0
- package/dist/templates/quality/iso9001/REQUIREMENTS_TRACEABILITY.md.ejs +61 -0
- package/dist/templates/regulated/overlay.json.ejs +26 -0
- package/dist/templates/regulated/regulated-overlay.md.ejs +40 -0
- package/dist/templates/resilience/RESILIENCE.md.ejs +310 -0
- package/dist/templates/root/.gitignore.ejs +85 -0
- package/dist/templates/root/CODEOWNERS.ejs +18 -0
- package/dist/templates/root/CONTRIBUTING.md.ejs +62 -0
- package/dist/templates/root/SECURITY.md.ejs +19 -0
- package/dist/templates/root/TEST_TAXONOMY.md.ejs +100 -0
- package/dist/templates/root/commitlint.config.js.ejs +4 -0
- package/dist/templates/root/docs/METHOD/CANONICAL_PATHS.md.ejs +24 -0
- package/dist/templates/root/docs/METHOD/ENGINEERING_DEFAULTS.md.ejs +136 -0
- package/dist/templates/root/docs/METHOD/KNOWLEDGE_MAP.md.ejs +90 -0
- package/dist/templates/root/docs/METHOD/PATTERNS_CATALOG.md.ejs +51 -0
- package/dist/templates/root/docs/METHOD/REUSE_REGISTRY.md.ejs +26 -0
- package/dist/templates/root/docs/METHOD/REUSE_REGISTRY_SPEC.md.ejs +80 -0
- package/dist/templates/root/docs/METHOD/SSOT_CORE_SET.md.ejs +62 -0
- package/dist/templates/root/docs/METHOD/TRACK_ROUTER.md.ejs +39 -0
- package/dist/templates/root/docs/SYSTEM/CANON.md.ejs +359 -0
- package/dist/templates/root/docs/SYSTEM/FAIL_CLOSED.md.ejs +171 -0
- package/dist/templates/root/editorconfig.ejs +39 -0
- package/dist/templates/root/tsconfig.json.ejs +21 -0
- package/dist/templates/rust/clippy.toml.ejs +14 -0
- package/dist/templates/schemas/milestone.schema.json.ejs +164 -0
- package/dist/templates/schemas/rtm-verdict.schema.json.ejs +52 -0
- package/dist/templates/schemas/source-record.schema.json.ejs +91 -0
- package/dist/templates/schemas/tabletop-evidence.schema.json.ejs +53 -0
- package/dist/templates/schemas/use-case.schema.json.ejs +59 -0
- package/dist/templates/scripts/apply-branch-protection.mjs.ejs +70 -0
- package/dist/templates/scripts/audit-toolchain.mjs.ejs +112 -0
- package/dist/templates/scripts/bg-run.sh.ejs +59 -0
- package/dist/templates/scripts/capacity-probe.mjs.ejs +147 -0
- package/dist/templates/scripts/capture-debt-baseline.mjs.ejs +99 -0
- package/dist/templates/scripts/check-acceptance.mjs.ejs +298 -0
- package/dist/templates/scripts/check-action-pins.mjs.ejs +215 -0
- package/dist/templates/scripts/check-agent-return.mjs.ejs +177 -0
- package/dist/templates/scripts/check-all.mjs.ejs +1746 -0
- package/dist/templates/scripts/check-anti-fake-green.mjs.ejs +80 -0
- package/dist/templates/scripts/check-anti-proforma.mjs.ejs +547 -0
- package/dist/templates/scripts/check-api-e2e.mjs.ejs +106 -0
- package/dist/templates/scripts/check-arc42-slots.mjs.ejs +25 -0
- package/dist/templates/scripts/check-assertion-delta.mjs.ejs +287 -0
- package/dist/templates/scripts/check-audit-dry-pass.mjs.ejs +176 -0
- package/dist/templates/scripts/check-bundle-size.mjs.ejs +151 -0
- package/dist/templates/scripts/check-canonical-paths.mjs.ejs +39 -0
- package/dist/templates/scripts/check-ci-tiers.mjs.ejs +111 -0
- package/dist/templates/scripts/check-claude-md-lint.mjs.ejs +142 -0
- package/dist/templates/scripts/check-collab-mode-wired.mjs.ejs +60 -0
- package/dist/templates/scripts/check-commit-footer-rationale.mjs.ejs +170 -0
- package/dist/templates/scripts/check-constraint-scan.mjs.ejs +681 -0
- package/dist/templates/scripts/check-consumer-audit.mjs.ejs +414 -0
- package/dist/templates/scripts/check-continue-on-error.mjs.ejs +227 -0
- package/dist/templates/scripts/check-cross-model-review.mjs.ejs +458 -0
- package/dist/templates/scripts/check-decision-registry.mjs.ejs +101 -0
- package/dist/templates/scripts/check-doc-freshness.mjs.ejs +17 -0
- package/dist/templates/scripts/check-doc-links.mjs.ejs +98 -0
- package/dist/templates/scripts/check-doc-set.mjs.ejs +18 -0
- package/dist/templates/scripts/check-docs.mjs.ejs +71 -0
- package/dist/templates/scripts/check-domain-api-surface.mjs.ejs +80 -0
- package/dist/templates/scripts/check-drift.mjs.ejs +167 -0
- package/dist/templates/scripts/check-duplication.mjs.ejs +45 -0
- package/dist/templates/scripts/check-e2e-escalation.mjs.ejs +199 -0
- package/dist/templates/scripts/check-e2e-quarantine.mjs.ejs +68 -0
- package/dist/templates/scripts/check-emission-parity.mjs.ejs +91 -0
- package/dist/templates/scripts/check-exit-code-contract.mjs.ejs +138 -0
- package/dist/templates/scripts/check-fail-closed-audit.mjs.ejs +389 -0
- package/dist/templates/scripts/check-fe-boundaries.mjs.ejs +184 -0
- package/dist/templates/scripts/check-feature-matrix.mjs.ejs +835 -0
- package/dist/templates/scripts/check-frontend-lane.mjs.ejs +73 -0
- package/dist/templates/scripts/check-grace-window.mjs.ejs +102 -0
- package/dist/templates/scripts/check-handoff-doc.mjs.ejs +192 -0
- package/dist/templates/scripts/check-hook-routing.mjs.ejs +224 -0
- package/dist/templates/scripts/check-image-pins.mjs.ejs +90 -0
- package/dist/templates/scripts/check-inline-suppressions.mjs.ejs +153 -0
- package/dist/templates/scripts/check-iso9001.mjs.ejs +146 -0
- package/dist/templates/scripts/check-knowledge-map.mjs.ejs +49 -0
- package/dist/templates/scripts/check-local-ci-parity.mjs.ejs +382 -0
- package/dist/templates/scripts/check-m16-handoff.mjs.ejs +112 -0
- package/dist/templates/scripts/check-merge-method.mjs.ejs +92 -0
- package/dist/templates/scripts/check-milestones.mjs.ejs +678 -0
- package/dist/templates/scripts/check-min-test-execution.mjs.ejs +225 -0
- package/dist/templates/scripts/check-muted-test.mjs.ejs +270 -0
- package/dist/templates/scripts/check-no-stub-redirects.mjs.ejs +154 -0
- package/dist/templates/scripts/check-no-tracked-artifacts.mjs.ejs +134 -0
- package/dist/templates/scripts/check-oracle-discrimination.mjs.ejs +244 -0
- package/dist/templates/scripts/check-pii-scan.mjs.ejs +67 -0
- package/dist/templates/scripts/check-pipe-tee-hazard.mjs.ejs +85 -0
- package/dist/templates/scripts/check-pr-size-gate.mjs.ejs +85 -0
- package/dist/templates/scripts/check-refutation-verdicts.mjs.ejs +256 -0
- package/dist/templates/scripts/check-regulated-overlay.mjs.ejs +158 -0
- package/dist/templates/scripts/check-render-smoke.mjs.ejs +100 -0
- package/dist/templates/scripts/check-reuse-registry.mjs.ejs +44 -0
- package/dist/templates/scripts/check-review-completion.mjs.ejs +708 -0
- package/dist/templates/scripts/check-safety-adopt-ratchet.mjs.ejs +142 -0
- package/dist/templates/scripts/check-secret-presence.mjs.ejs +191 -0
- package/dist/templates/scripts/check-secret-scan.mjs.ejs +82 -0
- package/dist/templates/scripts/check-skip-critical-e2e.mjs.ejs +142 -0
- package/dist/templates/scripts/check-smoke-journeys.mjs.ejs +272 -0
- package/dist/templates/scripts/check-solo-reactivation.mjs.ejs +102 -0
- package/dist/templates/scripts/check-sources.mjs.ejs +245 -0
- package/dist/templates/scripts/check-ssot-core.mjs.ejs +45 -0
- package/dist/templates/scripts/check-stack-conformity.mjs.ejs +160 -0
- package/dist/templates/scripts/check-stride-traceability.mjs.ejs +101 -0
- package/dist/templates/scripts/check-suppression-expiry.mjs.ejs +79 -0
- package/dist/templates/scripts/check-suppression-rationale.mjs.ejs +76 -0
- package/dist/templates/scripts/check-suppressions.mjs.ejs +150 -0
- package/dist/templates/scripts/check-tabletop-evidence.mjs.ejs +499 -0
- package/dist/templates/scripts/check-tdd-evidence.mjs.ejs +381 -0
- package/dist/templates/scripts/check-test-naming.mjs.ejs +151 -0
- package/dist/templates/scripts/check-test-pyramid.mjs.ejs +170 -0
- package/dist/templates/scripts/check-test-scope-tier.mjs.ejs +208 -0
- package/dist/templates/scripts/check-tier-coverage.mjs.ejs +71 -0
- package/dist/templates/scripts/check-todo-max-age.mjs.ejs +205 -0
- package/dist/templates/scripts/check-touched-vs-manifest.mjs.ejs +179 -0
- package/dist/templates/scripts/check-unwired-guards.mjs.ejs +367 -0
- package/dist/templates/scripts/check-use-cases.mjs.ejs +345 -0
- package/dist/templates/scripts/check-validator-helptext.mjs.ejs +80 -0
- package/dist/templates/scripts/check-wiki-lint.mjs.ejs +162 -0
- package/dist/templates/scripts/check-workflow-docs-sync.mjs.ejs +91 -0
- package/dist/templates/scripts/check-workflow-job-naming.mjs.ejs +102 -0
- package/dist/templates/scripts/check-workflow-perms.mjs.ejs +54 -0
- package/dist/templates/scripts/check-workflow-runners.mjs.ejs +87 -0
- package/dist/templates/scripts/check-workflow-sha-pinning.mjs.ejs +89 -0
- package/dist/templates/scripts/check-workflow-test-integrity.mjs.ejs +235 -0
- package/dist/templates/scripts/checks/check-rust-no-unsafe.mjs.ejs +58 -0
- package/dist/templates/scripts/checks/check-rust-no-unwrap.mjs.ejs +61 -0
- package/dist/templates/scripts/ci-classify-changes.mjs.ejs +119 -0
- package/dist/templates/scripts/conformance.mjs.ejs +37 -0
- package/dist/templates/scripts/constraint-map.json.ejs +25 -0
- package/dist/templates/scripts/debt-lib.mjs.ejs +1015 -0
- package/dist/templates/scripts/debt-report.mjs.ejs +199 -0
- package/dist/templates/scripts/domain-api-surface.json.ejs +23 -0
- package/dist/templates/scripts/done-evidence.mjs.ejs +359 -0
- package/dist/templates/scripts/evidence-collect.mjs.ejs +264 -0
- package/dist/templates/scripts/evidence-prune.mjs.ejs +96 -0
- package/dist/templates/scripts/evidence-rotate.mjs.ejs +53 -0
- package/dist/templates/scripts/evidence-writer.sh.ejs +220 -0
- package/dist/templates/scripts/gate-registry.yml.ejs +286 -0
- package/dist/templates/scripts/gen-doc-index.mjs.ejs +19 -0
- package/dist/templates/scripts/gen-gap.mjs.ejs +153 -0
- package/dist/templates/scripts/gen-llms-txt.mjs.ejs +18 -0
- package/dist/templates/scripts/gen-wiki.mjs.ejs +196 -0
- package/dist/templates/scripts/gh-cleanup-expired-artifacts.mjs.ejs +134 -0
- package/dist/templates/scripts/gold-audit.mjs.ejs +19 -0
- package/dist/templates/scripts/i18n-literal-scanner.mjs.ejs +175 -0
- package/dist/templates/scripts/ingest-zap-report.mjs.ejs +107 -0
- package/dist/templates/scripts/inject-pact-samples.sh.ejs +96 -0
- package/dist/templates/scripts/issue-readiness.mjs.ejs +84 -0
- package/dist/templates/scripts/knowledge-map-update.mjs.ejs +32 -0
- package/dist/templates/scripts/lib/acceptance-criteria.mjs.ejs +213 -0
- package/dist/templates/scripts/lib/agent-return-validate.mjs.ejs +481 -0
- package/dist/templates/scripts/lib/coverage-gate.mjs.ejs +54 -0
- package/dist/templates/scripts/lib/e2e-reliability.mjs.ejs +207 -0
- package/dist/templates/scripts/lib/ephemeral-server.mjs.ejs +148 -0
- package/dist/templates/scripts/lib/evidence-binding.mjs.ejs +140 -0
- package/dist/templates/scripts/lib/exact-sha-policy.mjs.ejs +233 -0
- package/dist/templates/scripts/lib/gate-args.mjs.ejs +20 -0
- package/dist/templates/scripts/lib/gate-evidence.mjs.ejs +622 -0
- package/dist/templates/scripts/lib/gate-mutex.mjs.ejs +340 -0
- package/dist/templates/scripts/lib/glob-walk.mjs.ejs +148 -0
- package/dist/templates/scripts/lib/run-helpers.mjs.ejs +416 -0
- package/dist/templates/scripts/lib/seed-common.sh.ejs +105 -0
- package/dist/templates/scripts/lib/waiter-count.mjs.ejs +62 -0
- package/dist/templates/scripts/muted-tests-baseline.json.ejs +4 -0
- package/dist/templates/scripts/optional-emissions.json.ejs +117 -0
- package/dist/templates/scripts/oracle-discrimination-baseline.json.ejs +6 -0
- package/dist/templates/scripts/pid-watch.sh.ejs +78 -0
- package/dist/templates/scripts/pii-scan.mjs.ejs +125 -0
- package/dist/templates/scripts/pr-merge-watch.mjs.ejs +445 -0
- package/dist/templates/scripts/record-agent-return.mjs.ejs +279 -0
- package/dist/templates/scripts/record-journey-evidence.mjs.ejs +69 -0
- package/dist/templates/scripts/rework-log.mjs.ejs +172 -0
- package/dist/templates/scripts/run-postman-tests.sh.ejs +112 -0
- package/dist/templates/scripts/schemas/agent-return-external.schema.json +74 -0
- package/dist/templates/scripts/schemas/agent-return.schema.json.ejs +181 -0
- package/dist/templates/scripts/schemas/cross-model-dispatch.schema.json +95 -0
- package/dist/templates/scripts/seed-test-data.sh.ejs +99 -0
- package/dist/templates/scripts/self-validation.mjs.ejs +126 -0
- package/dist/templates/scripts/setup-repo.sh.ejs +78 -0
- package/dist/templates/scripts/spotbugs-baseline.json.ejs +4 -0
- package/dist/templates/scripts/test-gate-layering.mjs.ejs +92 -0
- package/dist/templates/scripts/validate-api-snapshots.mjs.ejs +86 -0
- package/dist/templates/scripts/validate-k6-scenarios.mjs.ejs +96 -0
- package/dist/templates/scripts/validate-openapi-field-types.mjs.ejs +124 -0
- package/dist/templates/scripts/validate-postman-collection.mjs.ejs +159 -0
- package/dist/templates/scripts/verify-fe-coverage.mjs.ejs +138 -0
- package/dist/templates/scripts/verify-i18n-parity.mjs.ejs +114 -0
- package/dist/templates/scripts/verify-module-coverage.mjs.ejs +207 -0
- package/dist/templates/scripts/verify-mutation-baseline.mjs.ejs +209 -0
- package/dist/templates/scripts/verify-spotbugs.mjs.ejs +216 -0
- package/dist/templates/scripts/verify-tokens.mjs.ejs +177 -0
- package/dist/templates/security/RISK_ASSESSMENT.md.ejs +35 -0
- package/dist/templates/security/STRIDE.md.ejs +24 -0
- package/dist/templates/security/gitleaks.toml.ejs +21 -0
- package/dist/templates/security/zap/baseline-auth.context.ejs +58 -0
- package/dist/templates/security/zap/rules.tsv.ejs +100 -0
- package/dist/templates/ship/TICK_PROMPT.md.ejs +55 -0
- package/dist/templates/ship/supervisor.sh.ejs +46 -0
- package/dist/templates/smoke-journeys/manifest.json.ejs +1 -0
- package/dist/templates/sonar-project.properties.ejs +25 -0
- package/dist/templates/standards/doc-profile.ejs +27 -0
- package/dist/templates/standards/gold-doc-set.yml.ejs +528 -0
- package/dist/templates/standards/gold-registry.java.yml.ejs +266 -0
- package/dist/templates/standards/gold-registry.typescript.yml.ejs +104 -0
- package/dist/templates/standards/gold-registry.yml.ejs +119 -0
- package/dist/templates/standards/thresholds.yml.ejs +86 -0
- package/dist/templates/static-analysis/.dependency-cruiser.cjs.ejs +78 -0
- package/dist/templates/static-analysis/.golangci.yml.ejs +70 -0
- package/dist/templates/static-analysis/.shellcheckrc.ejs +13 -0
- package/dist/templates/static-analysis/.yamllint.yml.ejs +41 -0
- package/dist/templates/static-analysis/checkstyle.xml.ejs +83 -0
- package/dist/templates/static-analysis/detekt.yml.ejs +26 -0
- package/dist/templates/static-analysis/eslint.config.mjs.ejs +54 -0
- package/dist/templates/static-analysis/eslint.config.static.mjs.ejs +38 -0
- package/dist/templates/static-analysis/eslintrc-static.json.ejs +30 -0
- package/dist/templates/static-analysis/jscpd.json.ejs +25 -0
- package/dist/templates/static-analysis/knip.json.ejs +26 -0
- package/dist/templates/static-analysis/pmd-ruleset.xml.ejs +92 -0
- package/dist/templates/static-analysis/prettierignore.ejs +59 -0
- package/dist/templates/static-analysis/prettierrc.json.ejs +10 -0
- package/dist/templates/static-analysis/requirements-dev.txt.ejs +11 -0
- package/dist/templates/static-analysis/ruff.toml.ejs +40 -0
- package/dist/templates/static-analysis/rustfmt.toml.ejs +3 -0
- package/dist/templates/static-analysis/spotbugs-exclude.xml.ejs +120 -0
- package/dist/templates/static-analysis/spotbugs.gradle.ejs +18 -0
- package/dist/templates/static-analysis/spotless.gradle.ejs +38 -0
- package/dist/templates/static-analysis/stylelintrc.json.ejs +16 -0
- package/dist/templates/suppressions/archunit-baseline.json.ejs +1 -0
- package/dist/templates/suppressions/consumer-audit-allowlist.json.ejs +1 -0
- package/dist/templates/suppressions/gitleaksignore.ejs +20 -0
- package/dist/templates/suppressions/pii-allowlist.json.ejs +1 -0
- package/dist/templates/suppressions/suppressions-schema.json.ejs +39 -0
- package/dist/templates/suppressions/trivyignore.ejs +17 -0
- package/dist/templates/testing/test-taxonomy.md.ejs +120 -0
- package/dist/test/example.behavioral.test.js +2 -0
- package/dist/types/memory.d.ts +19 -0
- package/dist/types/memory.js +1 -0
- package/dist/types/plan.d.ts +102 -0
- package/dist/types/plan.js +2 -0
- package/dist/types/plugin.d.ts +45 -0
- package/dist/types/plugin.js +1 -0
- package/dist/utils/canon-loader.js +2 -0
- package/dist/utils/channel.js +1 -0
- package/dist/utils/config.d.ts +79 -0
- package/dist/utils/config.js +5 -0
- package/dist/utils/confirm-downgrade.js +5 -0
- package/dist/utils/env.js +2 -0
- package/dist/utils/error-catalog.js +2 -0
- package/dist/utils/errors.js +2 -0
- package/dist/utils/evidence-log.js +3 -0
- package/dist/utils/file-lock.js +2 -0
- package/dist/utils/first-run.js +8 -0
- package/dist/utils/fs.js +3 -0
- package/dist/utils/github-issue-helper.js +4 -0
- package/dist/utils/gradle.js +21 -0
- package/dist/utils/json-output.js +2 -0
- package/dist/utils/logger-config.js +4 -0
- package/dist/utils/logger.js +3 -0
- package/dist/utils/maturity-check.js +2 -0
- package/dist/utils/pkg.js +3 -0
- package/dist/utils/platform.js +2 -0
- package/dist/utils/plugin-loader.js +2 -0
- package/dist/utils/plugin-worker.js +2 -0
- package/dist/utils/prettier-format.js +2 -0
- package/dist/utils/profiler.js +2 -0
- package/dist/utils/render.js +2 -0
- package/dist/utils/replay.js +3 -0
- package/dist/utils/run-cli.js +6 -0
- package/dist/utils/run-id.js +3 -0
- package/dist/utils/safe-read.js +2 -0
- package/dist/utils/task-id.js +2 -0
- package/dist/utils/today.js +2 -0
- package/dist/utils/tty.js +2 -0
- package/dist/utils/walk-dir.js +2 -0
- package/dist/verify/rules/drive-by-scope.js +1 -0
- package/dist/verify/rules/orphan-todos.js +1 -0
- package/dist/verify/rules/registry.js +1 -0
- package/dist/verify/rules/skip-patterns.js +1 -0
- package/dist/verify/rules/types.d.ts +15 -0
- package/dist/verify/rules/types.js +1 -0
- package/dist/verify/rules/ui-language.js +1 -0
- package/dist/verify/run.js +3 -0
- package/dist/wizard/archetype-defaults.js +1 -0
- package/dist/wizard/presets.js +1 -0
- package/dist/wizard/prompts.js +44 -0
- package/dist/wizard/types.d.ts +637 -0
- package/dist/wizard/types.js +1 -0
- package/dist/worktree/harvest.js +2 -0
- package/dist/worktree/links.js +2 -0
- package/dist/worktree/paths.js +2 -0
- package/dist/worktree/validate.js +3 -0
- package/package.json +124 -0
- package/scripts/check-arc42-slots.mjs +738 -0
- package/scripts/check-doc-freshness.mjs +236 -0
- package/scripts/check-doc-set.mjs +243 -0
- package/scripts/check-doc-style.mjs +228 -0
- package/scripts/gold-audit.mjs +319 -0
- package/scripts/lib/doc-gate-allowlist.mjs +123 -0
- package/scripts/lib/doc-set-resolve.mjs +274 -0
- package/scripts/lib/glob-walk.mjs +148 -0
- package/scripts/lib/gold-audit-lib.mjs +1260 -0
- package/scripts/lib/run-helpers.mjs +413 -0
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// gold-audit-lib.mjs — deterministic gold-audit evaluator (#1373).
|
|
3
|
+
//
|
|
4
|
+
// Reimplements the upstream reference gold-audit registry evaluator natively in node (zero
|
|
5
|
+
// Python dependency). The score is computed by code, never by an AI. Pure functions only — no
|
|
6
|
+
// process.exit, no argv, no console. The thin CLI (scripts/gold-audit.mjs) and the report
|
|
7
|
+
// consumer (scripts/gold-report.mjs) both call evaluate() so there is one code path.
|
|
8
|
+
//
|
|
9
|
+
// Verdicts (per check):
|
|
10
|
+
// Y — verified true by code, with evidence (file [+ line])
|
|
11
|
+
// P — partial (a count_matches check met some but not all of its target, or a
|
|
12
|
+
// version_consistency check found real-but-divergent substance)
|
|
13
|
+
// N — verified false by code, with evidence (the path/pattern that was absent)
|
|
14
|
+
// NA — not applicable (applies_if overlay disabled)
|
|
15
|
+
// NV — not verified by code (manual / attestation-required check)
|
|
16
|
+
//
|
|
17
|
+
// Determinism contract: identical repo + identical registry ⇒ identical evaluate() output.
|
|
18
|
+
// Checks are evaluated in stable id order; no wall-clock value enters the scored payload.
|
|
19
|
+
|
|
20
|
+
import { existsSync, readFileSync, statSync, lstatSync } from 'node:fs'
|
|
21
|
+
import { resolve, relative, isAbsolute } from 'node:path'
|
|
22
|
+
import { globToRegExp, validateGlob, walkRepo } from './glob-walk.mjs'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a registry-declared path inside root; reject traversal + null bytes. Parity contract:
|
|
26
|
+
* behaves byte-identically to src/conformance/shared.ts safeResolve for string inputs (reject only
|
|
27
|
+
* `\0` + post-resolve `..` escape — a literal filename containing '..' that stays in-root is
|
|
28
|
+
* allowed, matching the TS engine). The leading non-string guard is .mjs-only safety (the TS engine
|
|
29
|
+
* defaults a missing args.path to ''); for any real path both engines agree.
|
|
30
|
+
*/
|
|
31
|
+
function safeResolve(root, p) {
|
|
32
|
+
if (typeof p !== 'string' || p.includes('\0')) return null
|
|
33
|
+
const abs = resolve(root, p)
|
|
34
|
+
const rel = relative(root, abs)
|
|
35
|
+
if (rel.startsWith('..') || isAbsolute(rel)) return null
|
|
36
|
+
return abs
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read a file's text, or null if missing/unreadable. */
|
|
40
|
+
function readText(abs) {
|
|
41
|
+
try {
|
|
42
|
+
return readFileSync(abs, 'utf-8')
|
|
43
|
+
} catch {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── #1525: ReDoS + unbounded-read hardening for registry-supplied regexes ────────────────────────
|
|
49
|
+
//
|
|
50
|
+
// The gold-registry is PROJECT-AUTHORED data and, in a consumer that wires this audit into CI and
|
|
51
|
+
// accepts fork PRs, an attacker controls both the registry pattern AND the scanned file contents.
|
|
52
|
+
// Two DETERMINISTIC layers (fixed input ⇒ fixed verdict ⇒ engine-parity), byte-identical to
|
|
53
|
+
// src/conformance/shared.ts: (1) readScanText caps the bytes a registry regex runs over; (2)
|
|
54
|
+
// hasNestedUnboundedQuantifier rejects the catastrophic-backtracking family at compile. A bare
|
|
55
|
+
// try/catch around new RegExp() only catches invalid-syntax — it never stops (a+)+$ from hanging.
|
|
56
|
+
|
|
57
|
+
/** Byte cap for any file a registry-supplied regex is run over (ReDoS input bound, ~2 MB). */
|
|
58
|
+
const MAX_SCAN_BYTES = 2_000_000
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read a file for regex scanning, capped at MAX_SCAN_BYTES. Returns { ok:true, text } or
|
|
62
|
+
* { ok:false, reason:'unreadable'|'oversize' }; an over-cap file is NEVER read (fails closed).
|
|
63
|
+
*/
|
|
64
|
+
function readScanText(abs) {
|
|
65
|
+
let size
|
|
66
|
+
try {
|
|
67
|
+
size = statSync(abs).size
|
|
68
|
+
} catch {
|
|
69
|
+
return { ok: false, reason: 'unreadable' }
|
|
70
|
+
}
|
|
71
|
+
if (size > MAX_SCAN_BYTES) return { ok: false, reason: 'oversize' }
|
|
72
|
+
const text = readText(abs)
|
|
73
|
+
return text === null ? { ok: false, reason: 'unreadable' } : { ok: true, text }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Advance past a `[...]` char class starting at `i` (on '['). Returns the index just after ']'. */
|
|
77
|
+
function skipCharClass(src, i) {
|
|
78
|
+
let j = i + 1
|
|
79
|
+
if (src[j] === '^') j++
|
|
80
|
+
if (src[j] === ']') j++ // a ']' immediately after '[' / '[^' is a literal class member
|
|
81
|
+
while (j < src.length && src[j] !== ']') {
|
|
82
|
+
if (src[j] === '\\') j++ // an escaped char inside the class spans two positions
|
|
83
|
+
j++
|
|
84
|
+
}
|
|
85
|
+
return j + 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Parse a `{...}` quantifier at `i` (on '{'). isQuant=false ⇒ '{' is a literal, not a quantifier. */
|
|
89
|
+
function braceQuantifier(src, i) {
|
|
90
|
+
const close = src.indexOf('}', i)
|
|
91
|
+
if (close < 0) return { isQuant: false }
|
|
92
|
+
const body = src.slice(i + 1, close)
|
|
93
|
+
// JS quantifier syntax: {n}, {n,}, {n,m}. {,m} is NOT a quantifier (treated as a literal).
|
|
94
|
+
if (!/^\d+(,\d*)?$/.test(body)) return { isQuant: false }
|
|
95
|
+
return { isQuant: true, unbounded: /^\d+,$/.test(body), next: close + 1 }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* True when `source` applies an UNBOUNDED quantifier (`*`, `+`, `{n,}`) to a group whose body itself
|
|
100
|
+
* contains an unbounded quantifier — the nested-quantifier / star-height≥2 family (e.g. `(a+)+`,
|
|
101
|
+
* `(.*)+`, `((a+))+`) whose backtracking grows exponentially with input length. One linear,
|
|
102
|
+
* dependency-free, DETERMINISTIC scan, byte-identical to src/conformance/shared.ts. Deliberately
|
|
103
|
+
* narrow defense-in-depth (paired with MAX_SCAN_BYTES): not every ReDoS family is modelled.
|
|
104
|
+
*/
|
|
105
|
+
function hasNestedUnboundedQuantifier(source) {
|
|
106
|
+
const groupSawUnbounded = [false]
|
|
107
|
+
let prevGroupSawUnbounded = false
|
|
108
|
+
let i = 0
|
|
109
|
+
const n = source.length
|
|
110
|
+
while (i < n) {
|
|
111
|
+
const ch = source[i]
|
|
112
|
+
if (ch === '\\') {
|
|
113
|
+
i += 2
|
|
114
|
+
prevGroupSawUnbounded = false
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
if (ch === '[') {
|
|
118
|
+
i = skipCharClass(source, i)
|
|
119
|
+
prevGroupSawUnbounded = false
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if (ch === '(') {
|
|
123
|
+
groupSawUnbounded.push(false)
|
|
124
|
+
i++
|
|
125
|
+
prevGroupSawUnbounded = false
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (ch === ')') {
|
|
129
|
+
const closed = groupSawUnbounded.length > 1 ? (groupSawUnbounded.pop() ?? false) : false
|
|
130
|
+
if (closed) groupSawUnbounded[groupSawUnbounded.length - 1] = true
|
|
131
|
+
prevGroupSawUnbounded = closed
|
|
132
|
+
i++
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
let unbounded = false
|
|
136
|
+
let next = i + 1
|
|
137
|
+
if (ch === '*' || ch === '+') {
|
|
138
|
+
unbounded = true
|
|
139
|
+
} else if (ch === '{') {
|
|
140
|
+
const q = braceQuantifier(source, i)
|
|
141
|
+
if (q.isQuant) {
|
|
142
|
+
unbounded = q.unbounded
|
|
143
|
+
next = q.next
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (unbounded) {
|
|
147
|
+
if (prevGroupSawUnbounded) return true
|
|
148
|
+
groupSawUnbounded[groupSawUnbounded.length - 1] = true
|
|
149
|
+
prevGroupSawUnbounded = false
|
|
150
|
+
i = next
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
prevGroupSawUnbounded = false
|
|
154
|
+
i = next
|
|
155
|
+
}
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 1-based line number of the first occurrence of `needle` in `text`, or null. */
|
|
160
|
+
function lineOf(text, needle) {
|
|
161
|
+
const idx = text.indexOf(needle)
|
|
162
|
+
if (idx < 0) return null
|
|
163
|
+
let line = 1
|
|
164
|
+
for (let i = 0; i < idx; i++) if (text[i] === '\n') line++
|
|
165
|
+
return line
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 1-based line number of the character at `idx` in `text`. */
|
|
169
|
+
function lineAtIndex(text, idx) {
|
|
170
|
+
let line = 1
|
|
171
|
+
for (let i = 0; i < idx && i < text.length; i++) if (text[i] === '\n') line++
|
|
172
|
+
return line
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Constrained deterministic glob (#1470) ──────────────────────────────────────
|
|
176
|
+
//
|
|
177
|
+
// Glob support reuses scripts/lib/glob-walk.mjs (globToRegExp/validateGlob/walkRepo: POSIX paths,
|
|
178
|
+
// prunes SKIP_DIRS) — one canonical matcher, never a hand-written copy (CANON-16). The TS engine
|
|
179
|
+
// mirrors this via src/conformance/shared.ts expandGlob (engine-parity gate). Both sort with a plain
|
|
180
|
+
// `.sort()` (code-unit), NEVER localeCompare, so the file order is byte-identical across engines.
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Expand a repo-rooted glob to a SORTED array of repo-relative POSIX paths, or null when the glob
|
|
184
|
+
* is invalid (absolute / `..`-traversal / non-string / empty). An empty array is a valid "matched
|
|
185
|
+
* nothing". The glob's RegExp is compiled ONCE (via globToRegExp) and `.test()`d per file, not
|
|
186
|
+
* recompiled per file (#1522/#1600).
|
|
187
|
+
*
|
|
188
|
+
* `files` is an OPTIONAL pre-walked repo file list (#1600): when a single evaluate() resolves several
|
|
189
|
+
* globs it walks the tree once and threads the result here, so K glob checks share ONE tree-walk
|
|
190
|
+
* instead of K. Absent ⇒ this walks the tree itself (the standalone-call contract is unchanged).
|
|
191
|
+
* Output-invariant ⇒ engine-parity stays byte-identical; mirrors src/conformance/shared.ts expandGlob.
|
|
192
|
+
*/
|
|
193
|
+
function expandGlob(root, pattern, files) {
|
|
194
|
+
if (typeof pattern !== 'string' || pattern === '' || !validateGlob(pattern)) return null
|
|
195
|
+
const re = globToRegExp(pattern)
|
|
196
|
+
return (files ?? walkRepo(root)).filter((f) => re.test(f)).sort()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Deterministic, locale-independent string order (UTF-16 code units) — byte-identical across the
|
|
201
|
+
* .mjs and TS engines and immune to Node/ICU collation drift (#1471). Used for every check /
|
|
202
|
+
* dimension sort so the scored payload's ordering can never silently diverge between engines.
|
|
203
|
+
*/
|
|
204
|
+
function cmpCodeUnit(a, b) {
|
|
205
|
+
return a < b ? -1 : a > b ? 1 : 0
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve a glob-check's `args.glob` to a SORTED match list. Returns { ok:true, glob, matched } on
|
|
210
|
+
* success, or { ok:false, result } (a verified-N) when the glob is invalid/empty — shared by every
|
|
211
|
+
* glob-based check so the invalid-glob guard exists once (mirrored in src/conformance/engine.ts).
|
|
212
|
+
* `getFiles` threads the per-evaluate memoized walk (#1600) so glob checks share one tree-walk; an
|
|
213
|
+
* empty glob short-circuits to null WITHOUT triggering the walk (K=0 ⇒ 0 walks).
|
|
214
|
+
*/
|
|
215
|
+
function resolveGlobArg(args, root, getFiles) {
|
|
216
|
+
const glob = typeof args.glob === 'string' ? args.glob : ''
|
|
217
|
+
const matched = glob === '' ? null : expandGlob(root, glob, getFiles())
|
|
218
|
+
if (matched === null) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
result: { verdict: 'N', evidence: { file: glob, detail: 'invalid or empty glob' } },
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { ok: true, glob, matched }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Whether git tracks the executable bit in this repo (core.fileMode). Reads `<root>/.git/config`
|
|
229
|
+
* deterministically (no spawn — INV-12); only an explicit `filemode = false` disables it. A missing
|
|
230
|
+
* or unreadable config (e.g. a worktree pointer file, or a non-git fixture) ⇒ treated as enabled, so
|
|
231
|
+
* the exec-bit check still runs. Mirrored byte-for-byte in src/conformance/engine.ts.
|
|
232
|
+
*/
|
|
233
|
+
function gitFileModeEnabled(root) {
|
|
234
|
+
const cfg = readText(resolve(root, '.git/config'))
|
|
235
|
+
if (cfg === null) return true
|
|
236
|
+
let inCore = false
|
|
237
|
+
let lastValue = null
|
|
238
|
+
for (const raw of cfg.split('\n')) {
|
|
239
|
+
const line = raw.trim()
|
|
240
|
+
if (line.startsWith('[')) {
|
|
241
|
+
// Only the exact top-level [core] section — a `[core "subsection"]` is NOT [core].filemode.
|
|
242
|
+
inCore = /^\[core\]/i.test(line)
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (!inCore) continue
|
|
246
|
+
// git honors the LAST value when a key is declared more than once — keep scanning, don't return.
|
|
247
|
+
const m = /^filemode\s*=\s*(\S+)/i.exec(line)
|
|
248
|
+
if (m !== null) lastValue = m[1].toLowerCase()
|
|
249
|
+
}
|
|
250
|
+
return lastValue === null ? true : lastValue !== 'false'
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── #1413: deterministic report metric extraction (json/xml/regex) ──────────────
|
|
254
|
+
//
|
|
255
|
+
// A `value` check with an `args.format` reads a PRE-GENERATED tool report deterministically (no live
|
|
256
|
+
// spawn: that would break the determinism + parity + fail-closed contracts). It extracts a single
|
|
257
|
+
// numeric metric and compares it against a per-brownfield-class bar resolved via `threshold_ref`. A
|
|
258
|
+
// check whose report file is ABSENT resolves to NA (the tool did not run / does not apply) — never a
|
|
259
|
+
// false-N. This logic is byte-identical to src/conformance/engine.ts (engine-parity.test.ts).
|
|
260
|
+
|
|
261
|
+
/** Read a numeric metric from a JSON report via a dotted path (e.g. `total.lines.pct`), or null. */
|
|
262
|
+
function extractJson(text, select) {
|
|
263
|
+
let node
|
|
264
|
+
try {
|
|
265
|
+
node = JSON.parse(text)
|
|
266
|
+
} catch {
|
|
267
|
+
return null
|
|
268
|
+
}
|
|
269
|
+
const keys = select.split('.').filter((k) => k !== '')
|
|
270
|
+
for (const [i, key] of keys.entries()) {
|
|
271
|
+
// A null/absent collection has zero elements: a TERMINAL `length` selector over a null/undefined
|
|
272
|
+
// node resolves to 0, not "no metric". golangci-lint marshals a nil (zero-issue) issue slice to
|
|
273
|
+
// JSON `null`, so a CLEAN Go lint run reads `Issues.length` = 0 ≤ ceiling (Y) instead of N "no
|
|
274
|
+
// metric for json:Issues.length" — the asymmetry that only bit the passing case (#1569).
|
|
275
|
+
if (key === 'length' && i === keys.length - 1 && (node === null || node === undefined)) {
|
|
276
|
+
return 0
|
|
277
|
+
}
|
|
278
|
+
if (node === null || typeof node !== 'object') return null
|
|
279
|
+
node = node[key]
|
|
280
|
+
}
|
|
281
|
+
const n = typeof node === 'number' ? node : Number(node)
|
|
282
|
+
return Number.isFinite(n) ? n : null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// A real element open ends in whitespace, '>' or '/' (not another name char) — mirrors the TS
|
|
286
|
+
// engine's isXmlElementBoundary so neither `count:` nor `attr:` matches a tag PREFIX.
|
|
287
|
+
function isXmlElementBoundary(c) {
|
|
288
|
+
return (
|
|
289
|
+
c === undefined || c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '>' || c === '/'
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Index of the Nth `<tag` whose next char ends the element name (a real open, never a tag PREFIX),
|
|
294
|
+
// or -1. `nth < 0` ⇒ count all such opens instead. Shared by `count:` and `attr:` so the
|
|
295
|
+
// prefix-boundary guard lives once (mirrors src/conformance/engine.ts scanElementOpens).
|
|
296
|
+
function scanElementOpens(text, tag, nth) {
|
|
297
|
+
const needle = `<${tag}`
|
|
298
|
+
let count = 0
|
|
299
|
+
let from = 0
|
|
300
|
+
for (;;) {
|
|
301
|
+
const i = text.indexOf(needle, from)
|
|
302
|
+
if (i < 0) break
|
|
303
|
+
if (isXmlElementBoundary(text[i + needle.length])) {
|
|
304
|
+
if (nth >= 0 && count === nth) return i
|
|
305
|
+
count++
|
|
306
|
+
}
|
|
307
|
+
from = i + needle.length
|
|
308
|
+
}
|
|
309
|
+
return nth >= 0 ? -1 : count
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function extractXmlAttr(text, spec) {
|
|
313
|
+
const at = spec.indexOf('@')
|
|
314
|
+
if (at < 0) return null
|
|
315
|
+
const tag = spec.slice(0, at)
|
|
316
|
+
const attr = spec.slice(at + 1)
|
|
317
|
+
if (tag === '' || attr === '') return null
|
|
318
|
+
// Read from the FIRST boundary-valid `<tag` open — never a tag prefix.
|
|
319
|
+
const open = scanElementOpens(text, tag, 0)
|
|
320
|
+
if (open < 0) return null
|
|
321
|
+
const close = text.indexOf('>', open)
|
|
322
|
+
const segment = close < 0 ? text.slice(open) : text.slice(open, close)
|
|
323
|
+
// Guard the attr RegExp build: a regex metachar in the attr name (`attr:a@(`) would otherwise
|
|
324
|
+
// throw — yield null (⇒ per-check N 'no metric') instead of crashing the whole audit. Reject the
|
|
325
|
+
// catastrophic-backtracking family FIRST (matching this file's 3 other dynamic-RegExp sites) lest
|
|
326
|
+
// a valid `(a+)+`-shaped attr name hang the audit instead of throwing (#1551).
|
|
327
|
+
if (hasNestedUnboundedQuantifier(attr)) return null
|
|
328
|
+
let m
|
|
329
|
+
try {
|
|
330
|
+
m = new RegExp(`${attr}\\s*=\\s*"([^"]*)"`).exec(segment)
|
|
331
|
+
} catch {
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
if (m === null) return null
|
|
335
|
+
const n = Number(m[1])
|
|
336
|
+
return Number.isFinite(n) ? n : null
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Parse one `<tag …>` element segment's `name="value"` pairs into a map (fixed regex, ReDoS-safe). */
|
|
340
|
+
function parseXmlAttrs(segment) {
|
|
341
|
+
const attrs = {}
|
|
342
|
+
const re = /([\w:.-]+)\s*=\s*"([^"]*)"/g
|
|
343
|
+
let m
|
|
344
|
+
while ((m = re.exec(segment)) !== null) attrs[m[1]] = m[2]
|
|
345
|
+
return attrs
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Coverage PERCENT from a JaCoCo-style report's report-TOTAL counter: the LAST
|
|
350
|
+
* `<counter type="<TYPE>" missed="M" covered="C"/>` element (JaCoCo emits counters in
|
|
351
|
+
* method → class → package → report order, so the report total is last in document order),
|
|
352
|
+
* computed as C*100/(C+M). Selecting the LAST match — not the leftmost — and dividing covered by
|
|
353
|
+
* the total is what makes this a real coverage RATIO instead of one arbitrary method's raw
|
|
354
|
+
* covered-line COUNT (#1629). Null when no such counter exists, an attr is missing/non-numeric, or
|
|
355
|
+
* the total is 0 (0/0 asserts no coverage). `(C*100)/total` keeps clean integers exactly representable.
|
|
356
|
+
*/
|
|
357
|
+
function extractCoveragePercent(text, counterType) {
|
|
358
|
+
const needle = '<counter'
|
|
359
|
+
let from = 0
|
|
360
|
+
let covered = 0
|
|
361
|
+
let missed = 0
|
|
362
|
+
let found = false
|
|
363
|
+
for (;;) {
|
|
364
|
+
const i = text.indexOf(needle, from)
|
|
365
|
+
if (i < 0) break
|
|
366
|
+
from = i + needle.length
|
|
367
|
+
if (!isXmlElementBoundary(text[i + needle.length])) continue
|
|
368
|
+
const close = text.indexOf('>', i)
|
|
369
|
+
const segment = close < 0 ? text.slice(i) : text.slice(i, close)
|
|
370
|
+
const attrs = parseXmlAttrs(segment)
|
|
371
|
+
if (attrs['type'] !== counterType) continue
|
|
372
|
+
const c = Number(attrs['covered'])
|
|
373
|
+
const m = Number(attrs['missed'])
|
|
374
|
+
if (!Number.isFinite(c) || !Number.isFinite(m)) continue
|
|
375
|
+
covered = c
|
|
376
|
+
missed = m
|
|
377
|
+
found = true
|
|
378
|
+
}
|
|
379
|
+
if (!found) return null
|
|
380
|
+
const total = covered + missed
|
|
381
|
+
if (total <= 0) return null
|
|
382
|
+
return (covered * 100) / total
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Read a numeric metric from an XML report. Selectors (deterministic, dependency-free):
|
|
387
|
+
* `count:tag` → number of `<tag` occurrences (open or self-closing)
|
|
388
|
+
* `attr:tag@name` → the numeric `name="…"` attribute of the first `<tag …>` element
|
|
389
|
+
* `coverage:TYPE` → report-total coverage PERCENT of the LAST `<counter type="TYPE">` (#1629)
|
|
390
|
+
*/
|
|
391
|
+
function extractXml(text, select) {
|
|
392
|
+
if (select.startsWith('count:')) {
|
|
393
|
+
const tag = select.slice('count:'.length)
|
|
394
|
+
if (tag === '') return null
|
|
395
|
+
return scanElementOpens(text, tag, -1)
|
|
396
|
+
}
|
|
397
|
+
if (select.startsWith('attr:')) {
|
|
398
|
+
return extractXmlAttr(text, select.slice('attr:'.length))
|
|
399
|
+
}
|
|
400
|
+
if (select.startsWith('coverage:')) {
|
|
401
|
+
const counterType = select.slice('coverage:'.length)
|
|
402
|
+
if (counterType === '') return null
|
|
403
|
+
return extractCoveragePercent(text, counterType)
|
|
404
|
+
}
|
|
405
|
+
return null
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Read a numeric metric from text via a regex whose first capture group is the number. */
|
|
409
|
+
function extractRegex(text, select) {
|
|
410
|
+
// Reject catastrophic-backtracking patterns (#1525) — a valid-syntax ReDoS regex would otherwise
|
|
411
|
+
// hang on adversarial report text; the surrounding try/catch only catches invalid syntax.
|
|
412
|
+
if (hasNestedUnboundedQuantifier(select)) return null
|
|
413
|
+
let re
|
|
414
|
+
try {
|
|
415
|
+
re = new RegExp(select)
|
|
416
|
+
} catch {
|
|
417
|
+
return null
|
|
418
|
+
}
|
|
419
|
+
const m = re.exec(text)
|
|
420
|
+
if (m === null || m[1] === undefined) return null
|
|
421
|
+
const n = Number(m[1])
|
|
422
|
+
return Number.isFinite(n) ? n : null
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Total statement (≈ line) coverage PERCENT from a Go `go test -coverprofile` profile — the real
|
|
427
|
+
* artifact arbiter emits (there is no Go coverage.json). Each data line is
|
|
428
|
+
* `file.go:sl.sc,el.ec numStmt hitCount`; sum numStmt over all blocks and over the hit subset, then
|
|
429
|
+
* coveredStmts*100/totalStmts — exactly what `go tool cover -func` reports as total (#1629).
|
|
430
|
+
* Deterministic + dependency-free. Null when the profile has no data block or zero total statements.
|
|
431
|
+
*/
|
|
432
|
+
function extractGoCoverProfile(text) {
|
|
433
|
+
let total = 0
|
|
434
|
+
let covered = 0
|
|
435
|
+
let sawBlock = false
|
|
436
|
+
for (const raw of text.split('\n')) {
|
|
437
|
+
const m = /:\d+\.\d+,\d+\.\d+\s+(\d+)\s+(\d+)\s*$/.exec(raw)
|
|
438
|
+
if (m === null) continue
|
|
439
|
+
const numStmt = Number(m[1])
|
|
440
|
+
const hits = Number(m[2])
|
|
441
|
+
if (!Number.isFinite(numStmt) || !Number.isFinite(hits)) continue
|
|
442
|
+
total += numStmt
|
|
443
|
+
if (hits > 0) covered += numStmt
|
|
444
|
+
sawBlock = true
|
|
445
|
+
}
|
|
446
|
+
if (!sawBlock || total <= 0) return null
|
|
447
|
+
return (covered * 100) / total
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Extract a numeric metric from a report's text for the given format + selector, or null. */
|
|
451
|
+
function extractMetric(text, format, select) {
|
|
452
|
+
if (format === 'json') return extractJson(text, select)
|
|
453
|
+
if (format === 'xml') return extractXml(text, select)
|
|
454
|
+
if (format === 'regex') return extractRegex(text, select)
|
|
455
|
+
if (format === 'go-coverprofile') return extractGoCoverProfile(text)
|
|
456
|
+
return null
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Apply a comparison operator. Unknown op fails closed (false). */
|
|
460
|
+
function compareValue(actual, op, bar) {
|
|
461
|
+
if (op === 'gte') return actual >= bar
|
|
462
|
+
if (op === 'lte') return actual <= bar
|
|
463
|
+
if (op === 'eq') return actual === bar
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Resolve the comparison bar for a value check: `threshold_ref` row keyed by the active brownfield
|
|
469
|
+
* class, else the literal `args.expected`. Returns null when neither yields a finite number.
|
|
470
|
+
*/
|
|
471
|
+
function resolveBar(check, options) {
|
|
472
|
+
const ref = check.threshold_ref
|
|
473
|
+
if (ref !== undefined && ref !== '') {
|
|
474
|
+
const table = options.thresholds ?? {}
|
|
475
|
+
const row = table[ref]
|
|
476
|
+
if (row === undefined) return null
|
|
477
|
+
const cls = options.brownfieldClass ?? 'gold'
|
|
478
|
+
const bar = Object.prototype.hasOwnProperty.call(row, cls) ? row[cls] : undefined
|
|
479
|
+
return typeof bar === 'number' && Number.isFinite(bar) ? bar : null
|
|
480
|
+
}
|
|
481
|
+
const lit = check.args?.expected
|
|
482
|
+
return typeof lit === 'number' && Number.isFinite(lit) ? lit : null
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* value-op report-extraction evaluator (#1413). Reads a pre-generated tool report; absent report ⇒
|
|
487
|
+
* NA (no false-N). Bar resolved per brownfield class via threshold_ref / args.expected.
|
|
488
|
+
*/
|
|
489
|
+
function evalValueReport(abs, rel, check, options) {
|
|
490
|
+
// Absent report ⇒ NA: the tool did not run / does not apply for this stack (never a false-N).
|
|
491
|
+
if (!existsSync(abs)) return { verdict: 'NA', evidence: null }
|
|
492
|
+
const args = check.args || {}
|
|
493
|
+
const format = String(args.format ?? '')
|
|
494
|
+
const select = String(args.select ?? '')
|
|
495
|
+
const op = String(args.op ?? '')
|
|
496
|
+
const bar = resolveBar(check, options)
|
|
497
|
+
if (bar === null) {
|
|
498
|
+
return { verdict: 'N', evidence: { file: rel, detail: 'unresolved threshold' } }
|
|
499
|
+
}
|
|
500
|
+
// Capped read (#1525): a regex-format report is fed to a registry regex — bound the bytes it sees.
|
|
501
|
+
const read = readScanText(abs)
|
|
502
|
+
if (!read.ok) {
|
|
503
|
+
const detail = read.reason === 'oversize' ? 'report too large to scan' : 'unreadable'
|
|
504
|
+
return { verdict: 'N', evidence: { file: rel, detail } }
|
|
505
|
+
}
|
|
506
|
+
const text = read.text
|
|
507
|
+
const actual = extractMetric(text, format, select)
|
|
508
|
+
if (actual === null) {
|
|
509
|
+
return { verdict: 'N', evidence: { file: rel, detail: `no metric for ${format}:${select}` } }
|
|
510
|
+
}
|
|
511
|
+
const pass = compareValue(actual, op, bar)
|
|
512
|
+
return pass
|
|
513
|
+
? { verdict: 'Y', evidence: { file: rel, detail: `${actual} ${op} ${bar}` } }
|
|
514
|
+
: { verdict: 'N', evidence: { file: rel, detail: `${actual} !${op} ${bar}` } }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ── version_consistency: a VERSION file ↔ the latest CHANGELOG entry ─────────────
|
|
518
|
+
//
|
|
519
|
+
// Catches release-hygiene drift: the declared VERSION must equal the newest version recorded in
|
|
520
|
+
// the CHANGELOG. Pure text read + a single regex (no spawn, no wall-clock) — byte-deterministic.
|
|
521
|
+
|
|
522
|
+
/** First capture group of `pattern` (multiline) in `text` = the latest declared version, or null. */
|
|
523
|
+
function latestChangelogVersion(text, pattern) {
|
|
524
|
+
if (typeof pattern !== 'string' || pattern === '') return null
|
|
525
|
+
// Reject catastrophic-backtracking patterns (#1525) before running over the full changelog text.
|
|
526
|
+
if (hasNestedUnboundedQuantifier(pattern)) return null
|
|
527
|
+
let re
|
|
528
|
+
try {
|
|
529
|
+
re = new RegExp(pattern, 'm')
|
|
530
|
+
} catch {
|
|
531
|
+
return null
|
|
532
|
+
}
|
|
533
|
+
const m = re.exec(text)
|
|
534
|
+
// A falsy/empty capture is NOT a version — return null so the caller scores P (indeterminate),
|
|
535
|
+
// never a false Y (e.g. an empty trimmed VERSION === an empty capture).
|
|
536
|
+
return m !== null && m[1] ? m[1] : null
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Read a STRING value from a JSON document via a dotted path (e.g. `version` in package.json), or
|
|
541
|
+
* null when the text is not JSON / the path is absent / the value is not a non-empty string. The
|
|
542
|
+
* string twin of extractJson — a non-string field is never coerced (so a missing version scores P,
|
|
543
|
+
* never a false Y).
|
|
544
|
+
*/
|
|
545
|
+
function extractJsonString(text, select) {
|
|
546
|
+
let node
|
|
547
|
+
try {
|
|
548
|
+
node = JSON.parse(text)
|
|
549
|
+
} catch {
|
|
550
|
+
return null
|
|
551
|
+
}
|
|
552
|
+
for (const key of select.split('.')) {
|
|
553
|
+
if (key === '') continue
|
|
554
|
+
if (node === null || typeof node !== 'object') return null
|
|
555
|
+
node = node[key]
|
|
556
|
+
}
|
|
557
|
+
return typeof node === 'string' && node !== '' ? node : null
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Classify an unusable declared version without letting an empty artifact earn partial credit. */
|
|
561
|
+
function invalidVersionResult(version, vFile, vSelect) {
|
|
562
|
+
if (version === null)
|
|
563
|
+
return { verdict: 'P', evidence: { file: vFile, detail: `no ${vSelect} in version file` } }
|
|
564
|
+
if (version === '')
|
|
565
|
+
return { verdict: 'N', evidence: { file: vFile, detail: 'empty version file' } }
|
|
566
|
+
return null
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* version_consistency evaluator. Y = the declared version equals the latest CHANGELOG entry; P =
|
|
571
|
+
* both present and substantively divergent OR no substantive changelog entry matches the pattern
|
|
572
|
+
* OR the version is indeterminate (indeterminate — never a false Y); N = a required file is
|
|
573
|
+
* missing, unreadable, or empty.
|
|
574
|
+
* The version comes from a plain-text file (trimmed) or, when `version_select` is set, from a
|
|
575
|
+
* dotted JSON path inside `version_file` (e.g. version_file: package.json, version_select: version).
|
|
576
|
+
*/
|
|
577
|
+
function evalVersionConsistency(args, root) {
|
|
578
|
+
const vFile = typeof args.version_file === 'string' ? args.version_file : ''
|
|
579
|
+
const cFile = typeof args.changelog_file === 'string' ? args.changelog_file : ''
|
|
580
|
+
const vSelect = typeof args.version_select === 'string' ? args.version_select : ''
|
|
581
|
+
const vAbs = safeResolve(root, vFile)
|
|
582
|
+
const cAbs = safeResolve(root, cFile)
|
|
583
|
+
if (vAbs === null || cAbs === null) {
|
|
584
|
+
return { verdict: 'N', evidence: { file: vFile || cFile, detail: 'invalid path' } }
|
|
585
|
+
}
|
|
586
|
+
const vText = readText(vAbs)
|
|
587
|
+
if (vText === null)
|
|
588
|
+
return { verdict: 'N', evidence: { file: vFile, detail: 'missing version file' } }
|
|
589
|
+
// Capped read (#1525): the changelog is fed to a registry regex — bound the bytes it runs over.
|
|
590
|
+
const cRead = readScanText(cAbs)
|
|
591
|
+
if (!cRead.ok) {
|
|
592
|
+
const detail = cRead.reason === 'oversize' ? 'changelog too large to scan' : 'missing changelog'
|
|
593
|
+
return { verdict: 'N', evidence: { file: cFile, detail } }
|
|
594
|
+
}
|
|
595
|
+
const cText = cRead.text
|
|
596
|
+
if (cText.trim() === '')
|
|
597
|
+
return { verdict: 'N', evidence: { file: cFile, detail: 'present but empty' } }
|
|
598
|
+
const version = vSelect !== '' ? extractJsonString(vText, vSelect) : vText.trim()
|
|
599
|
+
const invalidVersion = invalidVersionResult(version, vFile, vSelect)
|
|
600
|
+
if (invalidVersion !== null) return invalidVersion
|
|
601
|
+
const latest = latestChangelogVersion(cText, args.changelog_pattern)
|
|
602
|
+
if (latest === null) {
|
|
603
|
+
return {
|
|
604
|
+
verdict: 'P',
|
|
605
|
+
evidence: { file: cFile, detail: 'no changelog entry matches the pattern' },
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (version === latest) {
|
|
609
|
+
return { verdict: 'Y', evidence: { file: vFile, detail: `${version} == ${latest}` } }
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
verdict: 'P',
|
|
613
|
+
evidence: { file: cFile, detail: `VERSION ${version} != CHANGELOG ${latest}` },
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ── Per-type check evaluators (extracted to mirror src/conformance/engine.ts) ────
|
|
618
|
+
// The dispatch in evalCheck stays a thin if-chain over these handlers so the two engines keep an
|
|
619
|
+
// identical structure (parity) and no single function grows past the complexity ceiling as types
|
|
620
|
+
// are added. file_exists rejects directories, returns N for empty files, and returns identical
|
|
621
|
+
// evidence to the TS engine (removes previously-tolerated directory/evidence divergence).
|
|
622
|
+
|
|
623
|
+
function evalFileExists(abs, rel) {
|
|
624
|
+
if (!existsSync(abs)) return { verdict: 'N', evidence: { file: rel, detail: 'missing' } }
|
|
625
|
+
try {
|
|
626
|
+
if (statSync(abs).isDirectory())
|
|
627
|
+
return { verdict: 'N', evidence: { file: rel, detail: 'is a directory' } }
|
|
628
|
+
} catch {
|
|
629
|
+
return { verdict: 'N', evidence: { file: rel, detail: 'unreadable' } }
|
|
630
|
+
}
|
|
631
|
+
const text = readText(abs)
|
|
632
|
+
if (text === null) return { verdict: 'N', evidence: { file: rel, detail: 'unreadable' } }
|
|
633
|
+
// A presence check asks whether content exists: empty artifacts earn nothing. P is reserved for
|
|
634
|
+
// version_consistency's real-but-divergent substance, not an empty file that touch can create.
|
|
635
|
+
if (text.trim() === '')
|
|
636
|
+
return { verdict: 'N', evidence: { file: rel, detail: 'present but empty' } }
|
|
637
|
+
return { verdict: 'Y', evidence: { file: rel } }
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function evalFileContains(abs, rel, pattern) {
|
|
641
|
+
// An empty/missing pattern is a registry authoring error, never a satisfied property: `''.indexOf`
|
|
642
|
+
// matches at index 0 of any readable file, so an omitted `pattern` would fake-green a verified-Y
|
|
643
|
+
// with zero evidence that anything is present. Refuse it — mirrors evalForbiddenPattern's
|
|
644
|
+
// empty-pattern N so the anti-fake-green contract is uniform across check types (#1591).
|
|
645
|
+
if (pattern === '') {
|
|
646
|
+
return { verdict: 'N', evidence: { file: rel, detail: 'empty or missing pattern' } }
|
|
647
|
+
}
|
|
648
|
+
const text = readText(abs)
|
|
649
|
+
if (text === null) return { verdict: 'N', evidence: { file: rel, detail: 'missing' } }
|
|
650
|
+
const line = lineOf(text, pattern)
|
|
651
|
+
return line !== null
|
|
652
|
+
? { verdict: 'Y', evidence: { file: rel, line } }
|
|
653
|
+
: { verdict: 'N', evidence: { file: rel, detail: `pattern not found: ${pattern}` } }
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Count non-overlapping occurrences of `pattern` in `text` (0 for an empty pattern). */
|
|
657
|
+
function countOccurrences(text, pattern) {
|
|
658
|
+
if (pattern.length === 0) return 0
|
|
659
|
+
let count = 0
|
|
660
|
+
let from = 0
|
|
661
|
+
for (;;) {
|
|
662
|
+
const i = text.indexOf(pattern, from)
|
|
663
|
+
if (i < 0) break
|
|
664
|
+
count++
|
|
665
|
+
from = i + pattern.length
|
|
666
|
+
}
|
|
667
|
+
return count
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function evalCountMatches(abs, rel, pattern, want) {
|
|
671
|
+
const text = readText(abs)
|
|
672
|
+
if (text === null) return { verdict: 'N', evidence: { file: rel, detail: 'missing' } }
|
|
673
|
+
const count = countOccurrences(text, pattern)
|
|
674
|
+
if (count >= want) return { verdict: 'Y', evidence: { file: rel, detail: `count=${count}` } }
|
|
675
|
+
if (count > 0) return { verdict: 'P', evidence: { file: rel, detail: `count=${count}/${want}` } }
|
|
676
|
+
return { verdict: 'N', evidence: { file: rel, detail: `count=0/${want}` } }
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function evalValue(abs, rel, expected) {
|
|
680
|
+
// Same empty-needle hole as evalFileContains: `''.indexOf` matches line 1 of any readable file, so
|
|
681
|
+
// a value check that omits `equals` would fake-green to Y with no evidence. Refuse it (#1591).
|
|
682
|
+
if (expected === '') {
|
|
683
|
+
return { verdict: 'N', evidence: { file: rel, detail: 'empty or missing pattern' } }
|
|
684
|
+
}
|
|
685
|
+
const text = readText(abs)
|
|
686
|
+
if (text === null) return { verdict: 'N', evidence: { file: rel, detail: 'missing' } }
|
|
687
|
+
const line = lineOf(text, expected)
|
|
688
|
+
return line !== null
|
|
689
|
+
? { verdict: 'Y', evidence: { file: rel, line } }
|
|
690
|
+
: { verdict: 'N', evidence: { file: rel, detail: `value not present: ${expected}` } }
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ── forbidden_pattern: a regex that must NOT appear in any file under a glob (#1470) ──
|
|
694
|
+
//
|
|
695
|
+
// Scores an ABSENCE property with anti-fake-green hardening (red-team): an empty scan must never
|
|
696
|
+
// fake-green. Verdict ladder (order matters — evaluated top-down, byte-identical to engine.ts):
|
|
697
|
+
// empty/non-string pattern ⇒ N · invalid regex ⇒ N · invalid/empty glob ⇒ N ·
|
|
698
|
+
// exclude entry with a glob char ⇒ N (literal-only) · exclude_paths set w/o rationale ⇒ N ·
|
|
699
|
+
// glob matched 0 files ⇒ NA (nothing of this kind exists) ·
|
|
700
|
+
// excludes removed ALL matched files ⇒ N (refuse to fake-green an emptied scan) ·
|
|
701
|
+
// pattern found ⇒ N (first SORTED file + line) · absent across every scanned file ⇒ Y.
|
|
702
|
+
|
|
703
|
+
/** True if a string contains any glob metacharacter (so it is NOT a literal path). */
|
|
704
|
+
function hasGlobChar(s) {
|
|
705
|
+
return /[*?[\]]/.test(s)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function evalForbiddenPattern(args, root, getFiles) {
|
|
709
|
+
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
|
710
|
+
if (pattern === '') {
|
|
711
|
+
return { verdict: 'N', evidence: { file: '', detail: 'empty or non-string pattern' } }
|
|
712
|
+
}
|
|
713
|
+
let re
|
|
714
|
+
try {
|
|
715
|
+
re = new RegExp(pattern)
|
|
716
|
+
} catch {
|
|
717
|
+
return { verdict: 'N', evidence: { file: '', detail: `invalid regex: ${pattern}` } }
|
|
718
|
+
}
|
|
719
|
+
// Reject catastrophic-backtracking patterns (#1525): a valid-syntax ReDoS regex like (a+)+$ would
|
|
720
|
+
// otherwise hang the scan over an adversarial matched file. Deterministic ⇒ engine-parity.
|
|
721
|
+
if (hasNestedUnboundedQuantifier(pattern)) {
|
|
722
|
+
return { verdict: 'N', evidence: { file: '', detail: `unsafe regex (ReDoS risk): ${pattern}` } }
|
|
723
|
+
}
|
|
724
|
+
const g = resolveGlobArg(args, root, getFiles)
|
|
725
|
+
if (!g.ok) return g.result
|
|
726
|
+
const { glob, matched } = g
|
|
727
|
+
const excludeRaw = Array.isArray(args.exclude_paths) ? args.exclude_paths : []
|
|
728
|
+
for (const ex of excludeRaw) {
|
|
729
|
+
if (typeof ex !== 'string' || hasGlobChar(ex)) {
|
|
730
|
+
return {
|
|
731
|
+
verdict: 'N',
|
|
732
|
+
evidence: { file: glob, detail: `exclude_paths must be literal: ${ex}` },
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (excludeRaw.length > 0) {
|
|
737
|
+
const rationale = typeof args.rationale === 'string' ? args.rationale.trim() : ''
|
|
738
|
+
if (rationale === '') {
|
|
739
|
+
return {
|
|
740
|
+
verdict: 'N',
|
|
741
|
+
evidence: { file: glob, detail: 'exclude_paths requires a rationale' },
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (matched.length === 0) {
|
|
746
|
+
return { verdict: 'NA', evidence: null }
|
|
747
|
+
}
|
|
748
|
+
const exclude = new Set(excludeRaw)
|
|
749
|
+
const remaining = matched.filter((f) => !exclude.has(f))
|
|
750
|
+
if (remaining.length === 0) {
|
|
751
|
+
return {
|
|
752
|
+
verdict: 'N',
|
|
753
|
+
evidence: { file: glob, detail: 'all matched files excluded (refusing fake-green)' },
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
for (const rel of remaining) {
|
|
757
|
+
const abs = safeResolve(root, rel)
|
|
758
|
+
// Capped read (#1525): an over-cap matched file fails closed — we cannot assert ABSENCE over
|
|
759
|
+
// bytes we refuse to read, so it is N (never a fake-green Y), with the cap noted in the evidence.
|
|
760
|
+
const read = abs === null ? null : readScanText(abs)
|
|
761
|
+
if (read === null || !read.ok) {
|
|
762
|
+
// A matched, non-excluded file we cannot read (or that exceeds the scan cap) ⇒ we cannot assert
|
|
763
|
+
// the pattern is ABSENT over it. Fail-closed N (never a silent skip → a chmod-000 file holding
|
|
764
|
+
// the marker must not fake-green to Y). Deterministic: `remaining` is sorted, first anomaly wins.
|
|
765
|
+
const detail =
|
|
766
|
+
read !== null && read.reason === 'oversize'
|
|
767
|
+
? `too large to scan (> ${MAX_SCAN_BYTES} bytes) — cannot verify absence`
|
|
768
|
+
: 'unreadable — cannot verify absence'
|
|
769
|
+
return { verdict: 'N', evidence: { file: rel, detail } }
|
|
770
|
+
}
|
|
771
|
+
const text = read.text
|
|
772
|
+
const m = re.exec(text)
|
|
773
|
+
if (m !== null) {
|
|
774
|
+
return {
|
|
775
|
+
verdict: 'N',
|
|
776
|
+
evidence: {
|
|
777
|
+
file: rel,
|
|
778
|
+
line: lineAtIndex(text, m.index),
|
|
779
|
+
detail: 'forbidden pattern present',
|
|
780
|
+
},
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// Y only when EVERY remaining file was actually read — the count is honest, never inflated.
|
|
785
|
+
return {
|
|
786
|
+
verdict: 'Y',
|
|
787
|
+
evidence: { file: glob, detail: `absent across ${remaining.length} file(s)` },
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ── file_stat: the executable bit on a glob of files (#1470) ─────────────────────
|
|
792
|
+
//
|
|
793
|
+
// Only the executable bit (mode & 0o111) is portable — git tracks 0o111 but read/write depend on
|
|
794
|
+
// the umask (non-deterministic), so a readable/writable request is a config error (N). Gated behind
|
|
795
|
+
// core.fileMode: when git does not track the exec bit the property is unmeasurable ⇒ NA. Symlinks
|
|
796
|
+
// are evaluated by their OWN mode (lstat), never their target's. Verdict: all exec ⇒ Y, some ⇒ P,
|
|
797
|
+
// none ⇒ N; a valid glob matching 0 files ⇒ NA; a malformed glob ⇒ N.
|
|
798
|
+
|
|
799
|
+
function evalFileStat(args, root, getFiles) {
|
|
800
|
+
const bit = typeof args.bit === 'string' ? args.bit.toLowerCase() : 'executable'
|
|
801
|
+
if (bit !== 'executable') {
|
|
802
|
+
return {
|
|
803
|
+
verdict: 'N',
|
|
804
|
+
evidence: { file: '', detail: `only the executable bit is deterministic, got: ${bit}` },
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const g = resolveGlobArg(args, root, getFiles)
|
|
808
|
+
if (!g.ok) return g.result
|
|
809
|
+
const { glob, matched } = g
|
|
810
|
+
if (matched.length === 0) {
|
|
811
|
+
return { verdict: 'NA', evidence: null }
|
|
812
|
+
}
|
|
813
|
+
if (!gitFileModeEnabled(root)) {
|
|
814
|
+
return { verdict: 'NA', evidence: null }
|
|
815
|
+
}
|
|
816
|
+
let withBit = 0
|
|
817
|
+
let firstMissing = null
|
|
818
|
+
for (const rel of matched) {
|
|
819
|
+
const abs = safeResolve(root, rel)
|
|
820
|
+
let exec = false
|
|
821
|
+
try {
|
|
822
|
+
const st = abs === null ? null : lstatSync(abs)
|
|
823
|
+
// A symlink is NOT a tracked executable regular file (git stores it as a symlink, not 0o755),
|
|
824
|
+
// and its OWN lstat mode is always 0o777 — trusting it would fake-green the exec bit. Treat a
|
|
825
|
+
// symlink as not-executable: deterministic (symlink-ness is stable) and anti-fake-green.
|
|
826
|
+
exec = st !== null && !st.isSymbolicLink() && (st.mode & 0o111) !== 0
|
|
827
|
+
} catch {
|
|
828
|
+
exec = false
|
|
829
|
+
}
|
|
830
|
+
if (exec) withBit++
|
|
831
|
+
else if (firstMissing === null) firstMissing = rel
|
|
832
|
+
}
|
|
833
|
+
if (withBit === matched.length) {
|
|
834
|
+
return {
|
|
835
|
+
verdict: 'Y',
|
|
836
|
+
evidence: { file: glob, detail: `executable across ${matched.length} file(s)` },
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (withBit === 0) {
|
|
840
|
+
return { verdict: 'N', evidence: { file: firstMissing ?? glob, detail: 'not executable' } }
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
verdict: 'P',
|
|
844
|
+
evidence: { file: firstMissing ?? glob, detail: `executable ${withBit}/${matched.length}` },
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Evaluate a single check against the repo. Thin dispatch over the per-type handlers above; returns
|
|
850
|
+
* { verdict, evidence } where evidence is { file, line?, detail? } for code-verifiable checks, or
|
|
851
|
+
* null for NA/NV. Glob/pair checks (version_consistency, forbidden_pattern, file_stat) dispatch
|
|
852
|
+
* BEFORE the single-file `args.path` resolve, since they own their own path handling.
|
|
853
|
+
*/
|
|
854
|
+
function evalCheck(check, root, options, getFiles) {
|
|
855
|
+
const type = check.type
|
|
856
|
+
if (type === 'manual') return { verdict: 'NV', evidence: null }
|
|
857
|
+
const args = check.args || {}
|
|
858
|
+
if (type === 'version_consistency') return evalVersionConsistency(args, root)
|
|
859
|
+
if (type === 'forbidden_pattern') return evalForbiddenPattern(args, root, getFiles)
|
|
860
|
+
if (type === 'file_stat') return evalFileStat(args, root, getFiles)
|
|
861
|
+
|
|
862
|
+
const abs = safeResolve(root, args.path)
|
|
863
|
+
if (abs === null) {
|
|
864
|
+
// An unresolvable / traversal path is a verified failure, with a detail note.
|
|
865
|
+
return { verdict: 'N', evidence: { file: String(args.path ?? ''), detail: 'invalid path' } }
|
|
866
|
+
}
|
|
867
|
+
const rel = args.path
|
|
868
|
+
|
|
869
|
+
if (type === 'file_exists') return evalFileExists(abs, rel)
|
|
870
|
+
if (type === 'file_contains') return evalFileContains(abs, rel, String(args.pattern ?? ''))
|
|
871
|
+
if (type === 'count_matches')
|
|
872
|
+
return evalCountMatches(abs, rel, String(args.pattern ?? ''), Number(args.min ?? 1))
|
|
873
|
+
if (type === 'value') {
|
|
874
|
+
// A value check with a report `format` reads a pre-generated tool report (#1413); without one it
|
|
875
|
+
// keeps the legacy single-line `equals`-contains behavior (back-compat — same verdicts as before).
|
|
876
|
+
return args.format
|
|
877
|
+
? evalValueReport(abs, rel, check, options || {})
|
|
878
|
+
: evalValue(abs, rel, String(args.equals ?? ''))
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Unknown check type — treat as not verified (never a silent pass).
|
|
882
|
+
return { verdict: 'N', evidence: { file: rel || '', detail: `unknown check type: ${type}` } }
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ── applies_if conditional gating (G1) ──────────────────────────────────────────
|
|
886
|
+
// Mirror of src/conformance/engine.ts (parity contract). A precondition that is MET ⇒ the check is
|
|
887
|
+
// evaluated; UNMET ⇒ NA. FAIL-SAFE: a malformed / uninterpretable precondition (unknown type,
|
|
888
|
+
// missing required field, invalid path) ⇒ the check APPLIES — a silent skip would be a false-green.
|
|
889
|
+
|
|
890
|
+
/** capability precondition: met when `name` is in the overlay set. Missing name ⇒ APPLIES. */
|
|
891
|
+
function capabilityMet(cond, overlays) {
|
|
892
|
+
const name = typeof cond.name === 'string' ? cond.name : ''
|
|
893
|
+
if (name === '') return true
|
|
894
|
+
return overlays.has(name)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** file_exists precondition: met when `path` resolves to an existing file. Bad path ⇒ APPLIES. */
|
|
898
|
+
function fileExistsMet(cond, root) {
|
|
899
|
+
const p = typeof cond.path === 'string' ? cond.path : ''
|
|
900
|
+
if (p === '') return true
|
|
901
|
+
const abs = safeResolve(root, p)
|
|
902
|
+
if (abs === null) return true
|
|
903
|
+
return existsSync(abs)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/** file_contains precondition: met when the file at `path` contains `pattern`. Malformed ⇒ APPLIES. */
|
|
907
|
+
function fileContainsMet(cond, root) {
|
|
908
|
+
const p = typeof cond.path === 'string' ? cond.path : ''
|
|
909
|
+
const pattern = typeof cond.pattern === 'string' ? cond.pattern : ''
|
|
910
|
+
if (p === '' || pattern === '') return true
|
|
911
|
+
const abs = safeResolve(root, p)
|
|
912
|
+
if (abs === null) return true
|
|
913
|
+
const text = readText(abs)
|
|
914
|
+
if (text === null) return false // marker file absent/unreadable ⇒ precondition UNMET ⇒ NA
|
|
915
|
+
return text.includes(pattern)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** count_matches precondition: met when `path` contains `pattern` ≥ `min` times. Malformed ⇒ APPLIES. */
|
|
919
|
+
function countMatchesMet(cond, root) {
|
|
920
|
+
const p = typeof cond.path === 'string' ? cond.path : ''
|
|
921
|
+
const pattern = typeof cond.pattern === 'string' ? cond.pattern : ''
|
|
922
|
+
if (p === '' || pattern === '') return true
|
|
923
|
+
const min = Number(cond.min ?? 1)
|
|
924
|
+
if (!Number.isFinite(min)) return true
|
|
925
|
+
const abs = safeResolve(root, p)
|
|
926
|
+
if (abs === null) return true
|
|
927
|
+
const text = readText(abs)
|
|
928
|
+
if (text === null) return false // marker file absent/unreadable ⇒ precondition UNMET ⇒ NA
|
|
929
|
+
return countOccurrences(text, pattern) >= min
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** Dispatch an object-form precondition. Unknown `type` ⇒ APPLIES (fail-safe, never a silent skip). */
|
|
933
|
+
function preconditionApplies(cond, overlays, root) {
|
|
934
|
+
const type = typeof cond.type === 'string' ? cond.type : ''
|
|
935
|
+
if (type === 'capability') return capabilityMet(cond, overlays)
|
|
936
|
+
if (type === 'file_exists') return fileExistsMet(cond, root)
|
|
937
|
+
if (type === 'file_contains') return fileContainsMet(cond, root)
|
|
938
|
+
if (type === 'count_matches') return countMatchesMet(cond, root)
|
|
939
|
+
return true
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* True if a check applies (applies_if absent ⇒ always). A string is an overlay/capability name
|
|
944
|
+
* (legacy). An object is a generic precondition (G1). Any other type ⇒ APPLIES (fail-safe).
|
|
945
|
+
*/
|
|
946
|
+
function isApplicable(check, overlays, root) {
|
|
947
|
+
const cond = check.applies_if
|
|
948
|
+
if (!cond || cond === 'always') return true
|
|
949
|
+
if (typeof cond === 'string') return overlays.has(cond)
|
|
950
|
+
if (typeof cond === 'object') return preconditionApplies(cond, overlays, root)
|
|
951
|
+
return true
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** Numeric verdict weight: Y=1, P=0.5, N=0; NA/NV excluded from the denominator. */
|
|
955
|
+
function verdictPoints(verdict) {
|
|
956
|
+
if (verdict === 'Y') return 1
|
|
957
|
+
if (verdict === 'P') return 0.5
|
|
958
|
+
return 0
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Evaluate the whole registry. Deterministic: checks sorted by id, no timestamps.
|
|
963
|
+
* @returns scored payload: { registryVersion, score, yCount, riskyCount, totals, dimensions, checks }
|
|
964
|
+
*/
|
|
965
|
+
function evaluateInner(registry, overlays, root, options = {}) {
|
|
966
|
+
const overlaySet = overlays instanceof Set ? overlays : new Set(overlays || [])
|
|
967
|
+
const opts = options && typeof options === 'object' ? options : {}
|
|
968
|
+
// Drop non-object entries (a stray `-`/commented item in a templated YAML list parses to null) so
|
|
969
|
+
// the valid checks are still scored and neither engine throws — shared fail-closed shape with the TS.
|
|
970
|
+
const rawChecks = (Array.isArray(registry?.checks) ? registry.checks : []).filter(
|
|
971
|
+
(c) => c && typeof c === 'object',
|
|
972
|
+
)
|
|
973
|
+
const sorted = [...rawChecks].sort((a, b) => cmpCodeUnit(String(a.id), String(b.id)))
|
|
974
|
+
|
|
975
|
+
const checks = []
|
|
976
|
+
const dims = new Map()
|
|
977
|
+
let yCount = 0
|
|
978
|
+
let riskyCount = 0
|
|
979
|
+
let earned = 0
|
|
980
|
+
let possible = 0
|
|
981
|
+
|
|
982
|
+
// Walk the repo tree at most ONCE for the whole evaluate() (#1600): the first glob check
|
|
983
|
+
// (forbidden_pattern / file_stat) triggers the walk and every later glob check reuses the cached
|
|
984
|
+
// list. A registry with no glob check never walks (K=0 ⇒ 0 walks). Per-evaluate state only — no
|
|
985
|
+
// global cache — so determinism + the fail-closed contract are untouched. Mirrors engine.ts EvalRun.
|
|
986
|
+
let walkedFiles = null
|
|
987
|
+
const getFiles = () => (walkedFiles ??= walkRepo(root))
|
|
988
|
+
|
|
989
|
+
for (const check of sorted) {
|
|
990
|
+
const applicable = isApplicable(check, overlaySet, root)
|
|
991
|
+
let verdict
|
|
992
|
+
let evidence
|
|
993
|
+
if (!applicable) {
|
|
994
|
+
verdict = 'NA'
|
|
995
|
+
evidence = null
|
|
996
|
+
} else {
|
|
997
|
+
const r = evalCheck(check, root, opts, getFiles)
|
|
998
|
+
verdict = r.verdict
|
|
999
|
+
evidence = r.evidence
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// A non-numeric/non-finite/negative weight (a registry typo like `weight: high`) yields NaN, and
|
|
1003
|
+
// a single NaN poisons the summed earned/possible accumulators for the WHOLE registry → a silent
|
|
1004
|
+
// overall score of 0. Coerce any such weight to the default 1 (mirrors src/conformance/engine.ts)
|
|
1005
|
+
// so one bad cell can never zero every sibling's score.
|
|
1006
|
+
const rawWeight = Number(check.weight ?? 1)
|
|
1007
|
+
const weight = Number.isFinite(rawWeight) && rawWeight >= 0 ? rawWeight : 1
|
|
1008
|
+
const risk = check.risk === 'RISKY' ? 'RISKY' : 'SAFE'
|
|
1009
|
+
if (risk === 'RISKY') riskyCount++
|
|
1010
|
+
if (verdict === 'Y') yCount++
|
|
1011
|
+
|
|
1012
|
+
// Scoring denominator: applicable, code-verifiable checks only (exclude NA + NV).
|
|
1013
|
+
if (verdict !== 'NA' && verdict !== 'NV') {
|
|
1014
|
+
earned += verdictPoints(verdict) * weight
|
|
1015
|
+
possible += weight
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const dimId = String(check.dimension ?? 'D-UNCLASSIFIED')
|
|
1019
|
+
if (!dims.has(dimId)) dims.set(dimId, { earned: 0, possible: 0, y: 0 })
|
|
1020
|
+
const d = dims.get(dimId)
|
|
1021
|
+
if (verdict === 'Y') d.y++
|
|
1022
|
+
if (verdict !== 'NA' && verdict !== 'NV') {
|
|
1023
|
+
d.earned += verdictPoints(verdict) * weight
|
|
1024
|
+
d.possible += weight
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
checks.push({
|
|
1028
|
+
id: String(check.id),
|
|
1029
|
+
dimension: dimId,
|
|
1030
|
+
title: String(check.title ?? ''),
|
|
1031
|
+
type: String(check.type ?? ''),
|
|
1032
|
+
verdict,
|
|
1033
|
+
weight,
|
|
1034
|
+
risk,
|
|
1035
|
+
anchor: check.anchor ? String(check.anchor) : null,
|
|
1036
|
+
evidence,
|
|
1037
|
+
})
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const score = possible > 0 ? Math.round((earned / possible) * 1000) / 10 : 0
|
|
1041
|
+
const dimensions = {}
|
|
1042
|
+
for (const [id, d] of [...dims.entries()].sort((a, b) => cmpCodeUnit(a[0], b[0]))) {
|
|
1043
|
+
dimensions[id] = {
|
|
1044
|
+
score: d.possible > 0 ? Math.round((d.earned / d.possible) * 1000) / 10 : 0,
|
|
1045
|
+
y: d.y,
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
return {
|
|
1050
|
+
registryVersion: String(registry?.version ?? '0'),
|
|
1051
|
+
score,
|
|
1052
|
+
yCount,
|
|
1053
|
+
riskyCount,
|
|
1054
|
+
totals: {
|
|
1055
|
+
checks: checks.length,
|
|
1056
|
+
y: yCount,
|
|
1057
|
+
p: checks.filter((c) => c.verdict === 'P').length,
|
|
1058
|
+
n: checks.filter((c) => c.verdict === 'N').length,
|
|
1059
|
+
na: checks.filter((c) => c.verdict === 'NA').length,
|
|
1060
|
+
nv: checks.filter((c) => c.verdict === 'NV').length,
|
|
1061
|
+
},
|
|
1062
|
+
dimensions,
|
|
1063
|
+
checks,
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Evaluate the whole registry, fail-closed: any uncaught error returns a zero-score payload rather
|
|
1069
|
+
* than throwing (byte-identical fail-closed shape to src/conformance/engine.ts — engine-parity).
|
|
1070
|
+
*/
|
|
1071
|
+
export function evaluate(registry, overlays, root, options = {}) {
|
|
1072
|
+
try {
|
|
1073
|
+
return evaluateInner(registry, overlays, root, options)
|
|
1074
|
+
} catch {
|
|
1075
|
+
return {
|
|
1076
|
+
registryVersion: '0',
|
|
1077
|
+
score: 0,
|
|
1078
|
+
yCount: 0,
|
|
1079
|
+
riskyCount: 0,
|
|
1080
|
+
totals: { checks: 0, y: 0, p: 0, n: 0, na: 0, nv: 0 },
|
|
1081
|
+
dimensions: {},
|
|
1082
|
+
checks: [],
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** Compare current vs baseline; return { ok, reasons } — ok=false on score/Y regression. */
|
|
1088
|
+
export function checkNoRegress(current, baseline) {
|
|
1089
|
+
const reasons = []
|
|
1090
|
+
if (!baseline || typeof baseline !== 'object') return { ok: true, reasons }
|
|
1091
|
+
if (typeof baseline.score === 'number' && current.score < baseline.score) {
|
|
1092
|
+
reasons.push(`score regressed: ${current.score} < baseline ${baseline.score}`)
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof baseline.yCount === 'number' && current.yCount < baseline.yCount) {
|
|
1095
|
+
reasons.push(`Y-count regressed: ${current.yCount} < baseline ${baseline.yCount}`)
|
|
1096
|
+
}
|
|
1097
|
+
return { ok: reasons.length === 0, reasons }
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/** Monotonic ratchet: per field, keep max(current, baseline) — can only tighten. */
|
|
1101
|
+
export function ratchet(current, baseline) {
|
|
1102
|
+
const prev = baseline && typeof baseline === 'object' ? baseline : {}
|
|
1103
|
+
const dimensions = {}
|
|
1104
|
+
const ids = new Set([
|
|
1105
|
+
...Object.keys(current.dimensions || {}),
|
|
1106
|
+
...Object.keys(prev.dimensions || {}),
|
|
1107
|
+
])
|
|
1108
|
+
for (const id of [...ids].sort()) {
|
|
1109
|
+
const c = current.dimensions?.[id] || { score: 0, y: 0 }
|
|
1110
|
+
const p = prev.dimensions?.[id] || { score: 0, y: 0 }
|
|
1111
|
+
dimensions[id] = { score: Math.max(c.score, p.score ?? 0), y: Math.max(c.y, p.y ?? 0) }
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
score: Math.max(current.score, typeof prev.score === 'number' ? prev.score : 0),
|
|
1115
|
+
yCount: Math.max(current.yCount, typeof prev.yCount === 'number' ? prev.yCount : 0),
|
|
1116
|
+
dimensions,
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** Baseline-shaped snapshot of a scored payload (the ratchet-compared subset, no timestamp). */
|
|
1121
|
+
export function baselineOf(current) {
|
|
1122
|
+
return { score: current.score, yCount: current.yCount, dimensions: current.dimensions }
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// ── #1414: deterministic gold-LEVEL band + "what's missing" gap report ──────────────────────────
|
|
1126
|
+
//
|
|
1127
|
+
// The level band maps a code-computed score (0–100) to a Target Maturity Level (L0–L3) keyed by
|
|
1128
|
+
// the repo's brownfieldClass: a heavy legacy repo reaches a given band at a LOWER score than a
|
|
1129
|
+
// greenfield gold repo (the realistic target differs by starting condition). Thresholds are the
|
|
1130
|
+
// minimum score required to ENTER each level; L0 is the floor (always entered).
|
|
1131
|
+
|
|
1132
|
+
/** Per-class entry thresholds: [L1, L2, L3] minimum scores. gold = strictest, heavy = most lenient. */
|
|
1133
|
+
const LEVEL_THRESHOLDS = {
|
|
1134
|
+
gold: [50, 75, 95],
|
|
1135
|
+
light: [45, 70, 90],
|
|
1136
|
+
medium: [40, 60, 85],
|
|
1137
|
+
heavy: [30, 50, 75],
|
|
1138
|
+
}
|
|
1139
|
+
const LEVELS = ['L0', 'L1', 'L2', 'L3']
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Map a score to a level band for a brownfieldClass. Pure + deterministic.
|
|
1143
|
+
* @returns { level, nextLevel, toNextLevel, brownfieldClass, thresholds }
|
|
1144
|
+
*/
|
|
1145
|
+
export function levelBand(score, brownfieldClass) {
|
|
1146
|
+
const cls = Object.prototype.hasOwnProperty.call(LEVEL_THRESHOLDS, brownfieldClass)
|
|
1147
|
+
? brownfieldClass
|
|
1148
|
+
: 'gold' // unknown class → strictest band (fail-safe, never over-credits)
|
|
1149
|
+
const thresholds = LEVEL_THRESHOLDS[cls]
|
|
1150
|
+
const s = Number.isFinite(score) ? score : 0
|
|
1151
|
+
let idx = 0
|
|
1152
|
+
for (let i = 0; i < thresholds.length; i++) if (s >= thresholds[i]) idx = i + 1
|
|
1153
|
+
const level = LEVELS[idx]
|
|
1154
|
+
const nextLevel = idx < LEVELS.length - 1 ? LEVELS[idx + 1] : null
|
|
1155
|
+
const toNextLevel =
|
|
1156
|
+
nextLevel === null ? 0 : Math.max(0, Math.round((thresholds[idx] - s) * 10) / 10)
|
|
1157
|
+
return { level, nextLevel, toNextLevel, brownfieldClass: cls, thresholds }
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* "What's missing" report: the checks with verdict N or P (the actionable gaps), grouped by
|
|
1162
|
+
* dimension (family), each with its evidence. Y/NA/NV are excluded — you cannot close an NV (code
|
|
1163
|
+
* can't verify it) or an NA (it doesn't apply) by working on it. Deterministic: dimensions sorted
|
|
1164
|
+
* by id, checks in the already-stable id order from evaluate().
|
|
1165
|
+
* @returns Array<{ dimension, title, checks: Array<{id,title,verdict,evidence,anchor}> }>
|
|
1166
|
+
*/
|
|
1167
|
+
export function gapReport(result) {
|
|
1168
|
+
const checks = Array.isArray(result?.checks) ? result.checks : []
|
|
1169
|
+
const byDim = new Map()
|
|
1170
|
+
for (const c of checks) {
|
|
1171
|
+
if (c.verdict !== 'N' && c.verdict !== 'P') continue
|
|
1172
|
+
const dim = String(c.dimension ?? 'D-UNCLASSIFIED')
|
|
1173
|
+
if (!byDim.has(dim)) byDim.set(dim, [])
|
|
1174
|
+
byDim.get(dim).push({
|
|
1175
|
+
id: c.id,
|
|
1176
|
+
title: c.title,
|
|
1177
|
+
verdict: c.verdict,
|
|
1178
|
+
anchor: c.anchor ?? null,
|
|
1179
|
+
evidence: c.evidence ?? null,
|
|
1180
|
+
})
|
|
1181
|
+
}
|
|
1182
|
+
return [...byDim.entries()]
|
|
1183
|
+
.sort((a, b) => cmpCodeUnit(a[0], b[0]))
|
|
1184
|
+
.map(([dimension, gaps]) => ({ dimension, checks: gaps }))
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// ── #1473: fail-closed freshness banner (out-of-band, NEVER in the scored payload) ───────────────
|
|
1188
|
+
//
|
|
1189
|
+
// A `value` check with an `args.format` reads a PRE-GENERATED tool report; an absent report scores
|
|
1190
|
+
// NA silently (the tool may not apply OR may simply never have run). freshness() is the out-of-band
|
|
1191
|
+
// signal that distinguishes those: it stats every value-check report path and reports FRESH/PARTIAL/
|
|
1192
|
+
// STALE. The wall-clock (mtime / now) lives ONLY here — it never enters evaluate()'s byte-deterministic
|
|
1193
|
+
// payload — so the scored artifact stays reproducible while the banner stays honest (fail-closed:
|
|
1194
|
+
// a report that is absent, or older than the window, is never counted as FRESH).
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Classify the freshness of the value-check report files declared by `registry`. Pure given (now).
|
|
1198
|
+
* FRESH — every declared report present AND within staleHours (or no reports declared at all)
|
|
1199
|
+
* PARTIAL — at least one report present, but some missing or older than the window
|
|
1200
|
+
* STALE — reports are declared but NONE are present (the tools never ran)
|
|
1201
|
+
* @param {{checks?: unknown[]}} registry
|
|
1202
|
+
* @param {string} root
|
|
1203
|
+
* @param {{ staleHours?: number, now?: number, overlays?: Set<string>|Iterable<string> }} [options]
|
|
1204
|
+
* @returns {{ status: 'FRESH'|'PARTIAL'|'STALE', staleHours: number,
|
|
1205
|
+
* counts: { total: number, present: number, fresh: number },
|
|
1206
|
+
* reports: Array<{ path: string, present: boolean, ageHours: number|null, fresh: boolean }> }}
|
|
1207
|
+
*/
|
|
1208
|
+
export function freshness(registry, root, options = {}) {
|
|
1209
|
+
const staleHours =
|
|
1210
|
+
Number.isFinite(options.staleHours) && options.staleHours >= 0 ? options.staleHours : 24
|
|
1211
|
+
const now = typeof options.now === 'number' ? options.now : Date.now()
|
|
1212
|
+
// #1580: gate freshness on the SAME overlay set evaluate() uses. A report check the engine would
|
|
1213
|
+
// score NA (its applies_if overlay is UNMET — capability off / cross-language audit) is not a
|
|
1214
|
+
// freshness concern: counting its absent report toward STALE/PARTIAL conflates "the tool never ran"
|
|
1215
|
+
// with "the capability does not apply" — the exact distinction freshness exists to make. Without a
|
|
1216
|
+
// gate, --check-fresh (a fail-closed exit-1 gate) hard-fails demanding reports the engine never scores.
|
|
1217
|
+
const overlaySet =
|
|
1218
|
+
options.overlays instanceof Set ? options.overlays : new Set(options.overlays || [])
|
|
1219
|
+
const checks = Array.isArray(registry?.checks) ? registry.checks : []
|
|
1220
|
+
const reports = []
|
|
1221
|
+
for (const c of checks) {
|
|
1222
|
+
if (!c || typeof c !== 'object') continue
|
|
1223
|
+
if (c.type !== 'value') continue
|
|
1224
|
+
const args = c.args || {}
|
|
1225
|
+
// Only a value check with a report `format` reads a pre-generated tool report; a legacy value
|
|
1226
|
+
// check (args.equals, no format) reads a tracked source file and is not a freshness concern.
|
|
1227
|
+
if (!args.format) continue
|
|
1228
|
+
// Skip a report check whose applies_if overlay is UNMET — the engine NA's it, so its report is
|
|
1229
|
+
// neither expected nor counted (isApplicable's fail-safe: a malformed precondition ⇒ APPLIES).
|
|
1230
|
+
if (!isApplicable(c, overlaySet, root)) continue
|
|
1231
|
+
const p = typeof args.path === 'string' ? args.path : null
|
|
1232
|
+
if (p === null) continue
|
|
1233
|
+
const abs = safeResolve(root, p)
|
|
1234
|
+
let present = false
|
|
1235
|
+
let ageHours = null
|
|
1236
|
+
if (abs !== null) {
|
|
1237
|
+
try {
|
|
1238
|
+
const st = statSync(abs)
|
|
1239
|
+
if (st.isFile()) {
|
|
1240
|
+
present = true
|
|
1241
|
+
ageHours = Math.max(0, (now - st.mtimeMs) / (3600 * 1000))
|
|
1242
|
+
}
|
|
1243
|
+
} catch {
|
|
1244
|
+
present = false
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const fresh = present && ageHours !== null && ageHours <= staleHours
|
|
1248
|
+
reports.push({ path: p, present, ageHours, fresh })
|
|
1249
|
+
}
|
|
1250
|
+
// Stable banner order (code-unit), deterministic across runs.
|
|
1251
|
+
reports.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
1252
|
+
const total = reports.length
|
|
1253
|
+
const present = reports.filter((r) => r.present).length
|
|
1254
|
+
const fresh = reports.filter((r) => r.fresh).length
|
|
1255
|
+
let status
|
|
1256
|
+
if (total === 0 || fresh === total) status = 'FRESH'
|
|
1257
|
+
else if (present === 0) status = 'STALE'
|
|
1258
|
+
else status = 'PARTIAL'
|
|
1259
|
+
return { status, staleHours, counts: { total, present, fresh }, reports }
|
|
1260
|
+
}
|