@codyswann/lisa 4.49.0 → 4.50.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/all/copy-overwrite/scripts/check-conflict-markers.mjs +4 -1
- package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +163 -0
- package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +178 -1
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +89 -10
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +44 -0
- package/all/copy-overwrite/scripts/lisa-lint-staged-preflight.mjs +1 -1
- package/all/copy-overwrite/scripts/lisa-run-gates.mjs +24 -24
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +490 -81
- package/cdk/package-lisa/package.lisa.json +45 -15
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +76 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +120 -227
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +45 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +44 -0
- package/expo/package-lisa/package.lisa.json +83 -53
- package/harper-fabric/package-lisa/package.lisa.json +62 -32
- package/nestjs/package-lisa/package.lisa.json +57 -27
- package/npm-package/package-lisa/package.lisa.json +43 -7
- package/package.json +11 -7
- package/phaser/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
- package/phaser/package-lisa/package.lisa.json +62 -32
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +4 -1
- package/plugins/lisa/hooks/block-direct-issue-create.sh +89 -10
- package/plugins/lisa/hooks/discharge-work-item-gates.sh +7 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +44 -0
- package/plugins/lisa/hooks/threshold-ratchet-families.mjs +12 -0
- package/plugins/lisa/rules/eager/00-rule-index.md +79 -0
- package/plugins/lisa/rules/eager/base-rules.md +10 -0
- package/plugins/lisa/rules/eager/config-resolution.md +1 -88
- package/plugins/lisa/rules/reference/automation-runbook-contract.md +97 -0
- package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +56 -0
- package/plugins/lisa/rules/reference/blocker-containment.md +58 -0
- package/plugins/lisa/rules/reference/claim-archaeology.md +42 -0
- package/plugins/lisa/rules/reference/claim-evidence-mapping.md +83 -0
- package/plugins/lisa/rules/reference/claim-time-guards.md +37 -0
- package/plugins/lisa/rules/reference/config-resolution.md +97 -0
- package/plugins/lisa/rules/reference/control-reachability.md +44 -0
- package/plugins/lisa/rules/reference/convergent-review.md +31 -0
- package/plugins/lisa/rules/reference/credential-substrate-precedence.md +57 -0
- package/plugins/lisa/rules/reference/dependency-decision-records.md +37 -0
- package/plugins/lisa/rules/reference/dependency-internalization-kit.md +47 -0
- package/plugins/lisa/rules/reference/dependency-trust-classes.md +48 -0
- package/plugins/lisa/rules/reference/deployed-state-readback.md +52 -0
- package/plugins/lisa/rules/reference/derived-branch-plan.md +52 -0
- package/plugins/lisa/rules/reference/design-source-of-truth.md +43 -0
- package/plugins/lisa/rules/reference/design-value-binding.md +103 -0
- package/plugins/lisa/rules/reference/documentation-source-paths.md +18 -0
- package/plugins/lisa/rules/reference/leaf-only-lifecycle.md +75 -0
- package/plugins/lisa/rules/reference/observability-audit.md +20 -0
- package/plugins/lisa/rules/reference/prd-definition-of-ready.md +37 -0
- package/plugins/lisa/rules/reference/prd-lifecycle-rollup.md +36 -0
- package/plugins/lisa/rules/reference/pre-flight-autofill.md +88 -0
- package/plugins/lisa/rules/reference/promotion-contract.md +21 -0
- package/plugins/lisa/rules/reference/readiness-rubric.md +63 -0
- package/plugins/lisa/rules/reference/ready-role-filing.md +34 -0
- package/plugins/lisa/rules/reference/rejection-detection.md +69 -0
- package/plugins/lisa/rules/reference/repo-scope-split.md +48 -0
- package/plugins/lisa/rules/reference/reset-seed-coverage.md +55 -0
- package/plugins/lisa/rules/reference/usage-accounting.md +38 -0
- package/plugins/lisa/rules/reference/work-item-definition-of-ready.md +32 -0
- package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +158 -1
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
- package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +4 -0
- package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +4 -0
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +4 -0
- package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +4 -1
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +89 -10
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +44 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +158 -1
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
- package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +4 -0
- package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +4 -0
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +4 -0
- package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +4 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +89 -10
- package/plugins/lisa-copilot/hooks/discharge-work-item-gates.sh +7 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +44 -0
- package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +12 -0
- package/plugins/lisa-copilot/rules/eager/00-rule-index.md +79 -0
- package/plugins/lisa-copilot/rules/eager/base-rules.md +10 -0
- package/plugins/lisa-copilot/rules/eager/config-resolution.md +1 -88
- package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +97 -0
- package/plugins/lisa-copilot/rules/reference/bdd-e2e-coverage.md +56 -0
- package/plugins/lisa-copilot/rules/reference/blocker-containment.md +58 -0
- package/plugins/lisa-copilot/rules/reference/claim-archaeology.md +42 -0
- package/plugins/lisa-copilot/rules/reference/claim-evidence-mapping.md +83 -0
- package/plugins/lisa-copilot/rules/reference/claim-time-guards.md +37 -0
- package/plugins/lisa-copilot/rules/reference/config-resolution.md +97 -0
- package/plugins/lisa-copilot/rules/reference/control-reachability.md +44 -0
- package/plugins/lisa-copilot/rules/reference/convergent-review.md +31 -0
- package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +57 -0
- package/plugins/lisa-copilot/rules/reference/dependency-decision-records.md +37 -0
- package/plugins/lisa-copilot/rules/reference/dependency-internalization-kit.md +47 -0
- package/plugins/lisa-copilot/rules/reference/dependency-trust-classes.md +48 -0
- package/plugins/lisa-copilot/rules/reference/deployed-state-readback.md +52 -0
- package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +52 -0
- package/plugins/lisa-copilot/rules/reference/design-source-of-truth.md +43 -0
- package/plugins/lisa-copilot/rules/reference/design-value-binding.md +103 -0
- package/plugins/lisa-copilot/rules/reference/documentation-source-paths.md +18 -0
- package/plugins/lisa-copilot/rules/reference/leaf-only-lifecycle.md +75 -0
- package/plugins/lisa-copilot/rules/reference/observability-audit.md +20 -0
- package/plugins/lisa-copilot/rules/reference/prd-definition-of-ready.md +37 -0
- package/plugins/lisa-copilot/rules/reference/prd-lifecycle-rollup.md +36 -0
- package/plugins/lisa-copilot/rules/reference/pre-flight-autofill.md +88 -0
- package/plugins/lisa-copilot/rules/reference/promotion-contract.md +21 -0
- package/plugins/lisa-copilot/rules/reference/readiness-rubric.md +63 -0
- package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +34 -0
- package/plugins/lisa-copilot/rules/reference/rejection-detection.md +69 -0
- package/plugins/lisa-copilot/rules/reference/repo-scope-split.md +48 -0
- package/plugins/lisa-copilot/rules/reference/reset-seed-coverage.md +55 -0
- package/plugins/lisa-copilot/rules/reference/usage-accounting.md +38 -0
- package/plugins/lisa-copilot/rules/reference/work-item-definition-of-ready.md +32 -0
- package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +158 -1
- package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
- package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +4 -0
- package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +4 -0
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +4 -0
- package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +4 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +89 -10
- package/plugins/lisa-cursor/hooks/discharge-work-item-gates.sh +7 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +44 -0
- package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +12 -0
- package/plugins/lisa-cursor/rules/00-rule-index.mdc +84 -0
- package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +97 -0
- package/plugins/lisa-cursor/rules/base-rules.mdc +10 -0
- package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +56 -0
- package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +58 -0
- package/plugins/lisa-cursor/rules/claim-archaeology-reference.mdc +42 -0
- package/plugins/lisa-cursor/rules/claim-evidence-mapping-reference.mdc +83 -0
- package/plugins/lisa-cursor/rules/claim-time-guards-reference.mdc +37 -0
- package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +97 -0
- package/plugins/lisa-cursor/rules/config-resolution.mdc +1 -88
- package/plugins/lisa-cursor/rules/control-reachability-reference.mdc +44 -0
- package/plugins/lisa-cursor/rules/convergent-review-reference.mdc +31 -0
- package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +57 -0
- package/plugins/lisa-cursor/rules/dependency-decision-records-reference.mdc +37 -0
- package/plugins/lisa-cursor/rules/dependency-internalization-kit-reference.mdc +47 -0
- package/plugins/lisa-cursor/rules/dependency-trust-classes-reference.mdc +48 -0
- package/plugins/lisa-cursor/rules/deployed-state-readback-reference.mdc +52 -0
- package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +52 -0
- package/plugins/lisa-cursor/rules/design-source-of-truth-reference.mdc +43 -0
- package/plugins/lisa-cursor/rules/design-value-binding-reference.mdc +103 -0
- package/plugins/lisa-cursor/rules/documentation-source-paths-reference.mdc +18 -0
- package/plugins/lisa-cursor/rules/leaf-only-lifecycle-reference.mdc +75 -0
- package/plugins/lisa-cursor/rules/observability-audit-reference.mdc +20 -0
- package/plugins/lisa-cursor/rules/prd-definition-of-ready-reference.mdc +37 -0
- package/plugins/lisa-cursor/rules/prd-lifecycle-rollup-reference.mdc +36 -0
- package/plugins/lisa-cursor/rules/pre-flight-autofill-reference.mdc +88 -0
- package/plugins/lisa-cursor/rules/promotion-contract-reference.mdc +21 -0
- package/plugins/lisa-cursor/rules/readiness-rubric-reference.mdc +63 -0
- package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +34 -0
- package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +69 -0
- package/plugins/lisa-cursor/rules/repo-scope-split-reference.mdc +48 -0
- package/plugins/lisa-cursor/rules/reset-seed-coverage-reference.mdc +55 -0
- package/plugins/lisa-cursor/rules/usage-accounting-reference.mdc +38 -0
- package/plugins/lisa-cursor/rules/work-item-definition-of-ready-reference.mdc +32 -0
- package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +158 -1
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
- package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +4 -0
- package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +4 -0
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +4 -0
- package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +4 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-direct-issue-create.sh +89 -10
- package/plugins/src/base/hooks/discharge-work-item-gates.sh +7 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +44 -0
- package/plugins/src/base/hooks/threshold-ratchet-families.mjs +12 -0
- package/plugins/src/base/rules/eager/00-rule-index.md +79 -0
- package/plugins/src/base/rules/eager/base-rules.md +10 -0
- package/plugins/src/base/rules/eager/config-resolution.md +1 -88
- package/plugins/src/base/rules/reference/automation-runbook-contract.md +97 -0
- package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +56 -0
- package/plugins/src/base/rules/reference/blocker-containment.md +58 -0
- package/plugins/src/base/rules/reference/claim-archaeology.md +42 -0
- package/plugins/src/base/rules/reference/claim-evidence-mapping.md +83 -0
- package/plugins/src/base/rules/reference/claim-time-guards.md +37 -0
- package/plugins/src/base/rules/reference/config-resolution.md +97 -0
- package/plugins/src/base/rules/reference/control-reachability.md +44 -0
- package/plugins/src/base/rules/reference/convergent-review.md +31 -0
- package/plugins/src/base/rules/reference/credential-substrate-precedence.md +57 -0
- package/plugins/src/base/rules/reference/dependency-decision-records.md +37 -0
- package/plugins/src/base/rules/reference/dependency-internalization-kit.md +47 -0
- package/plugins/src/base/rules/reference/dependency-trust-classes.md +48 -0
- package/plugins/src/base/rules/reference/deployed-state-readback.md +52 -0
- package/plugins/src/base/rules/reference/derived-branch-plan.md +52 -0
- package/plugins/src/base/rules/reference/design-source-of-truth.md +43 -0
- package/plugins/src/base/rules/reference/design-value-binding.md +103 -0
- package/plugins/src/base/rules/reference/documentation-source-paths.md +18 -0
- package/plugins/src/base/rules/reference/leaf-only-lifecycle.md +75 -0
- package/plugins/src/base/rules/reference/observability-audit.md +20 -0
- package/plugins/src/base/rules/reference/prd-definition-of-ready.md +37 -0
- package/plugins/src/base/rules/reference/prd-lifecycle-rollup.md +36 -0
- package/plugins/src/base/rules/reference/pre-flight-autofill.md +88 -0
- package/plugins/src/base/rules/reference/promotion-contract.md +21 -0
- package/plugins/src/base/rules/reference/readiness-rubric.md +63 -0
- package/plugins/src/base/rules/reference/ready-role-filing.md +34 -0
- package/plugins/src/base/rules/reference/rejection-detection.md +69 -0
- package/plugins/src/base/rules/reference/repo-scope-split.md +48 -0
- package/plugins/src/base/rules/reference/reset-seed-coverage.md +55 -0
- package/plugins/src/base/rules/reference/usage-accounting.md +38 -0
- package/plugins/src/base/rules/reference/work-item-definition-of-ready.md +32 -0
- package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +158 -1
- package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
- package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +4 -0
- package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +4 -0
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +4 -0
- package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +4 -1
- package/rails/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
- package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +12 -0
- package/scripts/check-eager-rule-budget.mjs +176 -0
- package/scripts/check-generated-artifact-merge-coverage.mjs +7 -0
- package/scripts/check-rules-pairing.sh +45 -14
- package/scripts/check-shipped-surface-removals.mjs +851 -0
- package/scripts/generate-export-surface.mjs +318 -0
- package/scripts/lib/shipped-surface.mjs +212 -0
- package/scripts/lisa-enforcement-fallback.sh +178 -1
- package/scripts/remedy-conformance.mjs +435 -0
- package/scripts/two-channel-couplings.json +2 -2
- package/typescript/copy-contents/.husky/pre-push +147 -2
- package/typescript/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
- package/typescript/copy-overwrite/audit.ignore.config.json +0 -10
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +180 -29
- package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +12 -0
- package/typescript/package-lisa/package.lisa.json +72 -42
- package/plugins/lisa/rules/eager/automation-runbook-contract.md +0 -92
- package/plugins/lisa/rules/eager/bdd-e2e-coverage.md +0 -51
- package/plugins/lisa/rules/eager/blocker-containment.md +0 -53
- package/plugins/lisa/rules/eager/claim-archaeology.md +0 -37
- package/plugins/lisa/rules/eager/claim-evidence-mapping.md +0 -78
- package/plugins/lisa/rules/eager/claim-time-guards.md +0 -32
- package/plugins/lisa/rules/eager/control-reachability.md +0 -39
- package/plugins/lisa/rules/eager/convergent-review.md +0 -24
- package/plugins/lisa/rules/eager/credential-substrate-precedence.md +0 -54
- package/plugins/lisa/rules/eager/dependency-decision-records.md +0 -32
- package/plugins/lisa/rules/eager/dependency-internalization-kit.md +0 -42
- package/plugins/lisa/rules/eager/dependency-trust-classes.md +0 -43
- package/plugins/lisa/rules/eager/deployed-state-readback.md +0 -47
- package/plugins/lisa/rules/eager/derived-branch-plan.md +0 -47
- package/plugins/lisa/rules/eager/design-source-of-truth.md +0 -38
- package/plugins/lisa/rules/eager/design-value-binding.md +0 -98
- package/plugins/lisa/rules/eager/documentation-source-paths.md +0 -13
- package/plugins/lisa/rules/eager/factory-model.md +0 -41
- package/plugins/lisa/rules/eager/leaf-only-lifecycle.md +0 -70
- package/plugins/lisa/rules/eager/observability-audit.md +0 -15
- package/plugins/lisa/rules/eager/prd-definition-of-ready.md +0 -32
- package/plugins/lisa/rules/eager/prd-lifecycle-rollup.md +0 -31
- package/plugins/lisa/rules/eager/pre-flight-autofill.md +0 -84
- package/plugins/lisa/rules/eager/project-learnings.md +0 -22
- package/plugins/lisa/rules/eager/promotion-contract.md +0 -18
- package/plugins/lisa/rules/eager/readiness-rubric.md +0 -58
- package/plugins/lisa/rules/eager/ready-role-filing.md +0 -29
- package/plugins/lisa/rules/eager/rejection-detection.md +0 -64
- package/plugins/lisa/rules/eager/repo-scope-split.md +0 -43
- package/plugins/lisa/rules/eager/reset-seed-coverage.md +0 -50
- package/plugins/lisa/rules/eager/usage-accounting.md +0 -33
- package/plugins/lisa/rules/eager/work-item-definition-of-ready.md +0 -27
- package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +0 -92
- package/plugins/lisa-copilot/rules/eager/bdd-e2e-coverage.md +0 -51
- package/plugins/lisa-copilot/rules/eager/blocker-containment.md +0 -53
- package/plugins/lisa-copilot/rules/eager/claim-archaeology.md +0 -37
- package/plugins/lisa-copilot/rules/eager/claim-evidence-mapping.md +0 -78
- package/plugins/lisa-copilot/rules/eager/claim-time-guards.md +0 -32
- package/plugins/lisa-copilot/rules/eager/control-reachability.md +0 -39
- package/plugins/lisa-copilot/rules/eager/convergent-review.md +0 -24
- package/plugins/lisa-copilot/rules/eager/credential-substrate-precedence.md +0 -54
- package/plugins/lisa-copilot/rules/eager/dependency-decision-records.md +0 -32
- package/plugins/lisa-copilot/rules/eager/dependency-internalization-kit.md +0 -42
- package/plugins/lisa-copilot/rules/eager/dependency-trust-classes.md +0 -43
- package/plugins/lisa-copilot/rules/eager/deployed-state-readback.md +0 -47
- package/plugins/lisa-copilot/rules/eager/derived-branch-plan.md +0 -47
- package/plugins/lisa-copilot/rules/eager/design-source-of-truth.md +0 -38
- package/plugins/lisa-copilot/rules/eager/design-value-binding.md +0 -98
- package/plugins/lisa-copilot/rules/eager/documentation-source-paths.md +0 -13
- package/plugins/lisa-copilot/rules/eager/factory-model.md +0 -41
- package/plugins/lisa-copilot/rules/eager/leaf-only-lifecycle.md +0 -70
- package/plugins/lisa-copilot/rules/eager/observability-audit.md +0 -15
- package/plugins/lisa-copilot/rules/eager/prd-definition-of-ready.md +0 -32
- package/plugins/lisa-copilot/rules/eager/prd-lifecycle-rollup.md +0 -31
- package/plugins/lisa-copilot/rules/eager/pre-flight-autofill.md +0 -84
- package/plugins/lisa-copilot/rules/eager/project-learnings.md +0 -22
- package/plugins/lisa-copilot/rules/eager/promotion-contract.md +0 -18
- package/plugins/lisa-copilot/rules/eager/readiness-rubric.md +0 -58
- package/plugins/lisa-copilot/rules/eager/ready-role-filing.md +0 -29
- package/plugins/lisa-copilot/rules/eager/rejection-detection.md +0 -64
- package/plugins/lisa-copilot/rules/eager/repo-scope-split.md +0 -43
- package/plugins/lisa-copilot/rules/eager/reset-seed-coverage.md +0 -50
- package/plugins/lisa-copilot/rules/eager/usage-accounting.md +0 -33
- package/plugins/lisa-copilot/rules/eager/work-item-definition-of-ready.md +0 -27
- package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +0 -97
- package/plugins/lisa-cursor/rules/bdd-e2e-coverage.mdc +0 -56
- package/plugins/lisa-cursor/rules/blocker-containment.mdc +0 -58
- package/plugins/lisa-cursor/rules/claim-archaeology.mdc +0 -42
- package/plugins/lisa-cursor/rules/claim-evidence-mapping.mdc +0 -83
- package/plugins/lisa-cursor/rules/claim-time-guards.mdc +0 -37
- package/plugins/lisa-cursor/rules/control-reachability.mdc +0 -44
- package/plugins/lisa-cursor/rules/convergent-review.mdc +0 -29
- package/plugins/lisa-cursor/rules/credential-substrate-precedence.mdc +0 -59
- package/plugins/lisa-cursor/rules/dependency-decision-records.mdc +0 -37
- package/plugins/lisa-cursor/rules/dependency-internalization-kit.mdc +0 -47
- package/plugins/lisa-cursor/rules/dependency-trust-classes.mdc +0 -48
- package/plugins/lisa-cursor/rules/deployed-state-readback.mdc +0 -52
- package/plugins/lisa-cursor/rules/derived-branch-plan.mdc +0 -52
- package/plugins/lisa-cursor/rules/design-source-of-truth.mdc +0 -43
- package/plugins/lisa-cursor/rules/design-value-binding.mdc +0 -103
- package/plugins/lisa-cursor/rules/documentation-source-paths.mdc +0 -18
- package/plugins/lisa-cursor/rules/factory-model.mdc +0 -46
- package/plugins/lisa-cursor/rules/leaf-only-lifecycle.mdc +0 -75
- package/plugins/lisa-cursor/rules/observability-audit.mdc +0 -20
- package/plugins/lisa-cursor/rules/prd-definition-of-ready.mdc +0 -37
- package/plugins/lisa-cursor/rules/prd-lifecycle-rollup.mdc +0 -36
- package/plugins/lisa-cursor/rules/pre-flight-autofill.mdc +0 -89
- package/plugins/lisa-cursor/rules/project-learnings.mdc +0 -27
- package/plugins/lisa-cursor/rules/promotion-contract.mdc +0 -23
- package/plugins/lisa-cursor/rules/readiness-rubric.mdc +0 -63
- package/plugins/lisa-cursor/rules/ready-role-filing.mdc +0 -34
- package/plugins/lisa-cursor/rules/rejection-detection.mdc +0 -69
- package/plugins/lisa-cursor/rules/repo-scope-split.mdc +0 -48
- package/plugins/lisa-cursor/rules/reset-seed-coverage.mdc +0 -55
- package/plugins/lisa-cursor/rules/usage-accounting.mdc +0 -38
- package/plugins/lisa-cursor/rules/work-item-definition-of-ready.mdc +0 -32
- package/plugins/src/base/rules/eager/automation-runbook-contract.md +0 -92
- package/plugins/src/base/rules/eager/bdd-e2e-coverage.md +0 -51
- package/plugins/src/base/rules/eager/blocker-containment.md +0 -53
- package/plugins/src/base/rules/eager/claim-archaeology.md +0 -37
- package/plugins/src/base/rules/eager/claim-evidence-mapping.md +0 -78
- package/plugins/src/base/rules/eager/claim-time-guards.md +0 -32
- package/plugins/src/base/rules/eager/control-reachability.md +0 -39
- package/plugins/src/base/rules/eager/convergent-review.md +0 -24
- package/plugins/src/base/rules/eager/credential-substrate-precedence.md +0 -54
- package/plugins/src/base/rules/eager/dependency-decision-records.md +0 -32
- package/plugins/src/base/rules/eager/dependency-internalization-kit.md +0 -42
- package/plugins/src/base/rules/eager/dependency-trust-classes.md +0 -43
- package/plugins/src/base/rules/eager/deployed-state-readback.md +0 -47
- package/plugins/src/base/rules/eager/derived-branch-plan.md +0 -47
- package/plugins/src/base/rules/eager/design-source-of-truth.md +0 -38
- package/plugins/src/base/rules/eager/design-value-binding.md +0 -98
- package/plugins/src/base/rules/eager/documentation-source-paths.md +0 -13
- package/plugins/src/base/rules/eager/factory-model.md +0 -41
- package/plugins/src/base/rules/eager/leaf-only-lifecycle.md +0 -70
- package/plugins/src/base/rules/eager/observability-audit.md +0 -15
- package/plugins/src/base/rules/eager/prd-definition-of-ready.md +0 -32
- package/plugins/src/base/rules/eager/prd-lifecycle-rollup.md +0 -31
- package/plugins/src/base/rules/eager/pre-flight-autofill.md +0 -84
- package/plugins/src/base/rules/eager/project-learnings.md +0 -22
- package/plugins/src/base/rules/eager/promotion-contract.md +0 -18
- package/plugins/src/base/rules/eager/readiness-rubric.md +0 -58
- package/plugins/src/base/rules/eager/ready-role-filing.md +0 -29
- package/plugins/src/base/rules/eager/rejection-detection.md +0 -64
- package/plugins/src/base/rules/eager/repo-scope-split.md +0 -43
- package/plugins/src/base/rules/eager/reset-seed-coverage.md +0 -50
- package/plugins/src/base/rules/eager/usage-accounting.md +0 -33
- package/plugins/src/base/rules/eager/work-item-definition-of-ready.md +0 -27
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic gate that nothing disappears from a SHIPPED surface without a
|
|
4
|
+
* record a consumer can act on (CodySwannGT/lisa#3849).
|
|
5
|
+
*
|
|
6
|
+
* ## Why removal is structurally harder than addition here
|
|
7
|
+
*
|
|
8
|
+
* `deepMergeWithArrayUnion` unions arrays and the copy lanes overwrite; none of
|
|
9
|
+
* them delete. A template edit can therefore ADD to a consumer's tree, and can
|
|
10
|
+
* never remove from it. The only mechanisms that reach an installed base are a
|
|
11
|
+
* `deletions.json` entry and a migration under `src/migrations/`. So a removal
|
|
12
|
+
* made upstream with neither is not a partial fix - downstream it does nothing
|
|
13
|
+
* at all, silently, while looking complete in the diff.
|
|
14
|
+
*
|
|
15
|
+
* That asymmetry has two failure shapes and this gate covers both.
|
|
16
|
+
*
|
|
17
|
+
* ### 1. A shipped FILE vanishes upstream
|
|
18
|
+
*
|
|
19
|
+
* Every host that ever applied a release carrying it keeps it, at its
|
|
20
|
+
* destination path, forever. No later upgrade removes it.
|
|
21
|
+
*
|
|
22
|
+
* ### 2. A shipped script's PUBLIC SURFACE shrinks
|
|
23
|
+
*
|
|
24
|
+
* This is the one that was actually reported, and the ticket's first diagnosis
|
|
25
|
+
* of it was wrong in a way worth recording. `SNAPSHOT_MAX_AGE_DAYS` was dropped
|
|
26
|
+
* from `typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs`
|
|
27
|
+
* (commit `f13bb00`, released in v4.31.0) and a consumer's own
|
|
28
|
+
* `scripts/check-required-checks-baseline.mjs` imported it. That importer was
|
|
29
|
+
* never a Lisa file - `git rev-list --all --objects` finds no such path in this
|
|
30
|
+
* repository's entire history - so a shipped-to-shipped import scan would not
|
|
31
|
+
* have caught it, and a deletions entry would have made it WORSE by propagating
|
|
32
|
+
* a removal into a host that had wired the thing to three npm scripts, a
|
|
33
|
+
* workflow, and a test.
|
|
34
|
+
*
|
|
35
|
+
* A missing NAMED export is not a syntax error in either file. It fails only
|
|
36
|
+
* when the module graph is linked, so it passes anything that lints or
|
|
37
|
+
* type-checks the files one at a time and then surfaces in whichever job runs
|
|
38
|
+
* the importer first - in the reported case `lint`, which points nowhere near
|
|
39
|
+
* the export.
|
|
40
|
+
*
|
|
41
|
+
* ## The two dispositions, and why one manifest could not carry both
|
|
42
|
+
*
|
|
43
|
+
* Before declaring any shipped path for deletion the question is not "does
|
|
44
|
+
* upstream still need it" but "could a consumer have wired this into their own
|
|
45
|
+
* scripts?" - and for an executable under `scripts/` the answer is usually yes.
|
|
46
|
+
* Those two answers need opposite handling, and `deletions.json` makes no such
|
|
47
|
+
* distinction:
|
|
48
|
+
*
|
|
49
|
+
* - PROPAGATE - a `deletions.json` entry, for a path no host could have bound
|
|
50
|
+
* to. Nothing further is required; the manifest IS the record.
|
|
51
|
+
* - RETAIN AND NOTIFY - an entry in the removal ledger, for a consumer-
|
|
52
|
+
* bindable executable and for every removed export. Deletion would break a
|
|
53
|
+
* working host capability, so the removal is recorded with a consumer-
|
|
54
|
+
* facing note instead.
|
|
55
|
+
*
|
|
56
|
+
* A consumer-bindable executable may still be propagated, but only with a
|
|
57
|
+
* `force` reason in its `deletions.json` - the same override that already
|
|
58
|
+
* exists for overriding workflow ownership, reused rather than reinvented.
|
|
59
|
+
*
|
|
60
|
+
* ## Both arms, or it is not a gate
|
|
61
|
+
*
|
|
62
|
+
* A removal WITH a correct record passes, an ordinary addition passes, and a
|
|
63
|
+
* healthy import pair passes. A check that flagged every deletion would be
|
|
64
|
+
* disabled inside a week, and this repository has a live precedent for exactly
|
|
65
|
+
* that failure of proportion.
|
|
66
|
+
*
|
|
67
|
+
* ## Baseline
|
|
68
|
+
*
|
|
69
|
+
* The window is `baseline..HEAD`, where `baseline` is a tag pinned in the
|
|
70
|
+
* ledger (the first tag of the supported major line). Advancing it retires
|
|
71
|
+
* obligations, so it is deliberately a one-line, reviewable edit in the ledger
|
|
72
|
+
* diff rather than something derived from the clock. The gate verifies the tag
|
|
73
|
+
* exists and is an ancestor of HEAD; a baseline it cannot resolve is exit 2,
|
|
74
|
+
* never a quiet pass.
|
|
75
|
+
*
|
|
76
|
+
* ## Determinism guarantees
|
|
77
|
+
*
|
|
78
|
+
* - zero third-party dependencies (Node built-ins only),
|
|
79
|
+
* - no network access,
|
|
80
|
+
* - no `Date` / `Math.random`,
|
|
81
|
+
* - the file lists come from `git ls-tree`, so the gate compares what a
|
|
82
|
+
* release actually carried rather than whatever is loose in a worktree.
|
|
83
|
+
*
|
|
84
|
+
* Discovering zero shipped files is exit 2, not a clean pass. A gate that
|
|
85
|
+
* passes because it could not look is the failure mode this file exists to
|
|
86
|
+
* prevent.
|
|
87
|
+
*
|
|
88
|
+
* CLI:
|
|
89
|
+
* node scripts/check-shipped-surface-removals.mjs [--root <dir>] [--since <ref>] [--json]
|
|
90
|
+
*
|
|
91
|
+
* Exit codes:
|
|
92
|
+
* 0 - every removal from a shipped surface is governed.
|
|
93
|
+
* 1 - at least one ungoverned removal, mis-propagated executable, unresolved
|
|
94
|
+
* shipped-to-shipped named import, or self-contradicting ledger entry.
|
|
95
|
+
* 2 - operational error: unknown flag, `--root` absent or not a git
|
|
96
|
+
* repository, git unavailable, an unreadable ledger or deletions
|
|
97
|
+
* manifest, an unresolvable baseline, or zero shipped files discovered.
|
|
98
|
+
*
|
|
99
|
+
* @module scripts/check-shipped-surface-removals
|
|
100
|
+
*/
|
|
101
|
+
import fs from "node:fs";
|
|
102
|
+
import path from "node:path";
|
|
103
|
+
import process from "node:process";
|
|
104
|
+
import { fileURLToPath } from "node:url";
|
|
105
|
+
|
|
106
|
+
import {
|
|
107
|
+
DELIVERY_LANES,
|
|
108
|
+
UsageError,
|
|
109
|
+
ancestryChain,
|
|
110
|
+
destinationPath,
|
|
111
|
+
effectiveDeletions,
|
|
112
|
+
matchDeletion,
|
|
113
|
+
} from "./check-delivery-deletion-conflicts.mjs";
|
|
114
|
+
import { boundedExecFileSync } from "./lib/bounded-spawn.mjs";
|
|
115
|
+
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
116
|
+
import {
|
|
117
|
+
hasUsableNote,
|
|
118
|
+
indexRemovals,
|
|
119
|
+
isConsumerBindable,
|
|
120
|
+
parseNamedExports,
|
|
121
|
+
parseRelativeNamedImports,
|
|
122
|
+
removalKey,
|
|
123
|
+
resolveRelative,
|
|
124
|
+
} from "./lib/shipped-surface.mjs";
|
|
125
|
+
|
|
126
|
+
const REPO_ROOT = path.resolve(
|
|
127
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
128
|
+
".."
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
/** The removal ledger, relative to the repository root. */
|
|
132
|
+
export const LEDGER_PATH = "shipped-removals.json";
|
|
133
|
+
|
|
134
|
+
/** Extensions whose export surface this gate reads. */
|
|
135
|
+
const MODULE_EXTENSIONS = Object.freeze([".cjs", ".js", ".mjs"]);
|
|
136
|
+
|
|
137
|
+
/** Max bytes of git output (the full tree listing is ~0.3 MB today). */
|
|
138
|
+
const MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run one git command under the shared timeout, turning any failure into a
|
|
142
|
+
* `UsageError` so an unverifiable state exits 2 rather than scanning nothing.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} root - repository root.
|
|
145
|
+
* @param {readonly string[]} args - git arguments after `-C root`.
|
|
146
|
+
* @param {string} what - short description, for the error message.
|
|
147
|
+
* @returns {string} stdout.
|
|
148
|
+
*/
|
|
149
|
+
function git(root, args, what) {
|
|
150
|
+
try {
|
|
151
|
+
return boundedExecFileSync("git", ["-C", root, ...args], {
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
154
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new UsageError(`${what}: ${error.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Split a repo-relative path into its stack, lane, and lane-relative parts,
|
|
163
|
+
* or null when it is not inside a delivery lane.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} file - repo-relative path.
|
|
166
|
+
* @returns {{ stack: string, lane: string, relative: string } | null} the parts.
|
|
167
|
+
*/
|
|
168
|
+
export function shippedParts(file) {
|
|
169
|
+
const [stack, lane, ...rest] = file.split("/");
|
|
170
|
+
if (rest.length === 0 || !DELIVERY_LANES.includes(lane)) return null;
|
|
171
|
+
return { lane, relative: rest.join("/"), stack };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Every tracked file inside a delivery lane at `ref`, keyed by repo path.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} root - repository root.
|
|
178
|
+
* @param {string} ref - a git ref.
|
|
179
|
+
* @returns {Map<string, { stack: string, lane: string, destination: string }>} shipped files.
|
|
180
|
+
*/
|
|
181
|
+
export function shippedFilesAt(root, ref) {
|
|
182
|
+
const stdout = git(
|
|
183
|
+
root,
|
|
184
|
+
["ls-tree", "-r", "-z", "--name-only", ref],
|
|
185
|
+
`could not list the tree at ${ref}`
|
|
186
|
+
);
|
|
187
|
+
const shipped = new Map();
|
|
188
|
+
for (const file of stdout.split("\0")) {
|
|
189
|
+
const parts = file === "" ? null : shippedParts(file);
|
|
190
|
+
if (parts === null) continue;
|
|
191
|
+
shipped.set(file, {
|
|
192
|
+
destination: destinationPath(parts.lane, parts.relative),
|
|
193
|
+
lane: parts.lane,
|
|
194
|
+
stack: parts.stack,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return shipped;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Effective deletion set and `force` reasons for every stack at HEAD.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} root - repository root.
|
|
204
|
+
* @returns {Map<string, { deleted: Set<string>, force: Map<string, string> }>} per stack.
|
|
205
|
+
*/
|
|
206
|
+
export function readDeletionManifests(root) {
|
|
207
|
+
const manifests = new Map();
|
|
208
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
209
|
+
if (!entry.isDirectory()) continue;
|
|
210
|
+
const label = `${entry.name}/deletions.json`;
|
|
211
|
+
const absolute = path.join(root, label);
|
|
212
|
+
if (!fs.existsSync(absolute)) continue;
|
|
213
|
+
const parsed = parseJson(absolute, label);
|
|
214
|
+
const force = parsed.force ?? {};
|
|
215
|
+
if (force === null || typeof force !== "object" || Array.isArray(force)) {
|
|
216
|
+
throw new UsageError(`${label}: "force" must be an object`);
|
|
217
|
+
}
|
|
218
|
+
manifests.set(entry.name, {
|
|
219
|
+
deleted: effectiveDeletions(parsed, label),
|
|
220
|
+
force: new Map(Object.entries(force)),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return manifests;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Read and parse a JSON file, turning any failure into a `UsageError`.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} absolute - absolute path.
|
|
230
|
+
* @param {string} label - repo-relative path, for error messages.
|
|
231
|
+
* @returns {Record<string, unknown>} the parsed object.
|
|
232
|
+
*/
|
|
233
|
+
function parseJson(absolute, label) {
|
|
234
|
+
let raw;
|
|
235
|
+
try {
|
|
236
|
+
raw = fs.readFileSync(absolute, "utf8");
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw new UsageError(`${label}: could not read - ${error.message}`);
|
|
239
|
+
}
|
|
240
|
+
let parsed;
|
|
241
|
+
try {
|
|
242
|
+
parsed = JSON.parse(raw);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
throw new UsageError(`${label}: could not parse - ${error.message}`);
|
|
245
|
+
}
|
|
246
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
247
|
+
throw new UsageError(`${label}: expected a JSON object`);
|
|
248
|
+
}
|
|
249
|
+
return parsed;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Load and validate the removal ledger.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} root - repository root.
|
|
256
|
+
* @returns {{ baseline: string, removals: Array<Record<string, unknown>> }} the ledger.
|
|
257
|
+
*/
|
|
258
|
+
export function loadLedger(root) {
|
|
259
|
+
const parsed = parseJson(path.join(root, LEDGER_PATH), LEDGER_PATH);
|
|
260
|
+
const { baseline, removals } = parsed;
|
|
261
|
+
if (typeof baseline !== "string" || baseline.trim() === "") {
|
|
262
|
+
throw new UsageError(`${LEDGER_PATH}: "baseline" must be a non-empty ref`);
|
|
263
|
+
}
|
|
264
|
+
if (!Array.isArray(removals)) {
|
|
265
|
+
throw new UsageError(`${LEDGER_PATH}: "removals" must be an array`);
|
|
266
|
+
}
|
|
267
|
+
for (const entry of removals) {
|
|
268
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
269
|
+
throw new UsageError(`${LEDGER_PATH}: every removal must be an object`);
|
|
270
|
+
}
|
|
271
|
+
if (typeof entry.path !== "string" || entry.path === "") {
|
|
272
|
+
throw new UsageError(`${LEDGER_PATH}: every removal needs a "path"`);
|
|
273
|
+
}
|
|
274
|
+
if (entry.export !== undefined && typeof entry.export !== "string") {
|
|
275
|
+
throw new UsageError(
|
|
276
|
+
`${LEDGER_PATH}: "export" on ${entry.path} must be a string`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { baseline, removals };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Resolve the baseline ref, refusing anything this gate cannot compare against.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} root - repository root.
|
|
287
|
+
* @param {string} ref - the requested baseline.
|
|
288
|
+
* @returns {string} the resolved commit sha.
|
|
289
|
+
*/
|
|
290
|
+
export function resolveBaseline(root, ref) {
|
|
291
|
+
const sha = git(
|
|
292
|
+
root,
|
|
293
|
+
["rev-parse", "--verify", `${ref}^{commit}`],
|
|
294
|
+
`baseline ${ref} does not resolve to a commit`
|
|
295
|
+
).trim();
|
|
296
|
+
try {
|
|
297
|
+
boundedExecFileSync(
|
|
298
|
+
"git",
|
|
299
|
+
["-C", root, "merge-base", "--is-ancestor", sha, "HEAD"],
|
|
300
|
+
{ stdio: "ignore" }
|
|
301
|
+
);
|
|
302
|
+
} catch {
|
|
303
|
+
throw new UsageError(
|
|
304
|
+
`baseline ${ref} is not an ancestor of HEAD, so the window is not a ` +
|
|
305
|
+
`release history this gate can read`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return sha;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The release a path's removal shipped in, so the report can name the reach of
|
|
313
|
+
* the consequence rather than only the file.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} root - repository root.
|
|
316
|
+
* @param {string} baseline - the baseline sha.
|
|
317
|
+
* @param {string} file - the removed repo path.
|
|
318
|
+
* @param {string} [symbol] - the removed export; when given, the commit is
|
|
319
|
+
* found by pickaxe on that symbol rather than by last touch of the file, so
|
|
320
|
+
* an unrelated later edit does not get the blame.
|
|
321
|
+
* @returns {string} a tag name, or a description of why there is none.
|
|
322
|
+
*/
|
|
323
|
+
export function removalRelease(root, baseline, file, symbol) {
|
|
324
|
+
const pickaxe = symbol === undefined ? [] : [`-S${symbol}`];
|
|
325
|
+
const sha = git(
|
|
326
|
+
root,
|
|
327
|
+
["log", "-1", "--format=%H", ...pickaxe, `${baseline}..HEAD`, "--", file],
|
|
328
|
+
`could not date the removal of ${file}`
|
|
329
|
+
).trim();
|
|
330
|
+
if (sha === "") return "an unidentified commit";
|
|
331
|
+
try {
|
|
332
|
+
const described = boundedExecFileSync(
|
|
333
|
+
"git",
|
|
334
|
+
["-C", root, "describe", "--contains", "--match", "v*", sha],
|
|
335
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
336
|
+
);
|
|
337
|
+
return described.trim().split("~")[0].split("^")[0];
|
|
338
|
+
} catch {
|
|
339
|
+
return `${sha.slice(0, 9)} (not yet released)`;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Whether some stack active alongside `stack` deletes `destination` for every
|
|
345
|
+
* consumer of it, and whether that deletion carries a `force` reason.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} stack - the stack whose lane shipped the path.
|
|
348
|
+
* @param {string} destination - the path in the consumer's tree.
|
|
349
|
+
* @param {Map<string, { deleted: Set<string>, force: Map<string, string> }>} manifests
|
|
350
|
+
* per-stack deletion manifests.
|
|
351
|
+
* @returns {{ by: string, forced: boolean } | null} the governing deletion.
|
|
352
|
+
*/
|
|
353
|
+
export function findGoverningDeletion(stack, destination, manifests) {
|
|
354
|
+
for (const candidate of ancestryChain(stack)) {
|
|
355
|
+
const manifest = manifests.get(candidate);
|
|
356
|
+
if (manifest === undefined) continue;
|
|
357
|
+
if (matchDeletion(destination, manifest.deleted) === null) continue;
|
|
358
|
+
return { by: candidate, forced: manifest.force.has(destination) };
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Findings for shipped FILES that existed at the baseline and are gone at HEAD.
|
|
365
|
+
*
|
|
366
|
+
* @param {{ root: string, baseline: string, before: Map<string, object>, after: Map<string, object>, manifests: Map<string, object>, ledger: Map<string, object> }} input
|
|
367
|
+
* the resolved scan inputs.
|
|
368
|
+
* @returns {Array<Record<string, unknown>>} one row per ungoverned removal.
|
|
369
|
+
*/
|
|
370
|
+
export function findRemovedPaths(input) {
|
|
371
|
+
const rows = [];
|
|
372
|
+
for (const [file, meta] of input.before) {
|
|
373
|
+
if (input.after.has(file)) continue;
|
|
374
|
+
const deletion = findGoverningDeletion(
|
|
375
|
+
meta.stack,
|
|
376
|
+
meta.destination,
|
|
377
|
+
input.manifests
|
|
378
|
+
);
|
|
379
|
+
const noted = hasUsableNote(input.ledger.get(removalKey(file)));
|
|
380
|
+
const verdict = classifyRemovedPath(meta.destination, deletion, noted);
|
|
381
|
+
if (verdict === null) continue;
|
|
382
|
+
rows.push({
|
|
383
|
+
destination: meta.destination,
|
|
384
|
+
kind: verdict.kind,
|
|
385
|
+
path: file,
|
|
386
|
+
reason: verdict.reason,
|
|
387
|
+
release: removalRelease(input.root, input.baseline, file),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return rows.sort((left, right) => left.path.localeCompare(right.path));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The verdict on one removed shipped file.
|
|
395
|
+
*
|
|
396
|
+
* @param {string} destination - the path in the consumer's tree.
|
|
397
|
+
* @param {{ by: string, forced: boolean } | null} deletion - the governing deletion.
|
|
398
|
+
* @param {boolean} noted - whether the ledger records the removal.
|
|
399
|
+
* @returns {{ kind: string, reason: string } | null} the finding, or null when governed.
|
|
400
|
+
*/
|
|
401
|
+
export function classifyRemovedPath(destination, deletion, noted) {
|
|
402
|
+
const bindable = isConsumerBindable(destination);
|
|
403
|
+
if (bindable && deletion !== null && !deletion.forced) {
|
|
404
|
+
return {
|
|
405
|
+
kind: "propagated-bindable",
|
|
406
|
+
reason:
|
|
407
|
+
`${deletion.by}/deletions.json propagates this removal, but a host may ` +
|
|
408
|
+
`have wired ${destination} into its own package.json - deleting it ` +
|
|
409
|
+
`takes away a working capability. Move the entry to "keep" and record ` +
|
|
410
|
+
`the removal in ${LEDGER_PATH}, or give it a "force" reason saying why ` +
|
|
411
|
+
`the removal overrides host wiring.`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (deletion !== null) return null;
|
|
415
|
+
if (noted) return null;
|
|
416
|
+
return bindable
|
|
417
|
+
? {
|
|
418
|
+
kind: "unrecorded-bindable",
|
|
419
|
+
reason:
|
|
420
|
+
`A host may have wired ${destination} into its own package.json, so ` +
|
|
421
|
+
`this must NOT be propagated as a deletion. Record it in ` +
|
|
422
|
+
`${LEDGER_PATH} with a consumer-facing note instead.`,
|
|
423
|
+
}
|
|
424
|
+
: {
|
|
425
|
+
kind: "unrecorded",
|
|
426
|
+
reason:
|
|
427
|
+
`No deletions manifest active for this stack removes ${destination}, ` +
|
|
428
|
+
`and ${LEDGER_PATH} does not record it. The copy lanes overwrite and ` +
|
|
429
|
+
`never delete, so every host that applied a release carrying this ` +
|
|
430
|
+
`path keeps it at ${destination} and no later upgrade removes it.`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Findings for named EXPORTS present at the baseline and gone at HEAD.
|
|
436
|
+
*
|
|
437
|
+
* @param {{ root: string, baseline: string, before: Map<string, object>, after: Map<string, object>, ledger: Map<string, object>, importers: Map<string, number> }} input
|
|
438
|
+
* the resolved scan inputs.
|
|
439
|
+
* @returns {Array<Record<string, unknown>>} one row per unrecorded export removal.
|
|
440
|
+
*/
|
|
441
|
+
export function findRemovedExports(input) {
|
|
442
|
+
const rows = [];
|
|
443
|
+
for (const file of changedModules(input.root, input.baseline)) {
|
|
444
|
+
if (!input.before.has(file) || !input.after.has(file)) continue;
|
|
445
|
+
const was = parseNamedExports(showFile(input.root, input.baseline, file));
|
|
446
|
+
const now = parseNamedExports(showFile(input.root, "HEAD", file));
|
|
447
|
+
for (const name of [...was.names].sort()) {
|
|
448
|
+
if (now.names.has(name)) continue;
|
|
449
|
+
if (hasUsableNote(input.ledger.get(removalKey(file, name)))) continue;
|
|
450
|
+
rows.push({
|
|
451
|
+
export: name,
|
|
452
|
+
importers: input.importers.get(file) ?? 0,
|
|
453
|
+
kind: "unrecorded-export",
|
|
454
|
+
path: file,
|
|
455
|
+
release: removalRelease(input.root, input.baseline, file, name),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return rows;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Shipped module files that changed between the baseline and HEAD. Comparing
|
|
464
|
+
* only these keeps the export scan to a handful of blob reads rather than one
|
|
465
|
+
* per shipped script.
|
|
466
|
+
*
|
|
467
|
+
* @param {string} root - repository root.
|
|
468
|
+
* @param {string} baseline - the baseline sha.
|
|
469
|
+
* @returns {string[]} repo-relative paths.
|
|
470
|
+
*/
|
|
471
|
+
function changedModules(root, baseline) {
|
|
472
|
+
const stdout = git(
|
|
473
|
+
root,
|
|
474
|
+
["diff", "--name-only", "-z", `${baseline}..HEAD`],
|
|
475
|
+
"could not diff the baseline against HEAD"
|
|
476
|
+
);
|
|
477
|
+
return stdout
|
|
478
|
+
.split("\0")
|
|
479
|
+
.filter(
|
|
480
|
+
file =>
|
|
481
|
+
file !== "" &&
|
|
482
|
+
shippedParts(file) !== null &&
|
|
483
|
+
MODULE_EXTENSIONS.includes(path.posix.extname(file))
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Read one blob at a ref.
|
|
489
|
+
*
|
|
490
|
+
* @param {string} root - repository root.
|
|
491
|
+
* @param {string} ref - a git ref.
|
|
492
|
+
* @param {string} file - repo-relative path.
|
|
493
|
+
* @returns {string} the file contents.
|
|
494
|
+
*/
|
|
495
|
+
function showFile(root, ref, file) {
|
|
496
|
+
return git(root, ["show", `${ref}:${file}`], `could not read ${file}@${ref}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* The delivery view a consumer of `stack` sees: destination path to repo path,
|
|
501
|
+
* more specific stacks winning over their ancestors.
|
|
502
|
+
*
|
|
503
|
+
* @param {string} stack - the leaf stack.
|
|
504
|
+
* @param {Map<string, { stack: string, destination: string }>} shipped - files at HEAD.
|
|
505
|
+
* @returns {Map<string, string>} destination path to repo path.
|
|
506
|
+
*/
|
|
507
|
+
export function deliveryView(stack, shipped) {
|
|
508
|
+
const chain = ancestryChain(stack);
|
|
509
|
+
const view = new Map();
|
|
510
|
+
for (const specificity of [...chain].reverse()) {
|
|
511
|
+
for (const [file, meta] of shipped) {
|
|
512
|
+
if (meta.stack === specificity) view.set(meta.destination, file);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return view;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Every named export a destination offers in one delivery view, following
|
|
520
|
+
* `export *` re-exports so a legitimate indirection is not read as a miss.
|
|
521
|
+
*
|
|
522
|
+
* @param {string} destination - the target's path in the consumer's tree.
|
|
523
|
+
* @param {Map<string, string>} view - destination path to repo path.
|
|
524
|
+
* @param {(file: string) => string} read - reads a repo path at HEAD.
|
|
525
|
+
* @param {Set<string>} [seen] - cycle guard.
|
|
526
|
+
* @returns {Set<string> | null} the names, or null when nothing ships there.
|
|
527
|
+
*/
|
|
528
|
+
export function viewExports(destination, view, read, seen = new Set()) {
|
|
529
|
+
const file = view.get(destination);
|
|
530
|
+
if (file === undefined || seen.has(destination)) return null;
|
|
531
|
+
seen.add(destination);
|
|
532
|
+
const parsed = parseNamedExports(read(file));
|
|
533
|
+
for (const specifier of parsed.starFrom) {
|
|
534
|
+
const inherited = viewExports(
|
|
535
|
+
resolveRelative(destination, specifier),
|
|
536
|
+
view,
|
|
537
|
+
read,
|
|
538
|
+
seen
|
|
539
|
+
);
|
|
540
|
+
if (inherited !== null)
|
|
541
|
+
for (const name of inherited) parsed.names.add(name);
|
|
542
|
+
}
|
|
543
|
+
return parsed.names;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Findings for shipped-to-shipped named imports that do not resolve.
|
|
548
|
+
*
|
|
549
|
+
* A specifier that resolves to nothing shipped is NOT a finding: the consumer's
|
|
550
|
+
* own tree may provide it, and this repository cannot see that. What is a
|
|
551
|
+
* finding is a target that IS shipped and does not export the requested name -
|
|
552
|
+
* the shape that fails at module link time with "does not provide an export
|
|
553
|
+
* named X".
|
|
554
|
+
*
|
|
555
|
+
* @param {{ shipped: Map<string, object>, read: (file: string) => string }} input
|
|
556
|
+
* files at HEAD and a blob reader.
|
|
557
|
+
* @returns {Array<Record<string, unknown>>} one row per missing name.
|
|
558
|
+
*/
|
|
559
|
+
export function findUnresolvedImports(input) {
|
|
560
|
+
const rows = new Map();
|
|
561
|
+
for (const stack of new Set([...input.shipped.values()].map(m => m.stack))) {
|
|
562
|
+
const view = deliveryView(stack, input.shipped);
|
|
563
|
+
for (const [destination, file] of view) {
|
|
564
|
+
if (!MODULE_EXTENSIONS.includes(path.posix.extname(file))) continue;
|
|
565
|
+
for (const request of parseRelativeNamedImports(input.read(file))) {
|
|
566
|
+
const target = resolveRelative(destination, request.specifier);
|
|
567
|
+
const offered = viewExports(target, view, input.read);
|
|
568
|
+
if (offered === null) continue;
|
|
569
|
+
for (const name of request.names) {
|
|
570
|
+
if (offered.has(name)) continue;
|
|
571
|
+
const row = {
|
|
572
|
+
export: name,
|
|
573
|
+
path: file,
|
|
574
|
+
stack,
|
|
575
|
+
target: view.get(target),
|
|
576
|
+
};
|
|
577
|
+
rows.set(`${file} ${name} ${stack}`, row);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return [...rows.values()].sort((left, right) =>
|
|
583
|
+
`${left.path} ${left.export}`.localeCompare(`${right.path} ${right.export}`)
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Ledger entries contradicted by HEAD: a path or symbol recorded as removed
|
|
589
|
+
* that is in fact still shipped. Left unchecked, these accumulate and start
|
|
590
|
+
* pre-authorising removals nobody reviewed.
|
|
591
|
+
*
|
|
592
|
+
* Entries that match no removal inside the window are NOT reported - they are
|
|
593
|
+
* the historical record, and are meant to outlive the baseline.
|
|
594
|
+
*
|
|
595
|
+
* @param {{ removals: ReadonlyArray<Record<string, unknown>>, shipped: Map<string, object>, read: (file: string) => string }} input
|
|
596
|
+
* the ledger and files at HEAD.
|
|
597
|
+
* @returns {Array<Record<string, unknown>>} one row per contradicted entry.
|
|
598
|
+
*/
|
|
599
|
+
export function findContradictedLedger(input) {
|
|
600
|
+
const rows = [];
|
|
601
|
+
for (const entry of input.removals) {
|
|
602
|
+
const present = input.shipped.has(entry.path);
|
|
603
|
+
if (entry.export === undefined) {
|
|
604
|
+
if (present) rows.push({ kind: "still-shipped", path: entry.path });
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (!present) continue;
|
|
608
|
+
if (!parseNamedExports(input.read(entry.path)).names.has(entry.export))
|
|
609
|
+
continue;
|
|
610
|
+
rows.push({
|
|
611
|
+
export: entry.export,
|
|
612
|
+
kind: "still-exported",
|
|
613
|
+
path: entry.path,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
return rows;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Assemble the machine-readable report.
|
|
621
|
+
*
|
|
622
|
+
* @param {{ paths: Array<object>, exports: Array<object>, imports: Array<object>, ledger: Array<object> }} findings
|
|
623
|
+
* every detector's rows.
|
|
624
|
+
* @param {{ root: string, baseline: string, shipped: number, recorded: number }} opts
|
|
625
|
+
* resolved options plus scan size.
|
|
626
|
+
* @returns {{ baseline: string, findings: object, root: string, schemaVersion: number, summary: { contradictedLedgerEntries: number, recordedRemovals: number, shippedFiles: number, unresolvedImports: number, ungovernedExports: number, ungovernedPaths: number, violations: number } }} the report object.
|
|
627
|
+
*/
|
|
628
|
+
export function buildReport(findings, opts) {
|
|
629
|
+
const total =
|
|
630
|
+
findings.paths.length +
|
|
631
|
+
findings.exports.length +
|
|
632
|
+
findings.imports.length +
|
|
633
|
+
findings.ledger.length;
|
|
634
|
+
return {
|
|
635
|
+
baseline: opts.baseline,
|
|
636
|
+
findings,
|
|
637
|
+
root: opts.root,
|
|
638
|
+
schemaVersion: 1,
|
|
639
|
+
summary: {
|
|
640
|
+
contradictedLedgerEntries: findings.ledger.length,
|
|
641
|
+
recordedRemovals: opts.recorded,
|
|
642
|
+
shippedFiles: opts.shipped,
|
|
643
|
+
unresolvedImports: findings.imports.length,
|
|
644
|
+
ungovernedExports: findings.exports.length,
|
|
645
|
+
ungovernedPaths: findings.paths.length,
|
|
646
|
+
violations: total,
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Render the human-readable report. Each line names the surface that changed
|
|
653
|
+
* and the consequence for a host, because "removal governance failed" is not
|
|
654
|
+
* something an operator can act on.
|
|
655
|
+
*
|
|
656
|
+
* @param {Record<string, unknown>} report - the report object.
|
|
657
|
+
* @returns {string} the rendered report.
|
|
658
|
+
*/
|
|
659
|
+
export function humanReport(report) {
|
|
660
|
+
const { findings, summary } = report;
|
|
661
|
+
if (summary.violations === 0) {
|
|
662
|
+
return [
|
|
663
|
+
`OK ${summary.shippedFiles} shipped file(s) since ${report.baseline}:`,
|
|
664
|
+
` every removal from a shipped surface is governed`,
|
|
665
|
+
` (${summary.recordedRemovals} removal(s) recorded in ${LEDGER_PATH})`,
|
|
666
|
+
].join("\n");
|
|
667
|
+
}
|
|
668
|
+
return [
|
|
669
|
+
...findings.paths.map(
|
|
670
|
+
row =>
|
|
671
|
+
`FAIL ${row.path}\n` +
|
|
672
|
+
` removed in ${row.release}; destination ${row.destination}\n` +
|
|
673
|
+
` ${row.reason}`
|
|
674
|
+
),
|
|
675
|
+
...findings.exports.map(
|
|
676
|
+
row =>
|
|
677
|
+
`FAIL ${row.path} no longer exports ${row.export}\n` +
|
|
678
|
+
` removed in ${row.release}; ${row.importers} shipped importer(s) ` +
|
|
679
|
+
`remain, and host importers cannot be counted from here\n` +
|
|
680
|
+
` A host importing ${row.export} fails at module link time with ` +
|
|
681
|
+
`"does not provide an export named '${row.export}'", in whichever job ` +
|
|
682
|
+
`runs the importer first. Record it in ${LEDGER_PATH} with a note ` +
|
|
683
|
+
`saying what a host should do instead.`
|
|
684
|
+
),
|
|
685
|
+
...findings.imports.map(
|
|
686
|
+
row =>
|
|
687
|
+
`FAIL ${row.path} imports ${row.export} from ${row.target}\n` +
|
|
688
|
+
` which does not export it (delivery view of stack ${row.stack})`
|
|
689
|
+
),
|
|
690
|
+
...findings.ledger.map(
|
|
691
|
+
row =>
|
|
692
|
+
`FAIL ${LEDGER_PATH} records ${removalKey(row.path, row.export)} as ` +
|
|
693
|
+
`removed, but it is still shipped at HEAD`
|
|
694
|
+
),
|
|
695
|
+
"",
|
|
696
|
+
`${summary.violations} ungoverned change(s) to a shipped surface.`,
|
|
697
|
+
"The copy lanes overwrite and never delete, so a removal made here reaches",
|
|
698
|
+
"an installed base only through a deletions manifest or a migration. A",
|
|
699
|
+
"removal with neither does nothing downstream, silently, while looking",
|
|
700
|
+
"complete in the diff.",
|
|
701
|
+
].join("\n");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Parse argv into resolved options. Throws `UsageError` on a bad invocation.
|
|
706
|
+
*
|
|
707
|
+
* @param {readonly string[]} argv - arguments (without node/script prefix).
|
|
708
|
+
* @returns {{ root: string, since: string | null, json: boolean }} options.
|
|
709
|
+
*/
|
|
710
|
+
export function parseArgs(argv) {
|
|
711
|
+
let root = null;
|
|
712
|
+
let since = null;
|
|
713
|
+
let json = false;
|
|
714
|
+
for (let index = 0; index < argv.length; index++) {
|
|
715
|
+
const arg = argv[index];
|
|
716
|
+
if (arg === "--json") {
|
|
717
|
+
json = true;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (arg !== "--root" && arg !== "--since") {
|
|
721
|
+
throw new UsageError(`unknown argument: ${arg}`);
|
|
722
|
+
}
|
|
723
|
+
const next = argv[index + 1];
|
|
724
|
+
if (next === undefined || next.startsWith("--")) {
|
|
725
|
+
throw new UsageError(`${arg} requires a value`);
|
|
726
|
+
}
|
|
727
|
+
if (arg === "--root") root = next;
|
|
728
|
+
else since = next;
|
|
729
|
+
index += 1;
|
|
730
|
+
}
|
|
731
|
+
return { json, root: path.resolve(root ?? REPO_ROOT), since };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Count, per shipped module, how many DISTINCT other shipped modules import
|
|
736
|
+
* from it.
|
|
737
|
+
*
|
|
738
|
+
* Distinct by importer path, not by resolution: a module reachable from
|
|
739
|
+
* several stack views would otherwise be counted once per view, and the number
|
|
740
|
+
* printed in a finding has to mean "files", because that is how a reader will
|
|
741
|
+
* read it.
|
|
742
|
+
*
|
|
743
|
+
* @param {Map<string, { destination: string, stack: string }>} shipped - files at HEAD.
|
|
744
|
+
* @param {(file: string) => string} read - reads a repo path at HEAD.
|
|
745
|
+
* @returns {Map<string, number>} repo path to importer count.
|
|
746
|
+
*/
|
|
747
|
+
export function countImporters(shipped, read) {
|
|
748
|
+
const importers = new Map();
|
|
749
|
+
for (const stack of new Set([...shipped.values()].map(meta => meta.stack))) {
|
|
750
|
+
const view = deliveryView(stack, shipped);
|
|
751
|
+
for (const [destination, file] of view) {
|
|
752
|
+
if (!MODULE_EXTENSIONS.includes(path.posix.extname(file))) continue;
|
|
753
|
+
for (const request of parseRelativeNamedImports(read(file))) {
|
|
754
|
+
const target = view.get(
|
|
755
|
+
resolveRelative(destination, request.specifier)
|
|
756
|
+
);
|
|
757
|
+
if (target === undefined || target === file) continue;
|
|
758
|
+
if (!importers.has(target)) importers.set(target, new Set());
|
|
759
|
+
importers.get(target).add(file);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return new Map([...importers].map(([file, set]) => [file, set.size]));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Run every detector against a resolved repository.
|
|
768
|
+
*
|
|
769
|
+
* @param {{ root: string, since: string | null }} opts - resolved options.
|
|
770
|
+
* @returns {Record<string, unknown>} the report object.
|
|
771
|
+
*/
|
|
772
|
+
export function scan(opts) {
|
|
773
|
+
const ledger = loadLedger(opts.root);
|
|
774
|
+
const baseline = resolveBaseline(opts.root, opts.since ?? ledger.baseline);
|
|
775
|
+
const before = shippedFilesAt(opts.root, baseline);
|
|
776
|
+
const after = shippedFilesAt(opts.root, "HEAD");
|
|
777
|
+
if (after.size === 0) {
|
|
778
|
+
throw new UsageError(
|
|
779
|
+
`no shipped files found under ${opts.root} - refusing to report a clean ` +
|
|
780
|
+
`scan of a tree this gate could not read`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
const read = file => showFile(opts.root, "HEAD", file);
|
|
784
|
+
const index = indexRemovals(ledger.removals);
|
|
785
|
+
const findings = {
|
|
786
|
+
exports: findRemovedExports({
|
|
787
|
+
after,
|
|
788
|
+
baseline,
|
|
789
|
+
before,
|
|
790
|
+
importers: countImporters(after, read),
|
|
791
|
+
ledger: index,
|
|
792
|
+
root: opts.root,
|
|
793
|
+
}),
|
|
794
|
+
imports: findUnresolvedImports({ read, shipped: after }),
|
|
795
|
+
ledger: findContradictedLedger({
|
|
796
|
+
read,
|
|
797
|
+
removals: ledger.removals,
|
|
798
|
+
shipped: after,
|
|
799
|
+
}),
|
|
800
|
+
paths: findRemovedPaths({
|
|
801
|
+
after,
|
|
802
|
+
baseline,
|
|
803
|
+
before,
|
|
804
|
+
ledger: index,
|
|
805
|
+
manifests: readDeletionManifests(opts.root),
|
|
806
|
+
root: opts.root,
|
|
807
|
+
}),
|
|
808
|
+
};
|
|
809
|
+
return buildReport(findings, {
|
|
810
|
+
baseline: opts.since ?? ledger.baseline,
|
|
811
|
+
recorded: ledger.removals.length,
|
|
812
|
+
root: opts.root,
|
|
813
|
+
shipped: after.size,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Run the gate. Returns the process exit code (does not call `exit`).
|
|
819
|
+
*
|
|
820
|
+
* @param {readonly string[]} argv - arguments (without node/script prefix).
|
|
821
|
+
* @param {{ stdout?: { write(s: string): void }, stderr?: { write(s: string): void } }} [io]
|
|
822
|
+
* injectable streams (defaults to process streams).
|
|
823
|
+
* @returns {number} exit code (0 clean, 1 finding, 2 usage/unverifiable).
|
|
824
|
+
*/
|
|
825
|
+
export function main(argv, io = {}) {
|
|
826
|
+
const out = io.stdout ?? process.stdout;
|
|
827
|
+
const err = io.stderr ?? process.stderr;
|
|
828
|
+
let report;
|
|
829
|
+
try {
|
|
830
|
+
const opts = parseArgs(argv);
|
|
831
|
+
if (!fs.existsSync(opts.root) || !fs.statSync(opts.root).isDirectory()) {
|
|
832
|
+
throw new UsageError(`--root ${opts.root} is not a directory`);
|
|
833
|
+
}
|
|
834
|
+
report = scan(opts);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
if (error instanceof UsageError) {
|
|
837
|
+
err.write(`check-shipped-surface-removals: ${error.message}\n`);
|
|
838
|
+
return 2;
|
|
839
|
+
}
|
|
840
|
+
throw error;
|
|
841
|
+
}
|
|
842
|
+
const rendered = argv.includes("--json")
|
|
843
|
+
? JSON.stringify(report, null, 2)
|
|
844
|
+
: humanReport(report);
|
|
845
|
+
out.write(`${rendered}\n`);
|
|
846
|
+
return report.summary.violations === 0 ? 0 : 1;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (invokedAsScript(import.meta.url)) {
|
|
850
|
+
process.exitCode = main(process.argv.slice(2));
|
|
851
|
+
}
|