@shrkcrft/cli 0.1.0-alpha.30 → 0.1.0-alpha.31
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/dist/asset-preview/apply-asset-preview.js +1 -1
- package/dist/authoring/authoring-kit.d.ts +9 -1
- package/dist/authoring/authoring-kit.d.ts.map +1 -1
- package/dist/authoring/authoring-kit.js +26 -13
- package/dist/bootstrap/unlinked-workspace-dependency.d.ts +11 -0
- package/dist/bootstrap/unlinked-workspace-dependency.d.ts.map +1 -0
- package/dist/bootstrap/unlinked-workspace-dependency.js +99 -0
- package/dist/command-registry.d.ts +100 -1
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +130 -3
- package/dist/commands/api-diff.command.d.ts.map +1 -1
- package/dist/commands/api-diff.command.js +12 -0
- package/dist/commands/apply.command.d.ts.map +1 -1
- package/dist/commands/apply.command.js +3 -0
- package/dist/commands/arch.command.d.ts.map +1 -1
- package/dist/commands/arch.command.js +26 -7
- package/dist/commands/architecture.command.d.ts.map +1 -1
- package/dist/commands/architecture.command.js +8 -2
- package/dist/commands/baseline.command.d.ts +54 -3
- package/dist/commands/baseline.command.d.ts.map +1 -1
- package/dist/commands/baseline.command.js +582 -74
- package/dist/commands/biome.command.d.ts.map +1 -1
- package/dist/commands/biome.command.js +20 -0
- package/dist/commands/boundaries.command.d.ts +9 -1
- package/dist/commands/boundaries.command.d.ts.map +1 -1
- package/dist/commands/boundaries.command.js +88 -20
- package/dist/commands/bundle.command.d.ts.map +1 -1
- package/dist/commands/bundle.command.js +57 -11
- package/dist/commands/cache-align.command.d.ts.map +1 -1
- package/dist/commands/cache-align.command.js +3 -1
- package/dist/commands/changelog-data.d.ts.map +1 -1
- package/dist/commands/changelog-data.js +138 -0
- package/dist/commands/changes.command.d.ts.map +1 -1
- package/dist/commands/changes.command.js +15 -0
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +1241 -322
- package/dist/commands/checks.command.d.ts.map +1 -1
- package/dist/commands/checks.command.js +188 -40
- package/dist/commands/ci.command.d.ts.map +1 -1
- package/dist/commands/ci.command.js +35 -2
- package/dist/commands/code-intel.command.d.ts.map +1 -1
- package/dist/commands/code-intel.command.js +3 -0
- package/dist/commands/command-catalog.d.ts +23 -1
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +152 -35
- package/dist/commands/commands.command.d.ts +21 -3
- package/dist/commands/commands.command.d.ts.map +1 -1
- package/dist/commands/commands.command.js +172 -27
- package/dist/commands/compress.command.d.ts.map +1 -1
- package/dist/commands/compress.command.js +4 -0
- package/dist/commands/constructs.command.d.ts.map +1 -1
- package/dist/commands/constructs.command.js +50 -12
- package/dist/commands/context.command.d.ts.map +1 -1
- package/dist/commands/context.command.js +40 -5
- package/dist/commands/contract-templates.command.d.ts.map +1 -1
- package/dist/commands/contract-templates.command.js +9 -1
- package/dist/commands/contract.command.d.ts.map +1 -1
- package/dist/commands/contract.command.js +35 -0
- package/dist/commands/conventions.command.d.ts.map +1 -1
- package/dist/commands/conventions.command.js +189 -29
- package/dist/commands/coverage.command.d.ts.map +1 -1
- package/dist/commands/coverage.command.js +9 -0
- package/dist/commands/daily.commands.d.ts +0 -1
- package/dist/commands/daily.commands.d.ts.map +1 -1
- package/dist/commands/daily.commands.js +12 -76
- package/dist/commands/dashboard.command.d.ts.map +1 -1
- package/dist/commands/dashboard.command.js +10 -0
- package/dist/commands/dev.command.d.ts.map +1 -1
- package/dist/commands/dev.command.js +45 -2
- package/dist/commands/diagnostics.command.d.ts +0 -2
- package/dist/commands/diagnostics.command.d.ts.map +1 -1
- package/dist/commands/diagnostics.command.js +5 -78
- package/dist/commands/diff-check.command.d.ts +6 -4
- package/dist/commands/diff-check.command.d.ts.map +1 -1
- package/dist/commands/diff-check.command.js +190 -103
- package/dist/commands/docs-references.command.d.ts +13 -0
- package/dist/commands/docs-references.command.d.ts.map +1 -1
- package/dist/commands/docs-references.command.js +269 -72
- package/dist/commands/doctor.command.d.ts.map +1 -1
- package/dist/commands/doctor.command.js +58 -4
- package/dist/commands/drift.command.d.ts.map +1 -1
- package/dist/commands/drift.command.js +17 -4
- package/dist/commands/eslint.command.d.ts.map +1 -1
- package/dist/commands/eslint.command.js +20 -0
- package/dist/commands/export.command.d.ts.map +1 -1
- package/dist/commands/export.command.js +23 -1
- package/dist/commands/feedback-dispatch.command.d.ts.map +1 -1
- package/dist/commands/feedback-dispatch.command.js +29 -0
- package/dist/commands/feedback.command.d.ts.map +1 -1
- package/dist/commands/feedback.command.js +6 -1
- package/dist/commands/finish.command.d.ts.map +1 -1
- package/dist/commands/finish.command.js +32 -8
- package/dist/commands/fix.command.d.ts.map +1 -1
- package/dist/commands/fix.command.js +19 -0
- package/dist/commands/gate.command.d.ts +6 -1
- package/dist/commands/gate.command.d.ts.map +1 -1
- package/dist/commands/gate.command.js +50 -124
- package/dist/commands/gates.command.d.ts +52 -0
- package/dist/commands/gates.command.d.ts.map +1 -1
- package/dist/commands/gates.command.js +811 -185
- package/dist/commands/gen.command.d.ts.map +1 -1
- package/dist/commands/gen.command.js +4 -0
- package/dist/commands/generated.command.d.ts +33 -3
- package/dist/commands/generated.command.d.ts.map +1 -1
- package/dist/commands/generated.command.js +499 -147
- package/dist/commands/graph-code-subverbs.d.ts +15 -0
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +198 -24
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +79 -37
- package/dist/commands/help.command.d.ts +30 -6
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +323 -199
- package/dist/commands/helper.command.d.ts +1 -0
- package/dist/commands/helper.command.d.ts.map +1 -1
- package/dist/commands/helper.command.js +195 -56
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +115 -12
- package/dist/commands/infer.command.d.ts.map +1 -1
- package/dist/commands/infer.command.js +20 -45
- package/dist/commands/ingest.command.d.ts +0 -1
- package/dist/commands/ingest.command.d.ts.map +1 -1
- package/dist/commands/ingest.command.js +25 -41
- package/dist/commands/knowledge-author.command.d.ts +0 -3
- package/dist/commands/knowledge-author.command.d.ts.map +1 -1
- package/dist/commands/knowledge-author.command.js +10 -23
- package/dist/commands/knowledge.command.d.ts.map +1 -1
- package/dist/commands/knowledge.command.js +469 -129
- package/dist/commands/languages.command.d.ts.map +1 -1
- package/dist/commands/languages.command.js +27 -0
- package/dist/commands/lint.command.d.ts.map +1 -1
- package/dist/commands/lint.command.js +3 -0
- package/dist/commands/mcp.command.d.ts +18 -0
- package/dist/commands/mcp.command.d.ts.map +1 -1
- package/dist/commands/mcp.command.js +33 -8
- package/dist/commands/onboard.command.d.ts.map +1 -1
- package/dist/commands/onboard.command.js +20 -0
- package/dist/commands/owners.command.d.ts.map +1 -1
- package/dist/commands/owners.command.js +14 -2
- package/dist/commands/packs-new.d.ts +13 -1
- package/dist/commands/packs-new.d.ts.map +1 -1
- package/dist/commands/packs-new.js +500 -335
- package/dist/commands/packs.command.d.ts.map +1 -1
- package/dist/commands/packs.command.js +378 -36
- package/dist/commands/paths.command.d.ts.map +1 -1
- package/dist/commands/paths.command.js +9 -1
- package/dist/commands/pipelines.command.d.ts.map +1 -1
- package/dist/commands/pipelines.command.js +7 -1
- package/dist/commands/plan-check.command.d.ts.map +1 -1
- package/dist/commands/plan-check.command.js +3 -0
- package/dist/commands/plan.command.d.ts.map +1 -1
- package/dist/commands/plan.command.js +3 -0
- package/dist/commands/playbooks.command.d.ts.map +1 -1
- package/dist/commands/playbooks.command.js +7 -2
- package/dist/commands/policy-lint.command.d.ts +19 -2
- package/dist/commands/policy-lint.command.d.ts.map +1 -1
- package/dist/commands/policy-lint.command.js +224 -65
- package/dist/commands/policy.command.d.ts.map +1 -1
- package/dist/commands/policy.command.js +6 -1
- package/dist/commands/pr.command.d.ts.map +1 -1
- package/dist/commands/pr.command.js +14 -0
- package/dist/commands/presets.command.d.ts.map +1 -1
- package/dist/commands/presets.command.js +12 -27
- package/dist/commands/profiles.command.d.ts.map +1 -1
- package/dist/commands/profiles.command.js +94 -26
- package/dist/commands/provenance.command.js +1 -1
- package/dist/commands/quality.command.d.ts.map +1 -1
- package/dist/commands/quality.command.js +129 -33
- package/dist/commands/recommend.command.d.ts +20 -4
- package/dist/commands/recommend.command.d.ts.map +1 -1
- package/dist/commands/recommend.command.js +172 -278
- package/dist/commands/registrations.command.d.ts.map +1 -1
- package/dist/commands/registrations.command.js +138 -13
- package/dist/commands/registry-lifecycle-run.d.ts +14 -0
- package/dist/commands/registry-lifecycle-run.d.ts.map +1 -0
- package/dist/commands/registry-lifecycle-run.js +336 -0
- package/dist/commands/registry.command.d.ts.map +1 -1
- package/dist/commands/registry.command.js +235 -62
- package/dist/commands/release.command.d.ts.map +1 -1
- package/dist/commands/release.command.js +99 -20
- package/dist/commands/report.command.d.ts.map +1 -1
- package/dist/commands/report.command.js +38 -2
- package/dist/commands/reuse-coverage.command.d.ts +23 -0
- package/dist/commands/reuse-coverage.command.d.ts.map +1 -0
- package/dist/commands/reuse-coverage.command.js +536 -0
- package/dist/commands/reuse.command.d.ts +3 -18
- package/dist/commands/reuse.command.d.ts.map +1 -1
- package/dist/commands/reuse.command.js +395 -244
- package/dist/commands/review.command.d.ts.map +1 -1
- package/dist/commands/review.command.js +33 -1
- package/dist/commands/rounds.command.d.ts.map +1 -1
- package/dist/commands/rounds.command.js +5 -0
- package/dist/commands/safety.command.d.ts.map +1 -1
- package/dist/commands/safety.command.js +9 -0
- package/dist/commands/scaffolds.command.d.ts.map +1 -1
- package/dist/commands/scaffolds.command.js +97 -25
- package/dist/commands/search.command.d.ts +0 -8
- package/dist/commands/search.command.d.ts.map +1 -1
- package/dist/commands/search.command.js +136 -28
- package/dist/commands/self-config-xrefs.command.d.ts +24 -0
- package/dist/commands/self-config-xrefs.command.d.ts.map +1 -0
- package/dist/commands/self-config-xrefs.command.js +170 -0
- package/dist/commands/self-config.command.d.ts.map +1 -1
- package/dist/commands/self-config.command.js +261 -65
- package/dist/commands/smart-context.command.d.ts.map +1 -1
- package/dist/commands/smart-context.command.js +39 -3
- package/dist/commands/spec.command.d.ts.map +1 -1
- package/dist/commands/spec.command.js +3 -0
- package/dist/commands/stats.command.d.ts.map +1 -1
- package/dist/commands/stats.command.js +3 -0
- package/dist/commands/surface.command.d.ts +6 -1
- package/dist/commands/surface.command.d.ts.map +1 -1
- package/dist/commands/surface.command.js +256 -54
- package/dist/commands/task-context.command.js +1 -1
- package/dist/commands/task.command.d.ts.map +1 -1
- package/dist/commands/task.command.js +22 -1
- package/dist/commands/templates.command.d.ts +4 -0
- package/dist/commands/templates.command.d.ts.map +1 -1
- package/dist/commands/templates.command.js +109 -22
- package/dist/commands/test.command.d.ts.map +1 -1
- package/dist/commands/test.command.js +161 -33
- package/dist/commands/tests.command.d.ts.map +1 -1
- package/dist/commands/tests.command.js +29 -3
- package/dist/commands/trace.command.d.ts.map +1 -1
- package/dist/commands/trace.command.js +29 -4
- package/dist/commands/watch.command.d.ts.map +1 -1
- package/dist/commands/watch.command.js +3 -0
- package/dist/commands/why.command.d.ts.map +1 -1
- package/dist/commands/why.command.js +3 -0
- package/dist/commands/wiring.command.d.ts +18 -4
- package/dist/commands/wiring.command.d.ts.map +1 -1
- package/dist/commands/wiring.command.js +360 -73
- package/dist/dashboard/code-intelligence-data.js +4 -4
- package/dist/diff/deleted-orphan-coverage.d.ts +32 -0
- package/dist/diff/deleted-orphan-coverage.d.ts.map +1 -0
- package/dist/diff/deleted-orphan-coverage.js +107 -0
- package/dist/diff/deleted-orphan-scope-notes.d.ts +23 -0
- package/dist/diff/deleted-orphan-scope-notes.d.ts.map +1 -0
- package/dist/diff/deleted-orphan-scope-notes.js +49 -0
- package/dist/diff/deleted-orphans.d.ts +32 -0
- package/dist/diff/deleted-orphans.d.ts.map +1 -1
- package/dist/diff/deleted-orphans.js +42 -2
- package/dist/dispatch/closest-match.d.ts +15 -0
- package/dist/dispatch/closest-match.d.ts.map +1 -0
- package/dist/dispatch/closest-match.js +47 -0
- package/dist/dispatch/global-flags.d.ts +94 -0
- package/dist/dispatch/global-flags.d.ts.map +1 -0
- package/dist/dispatch/global-flags.js +123 -0
- package/dist/dispatch/guard-invocation-input.d.ts +24 -0
- package/dist/dispatch/guard-invocation-input.d.ts.map +1 -0
- package/dist/dispatch/guard-invocation-input.js +1 -0
- package/dist/dispatch/guard-invocation.d.ts +33 -0
- package/dist/dispatch/guard-invocation.d.ts.map +1 -0
- package/dist/dispatch/guard-invocation.js +183 -0
- package/dist/dispatch/help-intercept.d.ts +22 -0
- package/dist/dispatch/help-intercept.d.ts.map +1 -0
- package/dist/dispatch/help-intercept.js +42 -0
- package/dist/dispatch/i-declared-invocation.d.ts +17 -0
- package/dist/dispatch/i-declared-invocation.d.ts.map +1 -0
- package/dist/dispatch/i-declared-invocation.js +1 -0
- package/dist/dispatch/i-unknown-flag-refusal-input.d.ts +28 -0
- package/dist/dispatch/i-unknown-flag-refusal-input.d.ts.map +1 -0
- package/dist/dispatch/i-unknown-flag-refusal-input.js +1 -0
- package/dist/dispatch/invocation-rejection-kind.d.ts +15 -0
- package/dist/dispatch/invocation-rejection-kind.d.ts.map +1 -0
- package/dist/dispatch/invocation-rejection-kind.js +15 -0
- package/dist/dispatch/invocation-rejection.d.ts +18 -0
- package/dist/dispatch/invocation-rejection.d.ts.map +1 -0
- package/dist/dispatch/invocation-rejection.js +1 -0
- package/dist/dispatch/judge-invocation.d.ts +24 -0
- package/dist/dispatch/judge-invocation.d.ts.map +1 -0
- package/dist/dispatch/judge-invocation.js +50 -0
- package/dist/dispatch/positional-mode.d.ts +18 -0
- package/dist/dispatch/positional-mode.d.ts.map +1 -0
- package/dist/dispatch/positional-mode.js +18 -0
- package/dist/dispatch/read-tracking-map.d.ts +28 -0
- package/dist/dispatch/read-tracking-map.d.ts.map +1 -0
- package/dist/dispatch/read-tracking-map.js +58 -0
- package/dist/dispatch/refuse-sibling-valve-flags.d.ts +30 -0
- package/dist/dispatch/refuse-sibling-valve-flags.d.ts.map +1 -0
- package/dist/dispatch/refuse-sibling-valve-flags.js +65 -0
- package/dist/dispatch/subverb-spec.d.ts +26 -0
- package/dist/dispatch/subverb-spec.d.ts.map +1 -0
- package/dist/dispatch/subverb-spec.js +1 -0
- package/dist/dispatch/undocumented-flag-reads.d.ts +22 -0
- package/dist/dispatch/undocumented-flag-reads.d.ts.map +1 -0
- package/dist/dispatch/undocumented-flag-reads.js +283 -0
- package/dist/dispatch/unknown-flag-refusal-mode.d.ts +13 -0
- package/dist/dispatch/unknown-flag-refusal-mode.d.ts.map +1 -0
- package/dist/dispatch/unknown-flag-refusal-mode.js +13 -0
- package/dist/dispatch/unknown-flag-refusal.d.ts +22 -0
- package/dist/dispatch/unknown-flag-refusal.d.ts.map +1 -0
- package/dist/dispatch/unknown-flag-refusal.js +57 -0
- package/dist/dispatch/unread-flags.d.ts +152 -0
- package/dist/dispatch/unread-flags.d.ts.map +1 -0
- package/dist/dispatch/unread-flags.js +295 -0
- package/dist/dispatch/verdict-valve-flag.d.ts +21 -0
- package/dist/dispatch/verdict-valve-flag.d.ts.map +1 -0
- package/dist/dispatch/verdict-valve-flag.js +21 -0
- package/dist/dispatch/walk-declared-subverbs.d.ts +14 -0
- package/dist/dispatch/walk-declared-subverbs.d.ts.map +1 -0
- package/dist/dispatch/walk-declared-subverbs.js +28 -0
- package/dist/exit-codes.d.ts +41 -3
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +153 -14
- package/dist/finish/run-finish.d.ts +72 -5
- package/dist/finish/run-finish.d.ts.map +1 -1
- package/dist/finish/run-finish.js +453 -104
- package/dist/gates/accepted-empty-note.d.ts +10 -0
- package/dist/gates/accepted-empty-note.d.ts.map +1 -0
- package/dist/gates/accepted-empty-note.js +11 -0
- package/dist/gates/allow-empty.d.ts +25 -0
- package/dist/gates/allow-empty.d.ts.map +1 -0
- package/dist/gates/allow-empty.js +27 -0
- package/dist/gates/asset-doctor-failing-units.d.ts +16 -0
- package/dist/gates/asset-doctor-failing-units.d.ts.map +1 -0
- package/dist/gates/asset-doctor-failing-units.js +14 -0
- package/dist/gates/asset-doctor-failure-line.d.ts +16 -0
- package/dist/gates/asset-doctor-failure-line.d.ts.map +1 -0
- package/dist/gates/asset-doctor-failure-line.js +28 -0
- package/dist/gates/empty-rule-advice-lines.d.ts +15 -0
- package/dist/gates/empty-rule-advice-lines.d.ts.map +1 -0
- package/dist/gates/empty-rule-advice-lines.js +20 -0
- package/dist/gates/gate-envelope.d.ts +85 -8
- package/dist/gates/gate-envelope.d.ts.map +1 -1
- package/dist/gates/gate-envelope.js +56 -7
- package/dist/gates/gate-rule-globs.d.ts +37 -3
- package/dist/gates/gate-rule-globs.d.ts.map +1 -1
- package/dist/gates/gate-rule-globs.js +78 -40
- package/dist/gates/gate-rule-view.d.ts +17 -4
- package/dist/gates/gate-rule-view.d.ts.map +1 -1
- package/dist/gates/gate-rule-view.js +105 -90
- package/dist/gates/i-coverage-dead-glob.d.ts +19 -0
- package/dist/gates/i-coverage-dead-glob.d.ts.map +1 -0
- package/dist/gates/i-coverage-dead-glob.js +1 -0
- package/dist/gates/i-coverage-negation.d.ts +13 -0
- package/dist/gates/i-coverage-negation.d.ts.map +1 -0
- package/dist/gates/i-coverage-negation.js +1 -0
- package/dist/gates/i-registration-query-settle-input.d.ts +18 -0
- package/dist/gates/i-registration-query-settle-input.d.ts.map +1 -0
- package/dist/gates/i-registration-query-settle-input.js +1 -0
- package/dist/gates/i-unit-state-note-row.d.ts +16 -0
- package/dist/gates/i-unit-state-note-row.d.ts.map +1 -0
- package/dist/gates/i-unit-state-note-row.js +1 -0
- package/dist/gates/i-unit-state-notes.d.ts +32 -0
- package/dist/gates/i-unit-state-notes.d.ts.map +1 -0
- package/dist/gates/i-unit-state-notes.js +1 -0
- package/dist/gates/incomplete-registry-inventory.d.ts +19 -0
- package/dist/gates/incomplete-registry-inventory.d.ts.map +1 -0
- package/dist/gates/incomplete-registry-inventory.js +63 -0
- package/dist/gates/measure-registration-roles.d.ts +10 -0
- package/dist/gates/measure-registration-roles.d.ts.map +1 -0
- package/dist/gates/measure-registration-roles.js +9 -0
- package/dist/gates/plane-verdict.d.ts +14 -0
- package/dist/gates/plane-verdict.d.ts.map +1 -0
- package/dist/gates/plane-verdict.js +22 -0
- package/dist/gates/qualify-clean-for-units.d.ts +12 -0
- package/dist/gates/qualify-clean-for-units.d.ts.map +1 -0
- package/dist/gates/qualify-clean-for-units.js +18 -0
- package/dist/gates/registration-graph-verdict.d.ts +34 -0
- package/dist/gates/registration-graph-verdict.d.ts.map +1 -0
- package/dist/gates/registration-graph-verdict.js +70 -0
- package/dist/gates/registration-roles.d.ts +9 -0
- package/dist/gates/registration-roles.d.ts.map +1 -0
- package/dist/gates/registration-roles.js +1 -0
- package/dist/gates/registry-liveness.d.ts +11 -0
- package/dist/gates/registry-liveness.d.ts.map +1 -0
- package/dist/gates/registry-liveness.js +13 -0
- package/dist/gates/rule-coverage.d.ts +187 -4
- package/dist/gates/rule-coverage.d.ts.map +1 -1
- package/dist/gates/rule-coverage.js +501 -49
- package/dist/gates/run-gate-planes.d.ts +14 -0
- package/dist/gates/run-gate-planes.d.ts.map +1 -1
- package/dist/gates/run-gate-planes.js +172 -30
- package/dist/gates/scaffold-selftest.d.ts +56 -0
- package/dist/gates/scaffold-selftest.d.ts.map +1 -0
- package/dist/gates/scaffold-selftest.js +119 -0
- package/dist/gates/seam-rejected-rules.d.ts +28 -0
- package/dist/gates/seam-rejected-rules.d.ts.map +1 -0
- package/dist/gates/seam-rejected-rules.js +95 -0
- package/dist/gates/self-test-check.d.ts +29 -0
- package/dist/gates/self-test-check.d.ts.map +1 -0
- package/dist/gates/self-test-check.js +1 -0
- package/dist/gates/self-test-subject.d.ts +28 -0
- package/dist/gates/self-test-subject.d.ts.map +1 -0
- package/dist/gates/self-test-subject.js +1 -0
- package/dist/gates/self-test.d.ts +20 -0
- package/dist/gates/self-test.d.ts.map +1 -0
- package/dist/gates/self-test.js +82 -0
- package/dist/gates/settle-verdict.d.ts +14 -0
- package/dist/gates/settle-verdict.d.ts.map +1 -0
- package/dist/gates/settle-verdict.js +13 -0
- package/dist/gates/settled-verdict.d.ts +8 -0
- package/dist/gates/settled-verdict.d.ts.map +1 -0
- package/dist/gates/settled-verdict.js +1 -0
- package/dist/gates/template-registry-coverage.d.ts +18 -0
- package/dist/gates/template-registry-coverage.d.ts.map +1 -0
- package/dist/gates/template-registry-coverage.js +24 -0
- package/dist/gates/unit-state-notes.d.ts +40 -0
- package/dist/gates/unit-state-notes.d.ts.map +1 -0
- package/dist/gates/unit-state-notes.js +71 -0
- package/dist/gates/verdict-line.d.ts +18 -0
- package/dist/gates/verdict-line.d.ts.map +1 -0
- package/dist/gates/verdict-line.js +40 -0
- package/dist/graph/graph-reuse-lookup.d.ts +10 -0
- package/dist/graph/graph-reuse-lookup.d.ts.map +1 -0
- package/dist/graph/graph-reuse-lookup.js +41 -0
- package/dist/graph/index-behind-hint.d.ts +11 -0
- package/dist/graph/index-behind-hint.d.ts.map +1 -0
- package/dist/graph/index-behind-hint.js +27 -0
- package/dist/knowledge/knowledge-stale-gate-flags.d.ts +3 -0
- package/dist/knowledge/knowledge-stale-gate-flags.d.ts.map +1 -0
- package/dist/knowledge/knowledge-stale-gate-flags.js +1 -0
- package/dist/knowledge/knowledge-stale-gate-input.d.ts +3 -0
- package/dist/knowledge/knowledge-stale-gate-input.d.ts.map +1 -0
- package/dist/knowledge/knowledge-stale-gate-input.js +1 -0
- package/dist/knowledge/knowledge-stale-gate-result.d.ts +3 -0
- package/dist/knowledge/knowledge-stale-gate-result.d.ts.map +1 -0
- package/dist/knowledge/knowledge-stale-gate-result.js +1 -0
- package/dist/knowledge/knowledge-stale-gate.d.ts +10 -0
- package/dist/knowledge/knowledge-stale-gate.d.ts.map +1 -0
- package/dist/knowledge/knowledge-stale-gate.js +9 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +200 -83
- package/dist/output/failure-hints.d.ts.map +1 -1
- package/dist/output/failure-hints.js +11 -9
- package/dist/output/rejected-entries-note.d.ts +57 -0
- package/dist/output/rejected-entries-note.d.ts.map +1 -0
- package/dist/output/rejected-entries-note.js +103 -0
- package/dist/output/stdout-is-pipe.d.ts +13 -0
- package/dist/output/stdout-is-pipe.d.ts.map +1 -0
- package/dist/output/stdout-is-pipe.js +21 -0
- package/dist/packs/stale-build-warning.d.ts +11 -0
- package/dist/packs/stale-build-warning.d.ts.map +1 -0
- package/dist/packs/stale-build-warning.js +23 -0
- package/dist/quality/run-quality.d.ts +123 -0
- package/dist/quality/run-quality.d.ts.map +1 -0
- package/dist/quality/run-quality.js +442 -0
- package/dist/schemas/json-schemas.d.ts +184 -3
- package/dist/schemas/json-schemas.d.ts.map +1 -1
- package/dist/schemas/json-schemas.js +113 -2
- package/dist/shrk.d.ts +3 -0
- package/dist/shrk.d.ts.map +1 -0
- package/dist/shrk.js +30 -0
- package/dist/surface/audience-applicability.d.ts +26 -0
- package/dist/surface/audience-applicability.d.ts.map +1 -0
- package/dist/surface/audience-applicability.js +60 -0
- package/dist/surface/catalog-command-safety.d.ts +18 -0
- package/dist/surface/catalog-command-safety.d.ts.map +1 -0
- package/dist/surface/catalog-command-safety.js +62 -0
- package/dist/surface/cli-command-resolver.d.ts +21 -0
- package/dist/surface/cli-command-resolver.d.ts.map +1 -0
- package/dist/surface/cli-command-resolver.js +87 -0
- package/dist/surface/command-dispatch-kind.d.ts +20 -0
- package/dist/surface/command-dispatch-kind.d.ts.map +1 -0
- package/dist/surface/command-dispatch-kind.js +20 -0
- package/dist/surface/command-index-entry.d.ts +40 -0
- package/dist/surface/command-index-entry.d.ts.map +1 -0
- package/dist/surface/command-index-entry.js +1 -0
- package/dist/surface/command-index.d.ts +77 -0
- package/dist/surface/command-index.d.ts.map +1 -0
- package/dist/surface/command-index.js +323 -0
- package/dist/surface/i-command-index.d.ts +22 -0
- package/dist/surface/i-command-index.d.ts.map +1 -0
- package/dist/surface/i-command-index.js +1 -0
- package/dist/surface/i-command-string-context.d.ts +20 -0
- package/dist/surface/i-command-string-context.d.ts.map +1 -0
- package/dist/surface/i-command-string-context.js +1 -0
- package/dist/surface/i-surface-denial.d.ts +13 -0
- package/dist/surface/i-surface-denial.d.ts.map +1 -0
- package/dist/surface/i-surface-denial.js +1 -0
- package/dist/surface/load-surface-context.d.ts +17 -1
- package/dist/surface/load-surface-context.d.ts.map +1 -1
- package/dist/surface/load-surface-context.js +48 -10
- package/dist/surface/not-enabled-error.d.ts +38 -4
- package/dist/surface/not-enabled-error.d.ts.map +1 -1
- package/dist/surface/not-enabled-error.js +96 -6
- package/dist/surface/profiles.d.ts +7 -0
- package/dist/surface/profiles.d.ts.map +1 -1
- package/dist/surface/resolve-command-string.d.ts +10 -0
- package/dist/surface/resolve-command-string.d.ts.map +1 -0
- package/dist/surface/resolve-command-string.js +572 -0
- package/dist/surface/surface-config-writer.d.ts +7 -2
- package/dist/surface/surface-config-writer.d.ts.map +1 -1
- package/dist/surface/surface-config-writer.js +25 -6
- package/dist/surface/surface-layer.d.ts +11 -0
- package/dist/surface/surface-layer.d.ts.map +1 -0
- package/dist/surface/surface-layer.js +11 -0
- package/dist/surface/surface-refusal-reason.d.ts +16 -0
- package/dist/surface/surface-refusal-reason.d.ts.map +1 -0
- package/dist/surface/surface-refusal-reason.js +16 -0
- package/dist/surface/surface-selector.d.ts +26 -0
- package/dist/surface/surface-selector.d.ts.map +1 -0
- package/dist/surface/surface-selector.js +42 -0
- package/dist/surface/surface-summary.d.ts +61 -18
- package/dist/surface/surface-summary.d.ts.map +1 -1
- package/dist/surface/surface-summary.js +147 -60
- package/dist/surface/tier.d.ts +85 -22
- package/dist/surface/tier.d.ts.map +1 -1
- package/dist/surface/tier.js +132 -30
- package/dist/usage/usage-log.d.ts +6 -1
- package/dist/usage/usage-log.d.ts.map +1 -1
- package/dist/usage/usage-log.js +7 -2
- package/dist/validation/run-validation-loop.d.ts +9 -0
- package/dist/validation/run-validation-loop.d.ts.map +1 -1
- package/dist/validation/run-validation-loop.js +14 -11
- package/dist/validation/typecheck-emitted.d.ts +8 -9
- package/dist/validation/typecheck-emitted.d.ts.map +1 -1
- package/dist/validation/typecheck-emitted.js +17 -97
- package/package.json +34 -34
|
@@ -1,31 +1,60 @@
|
|
|
1
|
-
import { buildAiReadinessReport, buildImportHygieneReport,
|
|
1
|
+
import { boundaryLoadIssueCoverage, boundaryLoadIssueLabel, boundaryLoadIssuesFromFile, buildAiReadinessReport, boundaryRulesCheckedNothing, boundaryRulesAcceptedEmpty, boundaryRulesEvaluated, boundarySkippedRuleRows, buildImportHygieneReport, buildPackDoctorReportAsync, buildPolyglotBoundaryReport, importHygieneCoverage, packDoctorCoverage, packDoctorVerdict, diagnoseActionHints, diffBoundaryRuleSets, emitImportHygieneAllowlistDraft, filterViolationsToChangedScope, ImportHygieneFindingKind, inspectSharkcraft, isTodoReason, renderImportHygieneText, resolveChangedFiles, resolveProjectConfig, runBoundaryCheck, doctorVerdict, DoctorVerdictKind, runDoctor, suggestBoundaryFixes, } from '@shrkcrft/inspector';
|
|
2
2
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import * as nodePath from 'node:path';
|
|
4
|
-
import { flagBool, flagNumber, flagString, flagVars, resolveCwd, } from "../command-registry.js";
|
|
4
|
+
import { firstUnknownFlag, flagBool, flagNumber, flagString, flagVars, resolveCwd, } from "../command-registry.js";
|
|
5
|
+
import { GLOBAL_FLAGS, IMPLICIT_FLAGS } from "../dispatch/global-flags.js";
|
|
6
|
+
import { unknownFlagRefusal } from "../dispatch/unknown-flag-refusal.js";
|
|
7
|
+
import { PositionalMode } from "../dispatch/positional-mode.js";
|
|
5
8
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
6
9
|
import { ExitCode } from "../exit-codes.js";
|
|
7
10
|
import { maybeRunInWatchMode } from "../output/watch-loop.js";
|
|
8
11
|
import { computeDeletedOrphans } from "../diff/deleted-orphans.js";
|
|
9
|
-
import {
|
|
12
|
+
import { deletedOrphanCoverage } from "../diff/deleted-orphan-coverage.js";
|
|
13
|
+
import { deletedOrphanScopeNotes } from "../diff/deleted-orphan-scope-notes.js";
|
|
14
|
+
import { renderWiringExplain, settleWiringExplain } from "./wiring.command.js";
|
|
15
|
+
import { runRegistryLifecycle } from "./registry-lifecycle-run.js";
|
|
10
16
|
import { validateTemplateVariables } from '@shrkcrft/templates';
|
|
11
17
|
import { FileChangeType, planGeneration } from '@shrkcrft/generator';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
18
|
+
import { explainWiring, loadBoundaryRulesFromFile, runWiring, summarizeImports, planWiringFix, planeScanExcludeDirs, readMatchingFiles, wiringGlobsOf, } from '@shrkcrft/boundaries';
|
|
19
|
+
import { coverageShortfall, formatCoverage, UnitLivenessState, } from '@shrkcrft/core';
|
|
20
|
+
import { buildGateEnvelope, } from "../gates/gate-envelope.js";
|
|
21
|
+
import { ALLOW_EMPTY_FLAG, allowEmptyValve } from "../gates/allow-empty.js";
|
|
22
|
+
import { emptyRuleAdviceLines } from "../gates/empty-rule-advice-lines.js";
|
|
23
|
+
import { qualifyCleanForUnits } from "../gates/qualify-clean-for-units.js";
|
|
24
|
+
import { unitStateNotes } from "../gates/unit-state-notes.js";
|
|
25
|
+
import { templateRegistryCoverage } from "../gates/template-registry-coverage.js";
|
|
26
|
+
import { verdictLine } from "../gates/verdict-line.js";
|
|
27
|
+
import { acceptedEmptyNote } from "../gates/accepted-empty-note.js";
|
|
28
|
+
import { settleVerdict } from "../gates/settle-verdict.js";
|
|
29
|
+
import { planeVerdictForExit } from "../gates/plane-verdict.js";
|
|
30
|
+
import { seamRejectedRules } from "../gates/seam-rejected-rules.js";
|
|
31
|
+
function knowledgeGroup(inspection, args) {
|
|
15
32
|
const dup = inspection.validationIssues.filter((i) => i.code === 'duplicate-id');
|
|
16
33
|
const missing = inspection.validationIssues.filter((i) => i.code !== 'duplicate-id');
|
|
34
|
+
const loaded = inspection.knowledgeEntries.length;
|
|
17
35
|
return {
|
|
18
36
|
name: 'knowledge',
|
|
19
37
|
passed: missing.length === 0 && dup.length === 0,
|
|
20
38
|
errors: missing.length,
|
|
21
39
|
warnings: dup.length,
|
|
40
|
+
...(loaded === 0 && missing.length === 0 ? { examinedNothing: true } : {}),
|
|
41
|
+
requestCoverage: [
|
|
42
|
+
{
|
|
43
|
+
unit: 'knowledge entries',
|
|
44
|
+
expected: loaded,
|
|
45
|
+
examined: loaded,
|
|
46
|
+
root: inspection.projectRoot,
|
|
47
|
+
reason: 'no knowledge entry is loaded',
|
|
48
|
+
...allowEmptyValve(args, loaded),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
22
51
|
details: [
|
|
23
52
|
...missing.map((m) => `error: ${m.code} on ${m.entryId}`),
|
|
24
53
|
...dup.map((m) => `warn: duplicate id ${m.entryId}`),
|
|
25
54
|
],
|
|
26
55
|
};
|
|
27
56
|
}
|
|
28
|
-
function templatesGroup(inspection) {
|
|
57
|
+
function templatesGroup(inspection, args) {
|
|
29
58
|
const details = [];
|
|
30
59
|
let errors = 0;
|
|
31
60
|
let warnings = 0;
|
|
@@ -48,10 +77,19 @@ function templatesGroup(inspection) {
|
|
|
48
77
|
passed: errors === 0,
|
|
49
78
|
errors,
|
|
50
79
|
warnings,
|
|
80
|
+
...(inspection.templates.length === 0 ? { examinedNothing: true } : {}),
|
|
81
|
+
// THE record `templates doctor` settles on (zero registered → 2).
|
|
82
|
+
requestCoverage: [
|
|
83
|
+
templateRegistryCoverage({
|
|
84
|
+
total: inspection.templates.length,
|
|
85
|
+
root: inspection.projectRoot,
|
|
86
|
+
acceptance: allowEmptyValve(args, inspection.templates.length),
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
51
89
|
details,
|
|
52
90
|
};
|
|
53
91
|
}
|
|
54
|
-
function pipelinesGroup(inspection) {
|
|
92
|
+
function pipelinesGroup(inspection, args) {
|
|
55
93
|
const details = [];
|
|
56
94
|
let errors = 0;
|
|
57
95
|
let warnings = 0;
|
|
@@ -73,22 +111,51 @@ function pipelinesGroup(inspection) {
|
|
|
73
111
|
details.push(`warn: pipeline ${p.id} has no description`);
|
|
74
112
|
}
|
|
75
113
|
}
|
|
114
|
+
const registered = inspection.pipelines.length;
|
|
76
115
|
return {
|
|
77
116
|
name: 'pipelines',
|
|
78
117
|
passed: errors === 0,
|
|
79
118
|
errors,
|
|
80
119
|
warnings,
|
|
120
|
+
...(registered === 0 ? { examinedNothing: true } : {}),
|
|
121
|
+
requestCoverage: [
|
|
122
|
+
{
|
|
123
|
+
unit: 'pipelines',
|
|
124
|
+
expected: registered,
|
|
125
|
+
examined: registered,
|
|
126
|
+
root: inspection.projectRoot,
|
|
127
|
+
reason: 'no pipeline is registered',
|
|
128
|
+
...allowEmptyValve(args, registered),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
81
131
|
details,
|
|
82
132
|
};
|
|
83
133
|
}
|
|
84
|
-
function packsGroup(inspection) {
|
|
85
|
-
|
|
134
|
+
async function packsGroup(inspection, args) {
|
|
135
|
+
// THE async doctor `packs doctor` runs — the registry loaders' rejected
|
|
136
|
+
// entries included. The sync builder never saw them, so `check packs` read
|
|
137
|
+
// `OK packs errors=0` where `packs doctor` exited 1 (round 12 review, R12-X2).
|
|
138
|
+
const report = await buildPackDoctorReportAsync(inspection);
|
|
139
|
+
// THE pack-doctor settlement (`packDoctorVerdict`, the one `shrk packs
|
|
140
|
+
// doctor` exits on): zero packs examined nothing (SKIP, never OK), and a
|
|
141
|
+
// compiled build never compared to its source is a real shortfall.
|
|
142
|
+
const verdict = packDoctorVerdict(report, inspection);
|
|
143
|
+
const compiled = report.compiledArtifactCoverage;
|
|
144
|
+
const compiledShortfall = compiled ? coverageShortfall(compiled) : undefined;
|
|
86
145
|
return {
|
|
87
146
|
name: 'packs',
|
|
88
147
|
passed: report.passed,
|
|
89
148
|
errors: report.summary.errors,
|
|
90
149
|
warnings: report.summary.warnings,
|
|
91
|
-
|
|
150
|
+
...(verdict.examinedNothing ? { examinedNothing: true } : {}),
|
|
151
|
+
...(compiled && compiledShortfall !== undefined ? { coverage: [compiled], notVerified: true } : {}),
|
|
152
|
+
// THE pack-doctor coverage — what `packs doctor` settles on, valve included
|
|
153
|
+
// (zero discovered packs → 2 when `check packs` is asked alone).
|
|
154
|
+
requestCoverage: packDoctorCoverage(report, inspection, allowEmptyValve(args, inspection.packs.discoveredPacks.length)),
|
|
155
|
+
details: [
|
|
156
|
+
...report.issues.map((i) => `${i.severity}: ${i.packageName} ${i.code} — ${i.message}`),
|
|
157
|
+
...(compiledShortfall !== undefined ? [`not verified: ${compiledShortfall}`] : []),
|
|
158
|
+
],
|
|
92
159
|
};
|
|
93
160
|
}
|
|
94
161
|
function actionHintsGroup(inspection) {
|
|
@@ -98,39 +165,88 @@ function actionHintsGroup(inspection) {
|
|
|
98
165
|
passed: true, // warnings only — they do not fail unless --strict
|
|
99
166
|
errors: 0,
|
|
100
167
|
warnings: report.issues.length,
|
|
168
|
+
// Action hints live on knowledge entries: none declared, nothing to lint.
|
|
169
|
+
...(inspection.knowledgeEntries.length === 0 ? { examinedNothing: true } : {}),
|
|
101
170
|
details: report.issues.map((i) => `warn: ${i.code} on ${i.entryId}`),
|
|
102
171
|
};
|
|
103
172
|
}
|
|
104
173
|
function doctorGroup(inspection) {
|
|
105
174
|
const result = runDoctor(inspection);
|
|
175
|
+
// THE doctor settlement (`doctorVerdict`), the reading `shrk doctor`
|
|
176
|
+
// settles its exit on: a setup it could not fully verify (a compiled pack
|
|
177
|
+
// build with no build record) is NOT VERIFIED here too, never OK.
|
|
178
|
+
const settled = doctorVerdict(result);
|
|
106
179
|
return {
|
|
107
180
|
name: 'doctor',
|
|
108
181
|
passed: result.passed,
|
|
109
182
|
errors: result.summary.errors,
|
|
110
183
|
warnings: result.summary.warnings,
|
|
184
|
+
...(result.coverage && result.coverage.length > 0 ? { coverage: result.coverage } : {}),
|
|
185
|
+
...(settled.verdict === DoctorVerdictKind.NotVerified ? { notVerified: true } : {}),
|
|
111
186
|
details: result.checks
|
|
112
187
|
.filter((c) => c.severity === 'error' || c.severity === 'warning')
|
|
113
188
|
.map((c) => `${c.severity}: ${c.title} — ${c.message}`),
|
|
114
189
|
};
|
|
115
190
|
}
|
|
116
|
-
|
|
191
|
+
/**
|
|
192
|
+
* `alone` — the invocation asked for exactly one group (`check packs`). Its
|
|
193
|
+
* request coverage is then the verdict's scope: the group's doctor record, so
|
|
194
|
+
* `check packs` over zero packs is 2 like `packs doctor` (`--allow-empty` → 0).
|
|
195
|
+
*/
|
|
196
|
+
function renderReport(args, groups, readinessLine, alone = false) {
|
|
117
197
|
const totalErrors = groups.reduce((s, g) => s + g.errors, 0);
|
|
118
198
|
const totalWarnings = groups.reduce((s, g) => s + g.warnings, 0);
|
|
119
199
|
const passed = totalErrors === 0;
|
|
120
200
|
const strict = flagBool(args, 'strict');
|
|
121
201
|
const minScore = flagNumber(args, 'min-score');
|
|
202
|
+
// Settle first, render second: a group that could not verify part of its
|
|
203
|
+
// scope (the doctor over an unrecorded compiled pack build) turns a clean
|
|
204
|
+
// proposed 0 into NOT VERIFIED (2), in text AND JSON. A group with NOTHING
|
|
205
|
+
// to validate is `shrk quality`'s aggregate rule: a deliberate skip by
|
|
206
|
+
// default (SKIP, never OK), required — so NOT VERIFIED — under `--strict`.
|
|
207
|
+
const emptyGroups = groups.filter((g) => g.examinedNothing === true);
|
|
208
|
+
const emptyUnit = {
|
|
209
|
+
knowledge: 'knowledge entries',
|
|
210
|
+
'action-hints': 'knowledge entries (action hints)',
|
|
211
|
+
};
|
|
212
|
+
const settled = settleVerdict(passed && (!strict || totalWarnings === 0) ? 0 : 1, alone
|
|
213
|
+
? groups.flatMap((g) => g.requestCoverage ?? g.coverage ?? [])
|
|
214
|
+
: [
|
|
215
|
+
...groups.flatMap((g) => g.coverage ?? []),
|
|
216
|
+
...(strict
|
|
217
|
+
? emptyGroups.map((g) => ({
|
|
218
|
+
unit: emptyUnit[g.name] ?? g.name,
|
|
219
|
+
expected: 0,
|
|
220
|
+
examined: 0,
|
|
221
|
+
subject: g.name,
|
|
222
|
+
reason: 'none declared — nothing to validate (required under --strict)',
|
|
223
|
+
}))
|
|
224
|
+
: []),
|
|
225
|
+
]);
|
|
226
|
+
const statusOf = (g) => !g.passed ? 'failed' : g.examinedNothing ? 'skipped' : g.notVerified ? 'partial' : 'passed';
|
|
122
227
|
if (flagBool(args, 'json')) {
|
|
123
228
|
process.stdout.write(asJson({
|
|
124
229
|
passed,
|
|
125
|
-
groups,
|
|
230
|
+
groups: groups.map((g) => ({ ...g, status: statusOf(g) })),
|
|
126
231
|
totals: { errors: totalErrors, warnings: totalWarnings },
|
|
127
232
|
readinessLine,
|
|
233
|
+
exitCode: settled.exit,
|
|
234
|
+
verdict: settled.verdict,
|
|
235
|
+
...(settled.shortfalls.length > 0 ? { shortfalls: settled.shortfalls } : {}),
|
|
236
|
+
...(settled.accepted.length > 0 ? { accepted: settled.accepted } : {}),
|
|
128
237
|
}) + '\n');
|
|
129
|
-
return
|
|
238
|
+
return settled.exit;
|
|
130
239
|
}
|
|
131
240
|
process.stdout.write(header('Check summary'));
|
|
241
|
+
const glyph = {
|
|
242
|
+
failed: 'FAIL ',
|
|
243
|
+
skipped: 'SKIP ',
|
|
244
|
+
partial: 'PART ',
|
|
245
|
+
passed: 'OK ',
|
|
246
|
+
};
|
|
132
247
|
for (const g of groups) {
|
|
133
|
-
|
|
248
|
+
const status = statusOf(g);
|
|
249
|
+
process.stdout.write(` ${glyph[status]} ${g.name.padEnd(16)} errors=${g.errors} warnings=${g.warnings}${status === 'skipped' ? ' (examined nothing)' : ''}\n`);
|
|
134
250
|
}
|
|
135
251
|
process.stdout.write(`\nTotals: ${totalErrors} errors, ${totalWarnings} warnings\n`);
|
|
136
252
|
if (readinessLine)
|
|
@@ -148,7 +264,13 @@ function renderReport(args, groups, readinessLine) {
|
|
|
148
264
|
}
|
|
149
265
|
const minOk = minScore === undefined ? true : true; // readiness floor already enforced by shrk doctor; only echo here
|
|
150
266
|
void minOk;
|
|
151
|
-
|
|
267
|
+
const line = verdictLine(settled, '');
|
|
268
|
+
if (line)
|
|
269
|
+
process.stdout.write(`\n${line}\n`);
|
|
270
|
+
if (alone && settled.exit === ExitCode.NotVerified && groups.some((g) => g.examinedNothing === true)) {
|
|
271
|
+
process.stdout.write(`Pass --${ALLOW_EMPTY_FLAG} to accept an empty ${groups[0]?.name ?? 'group'} set explicitly.\n`);
|
|
272
|
+
}
|
|
273
|
+
return settled.exit;
|
|
152
274
|
}
|
|
153
275
|
// ────────────────────────────────────────────────────────────────────────
|
|
154
276
|
// Subcommand: imports
|
|
@@ -157,8 +279,9 @@ async function checkImports(args) {
|
|
|
157
279
|
const cwd = resolveCwd(args);
|
|
158
280
|
const changedOnly = flagBool(args, 'changed-only');
|
|
159
281
|
const since = flagString(args, 'since');
|
|
282
|
+
const scoped = changedOnly || since !== undefined;
|
|
160
283
|
let files;
|
|
161
|
-
if (
|
|
284
|
+
if (scoped) {
|
|
162
285
|
const changed = resolveChangedFiles({
|
|
163
286
|
projectRoot: cwd,
|
|
164
287
|
...(since ? { since } : {}),
|
|
@@ -210,11 +333,37 @@ async function checkImports(args) {
|
|
|
210
333
|
}
|
|
211
334
|
return 0;
|
|
212
335
|
}
|
|
336
|
+
// Settle first, render second. The subject list is THE authority
|
|
337
|
+
// (`importHygieneSubjects`, which `buildImportHygieneReport` scanned): an
|
|
338
|
+
// EMPTY changeset — or one with no .ts/.tsx source — examined nothing, and an
|
|
339
|
+
// unreadable in-scope file was never checked. Either is NOT VERIFIED (2),
|
|
340
|
+
// never "OK"; `--allow-empty` accepts an empty scope explicitly.
|
|
341
|
+
const coverage = {
|
|
342
|
+
...importHygieneCoverage(report),
|
|
343
|
+
...((report.filesInScope ?? 0) === 0
|
|
344
|
+
? {
|
|
345
|
+
reason: scoped
|
|
346
|
+
? `no changed .ts/.tsx source in the changeset (${files?.length ?? 0} changed file(s))`
|
|
347
|
+
: 'no .ts/.tsx source under the scanned roots',
|
|
348
|
+
}
|
|
349
|
+
: {}),
|
|
350
|
+
root: cwd,
|
|
351
|
+
...allowEmptyValve(args, report.filesInScope ?? 0),
|
|
352
|
+
};
|
|
353
|
+
const settled = settleVerdict(report.verdict === 'errors' ? 1 : 0, [coverage]);
|
|
213
354
|
if (flagBool(args, 'json')) {
|
|
214
|
-
process.stdout.write(asJson(
|
|
215
|
-
|
|
355
|
+
process.stdout.write(asJson({
|
|
356
|
+
...report,
|
|
357
|
+
// The engine word, except when the run settled NOT VERIFIED.
|
|
358
|
+
verdict: settled.exit === ExitCode.NotVerified ? 'not-verified' : report.verdict,
|
|
359
|
+
coverage,
|
|
360
|
+
exitCode: settled.exit,
|
|
361
|
+
shortfalls: settled.shortfalls,
|
|
362
|
+
accepted: settled.accepted,
|
|
363
|
+
}) + '\n');
|
|
364
|
+
return settled.exit;
|
|
216
365
|
}
|
|
217
|
-
process.stdout.write(renderImportHygieneText(report));
|
|
366
|
+
process.stdout.write(renderImportHygieneText(report, settled.exit === ExitCode.NotVerified ? 'NOT VERIFIED' : undefined));
|
|
218
367
|
// When --fail-on-unexplained-allowlist is set, warn on existing
|
|
219
368
|
// entries whose reason is still TODO/empty. The scanner has already
|
|
220
369
|
// un-suppressed them, but we also surface a separate accounting line.
|
|
@@ -238,7 +387,13 @@ async function checkImports(args) {
|
|
|
238
387
|
}
|
|
239
388
|
}
|
|
240
389
|
}
|
|
241
|
-
|
|
390
|
+
const line = verdictLine(settled, '');
|
|
391
|
+
if (line)
|
|
392
|
+
process.stdout.write(`\n${line}\n`);
|
|
393
|
+
if (settled.exit === ExitCode.NotVerified && (report.filesInScope ?? 0) === 0) {
|
|
394
|
+
process.stdout.write(` Pass --${ALLOW_EMPTY_FLAG} to accept an empty changeset explicitly.\n`);
|
|
395
|
+
}
|
|
396
|
+
return settled.exit;
|
|
242
397
|
}
|
|
243
398
|
// ────────────────────────────────────────────────────────────────────────
|
|
244
399
|
// Subcommand: generation
|
|
@@ -309,7 +464,81 @@ function readChangedScopeOptions(args, cwd) {
|
|
|
309
464
|
out.includeWorktree = true;
|
|
310
465
|
return out;
|
|
311
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* Flags `check boundaries` accepts. Anything else is a typo, never an opt-in:
|
|
469
|
+
* `--rules` / `--diff-agains` used to parse as a silent `true` and the verb ran
|
|
470
|
+
* its bare form at exit 0 (round 11, 3.3#2 / 5.2).
|
|
471
|
+
*/
|
|
472
|
+
const BOUNDARY_CHECK_FLAGS = new Set([
|
|
473
|
+
'rule',
|
|
474
|
+
'rule-file',
|
|
475
|
+
'diff-against',
|
|
476
|
+
'include-comments',
|
|
477
|
+
'fail-on-dead-units',
|
|
478
|
+
'no-rule-escalation',
|
|
479
|
+
'changed-only',
|
|
480
|
+
'since',
|
|
481
|
+
'staged',
|
|
482
|
+
'files',
|
|
483
|
+
'polyglot',
|
|
484
|
+
'strict',
|
|
485
|
+
'json',
|
|
486
|
+
'fix-suggestions',
|
|
487
|
+
'watch',
|
|
488
|
+
'paths',
|
|
489
|
+
'debounce',
|
|
490
|
+
'once',
|
|
491
|
+
ALLOW_EMPTY_FLAG,
|
|
492
|
+
// Every dispatcher global (THE list) — a direct handler call may carry them.
|
|
493
|
+
...GLOBAL_FLAGS,
|
|
494
|
+
]);
|
|
495
|
+
const BOUNDARIES_CHECK_USAGE = 'shrk check boundaries [--rule <id>] [--rule-file <path> | --diff-against <path>]\n' +
|
|
496
|
+
' [--changed-only | --since <ref> | --staged | --files a,b] [--no-rule-escalation]\n' +
|
|
497
|
+
' [--include-comments] [--fail-on-dead-units] [--allow-empty] [--strict]\n' +
|
|
498
|
+
' [--fix-suggestions] [--polyglot] [--json] [--watch [--paths a,b] [--debounce N] [--once]]\n' +
|
|
499
|
+
' Evaluate every boundary rule against the import graph (tsconfig aliases resolved).\n' +
|
|
500
|
+
' --rule <id> evaluate one rule (an unknown id is a usage error)\n' +
|
|
501
|
+
' --rule-file <path> evaluate ONLY the rules in a candidate file (the boundaryFiles loader)\n' +
|
|
502
|
+
' --diff-against <path> dry run: violations a candidate rule set would ADD / REMOVE vs the active set\n' +
|
|
503
|
+
' (--strict does not apply to the diff: it settles on error violations, dead\n' +
|
|
504
|
+
' candidate scopes and --fail-on-dead-units only; a warning it adds is reported)\n' +
|
|
505
|
+
' --changed-only … report violations the changeset introduced; a changeset touching a rule\n' +
|
|
506
|
+
' source, the config or tsconfig ESCALATES those rules to the whole tree\n' +
|
|
507
|
+
' --no-rule-escalation keep escalated rules out — they are then reported unexamined (exit 2)\n' +
|
|
508
|
+
' --include-comments read imports from raw text (a commented-out import counts again)\n' +
|
|
509
|
+
' --fail-on-dead-units a selector unit that matches nothing, or a local expectEmpty marker whose\n' +
|
|
510
|
+
' target appeared (went live), fails the run (1); a pack marker never does\n' +
|
|
511
|
+
' --strict warning violations fail too, and so does a local went-live expectEmpty marker\n' +
|
|
512
|
+
' A unit marked { pattern, expectEmpty: true } whose target does not exist yet is accepted (printed).\n' +
|
|
513
|
+
' Exit: 0 verified pass · 1 violations / errored rule / stale exception / failOnEmpty rule\n' +
|
|
514
|
+
' 2 not verified (a rule checked nothing, an empty selection, a suppressed escalation) · 3 usage';
|
|
312
515
|
async function checkBoundaries(args) {
|
|
516
|
+
// `--help` / `-h` never reaches here: the dispatcher answers it (dispatch/help-intercept.ts).
|
|
517
|
+
const bad = firstUnknownFlag(args, BOUNDARY_CHECK_FLAGS);
|
|
518
|
+
if (bad !== undefined) {
|
|
519
|
+
// THE one refusal format (round 13, lane P) — the dispatcher's own wording.
|
|
520
|
+
const shown = [...BOUNDARY_CHECK_FLAGS].filter((f) => !IMPLICIT_FLAGS.has(f));
|
|
521
|
+
const refusal = unknownFlagRefusal({
|
|
522
|
+
label: 'check boundaries',
|
|
523
|
+
flags: [bad],
|
|
524
|
+
known: shown,
|
|
525
|
+
accepts: shown,
|
|
526
|
+
...(args.argv !== undefined ? { argv: args.argv } : {}),
|
|
527
|
+
exitCode: ExitCode.UsageError,
|
|
528
|
+
});
|
|
529
|
+
process.stderr.write(refusal.message);
|
|
530
|
+
return refusal.exitCode;
|
|
531
|
+
}
|
|
532
|
+
for (const flag of ['rule', 'rule-file', 'diff-against']) {
|
|
533
|
+
if (args.flags.has(flag) && !flagString(args, flag)) {
|
|
534
|
+
process.stderr.write(`--${flag} needs a value.\n${BOUNDARIES_CHECK_USAGE}\n`);
|
|
535
|
+
return ExitCode.UsageError;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (args.flags.has('rule-file') && args.flags.has('diff-against')) {
|
|
539
|
+
process.stderr.write('--rule-file and --diff-against are exclusive: --rule-file evaluates a candidate alone, --diff-against compares it with the active rules.\n');
|
|
540
|
+
return ExitCode.UsageError;
|
|
541
|
+
}
|
|
313
542
|
const watchExit = await maybeRunInWatchMode(args, checkBoundariesOnce, {
|
|
314
543
|
defaultPaths: BOUNDARIES_DEFAULT_WATCH_PATHS,
|
|
315
544
|
});
|
|
@@ -325,126 +554,229 @@ const BOUNDARIES_DEFAULT_WATCH_PATHS = [
|
|
|
325
554
|
'src',
|
|
326
555
|
'tools',
|
|
327
556
|
];
|
|
557
|
+
/**
|
|
558
|
+
* Load a `--rule-file` / `--diff-against` candidate through THE boundary rule
|
|
559
|
+
* loader (the one `boundaryFiles` use — it transpiles TS on the fly, so a rule
|
|
560
|
+
* edit is testable without a build). A missing file, one that throws, or one
|
|
561
|
+
* with no valid rule is a usage error; invalid rules next to valid ones become
|
|
562
|
+
* errored-rule rows.
|
|
563
|
+
*/
|
|
564
|
+
async function loadBoundaryRuleFileArg(cwd, projectRoot, raw, flag) {
|
|
565
|
+
const abs = nodePath.resolve(cwd, raw);
|
|
566
|
+
if (!existsSync(abs))
|
|
567
|
+
return { ok: false, message: `--${flag}: ${abs} does not exist.` };
|
|
568
|
+
const loaded = await loadBoundaryRulesFromFile(abs);
|
|
569
|
+
if (loaded.loadError !== undefined) {
|
|
570
|
+
return { ok: false, message: `--${flag}: ${abs} failed to load — ${loaded.loadError}` };
|
|
571
|
+
}
|
|
572
|
+
const loadIssues = boundaryLoadIssuesFromFile(loaded, projectRoot, 'rule-file');
|
|
573
|
+
if (loaded.rules.length === 0) {
|
|
574
|
+
const detail = loadIssues.map((i) => `${boundaryLoadIssueLabel(i)}: ${i.issues.join('; ')}`).join(' | ');
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
message: `--${flag}: ${abs} defines no valid boundary rule${detail ? ` (${detail})` : ''}.`,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return { ok: true, path: abs, rules: loaded.rules, loadIssues };
|
|
581
|
+
}
|
|
582
|
+
/** The envelope row an errored (never-evaluated) boundary rule becomes. */
|
|
583
|
+
function boundaryLoadIssueRow(issue) {
|
|
584
|
+
return {
|
|
585
|
+
id: boundaryLoadIssueLabel(issue),
|
|
586
|
+
type: 'boundary',
|
|
587
|
+
status: 'error',
|
|
588
|
+
severity: 'error',
|
|
589
|
+
counts: {},
|
|
590
|
+
violations: [],
|
|
591
|
+
error: `${issue.file}: ${issue.kind} — ${issue.issues.join('; ')}`,
|
|
592
|
+
coverage: boundaryLoadIssueCoverage(issue),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/** One envelope row per selected rule (+ one errored row per load issue). */
|
|
596
|
+
function boundaryGateRules(result) {
|
|
597
|
+
return [
|
|
598
|
+
...result.rules.map((r) => ({
|
|
599
|
+
id: r.ruleId,
|
|
600
|
+
type: 'boundary',
|
|
601
|
+
status: r.status,
|
|
602
|
+
severity: r.severity === 'error' ? 'error' : 'warning',
|
|
603
|
+
counts: {
|
|
604
|
+
filesInScope: r.detail.filesInScope,
|
|
605
|
+
edgesInScope: r.detail.edgesInScope,
|
|
606
|
+
violations: r.violations.length,
|
|
607
|
+
suppressed: r.detail.suppressed,
|
|
608
|
+
},
|
|
609
|
+
violations: [
|
|
610
|
+
...r.violations.map((v) => ({
|
|
611
|
+
id: v.importSpecifier,
|
|
612
|
+
file: v.file,
|
|
613
|
+
line: v.line,
|
|
614
|
+
message: v.message,
|
|
615
|
+
...(v.suggestedFix ? { hint: v.suggestedFix } : {}),
|
|
616
|
+
})),
|
|
617
|
+
...r.staleExceptions.map((s) => ({ id: `exceptions[${s.index}]`, file: s.file, message: s.message })),
|
|
618
|
+
],
|
|
619
|
+
...(r.skipReason ? { skipReason: r.skipReason } : {}),
|
|
620
|
+
coverage: r.coverage,
|
|
621
|
+
// Round 13: the rule's expectEmpty acceptance (record B) — folded by the
|
|
622
|
+
// envelope into the ONE settle, the same records the orchestrator
|
|
623
|
+
// settled (`boundaryCheckCoverage`) — and its unit lines.
|
|
624
|
+
...(r.detail.unitAcceptance ? { unitAcceptance: r.detail.unitAcceptance } : {}),
|
|
625
|
+
...(r.detail.units ? { units: r.detail.units } : {}),
|
|
626
|
+
})),
|
|
627
|
+
...result.loadIssues.map(boundaryLoadIssueRow),
|
|
628
|
+
];
|
|
629
|
+
}
|
|
630
|
+
/** The polyglot engine's contribution to a `--polyglot` run (its own section + one envelope row). */
|
|
631
|
+
function buildPolyglotSection(cwd, changedScope) {
|
|
632
|
+
const report = buildPolyglotBoundaryReport({ projectRoot: cwd });
|
|
633
|
+
const filtered = changedScope ? filterViolationsToChangedScope(report.violations, changedScope) : null;
|
|
634
|
+
const violations = filtered ? filtered.includedViolations : report.violations;
|
|
635
|
+
const errors = violations.filter((v) => v.severity === 'error').length;
|
|
636
|
+
const warnings = violations.filter((v) => v.severity === 'warning').length;
|
|
637
|
+
// No polyglot language detected → the engine evaluated nothing: no row (a
|
|
638
|
+
// row would claim an evaluation), and the run's coverage says so.
|
|
639
|
+
const rows = report.counts.rules > 0
|
|
640
|
+
? [
|
|
641
|
+
{
|
|
642
|
+
id: 'polyglot',
|
|
643
|
+
type: 'boundary',
|
|
644
|
+
status: errors > 0 ? 'failed' : 'passed',
|
|
645
|
+
severity: 'error',
|
|
646
|
+
counts: { rules: report.counts.rules, edges: report.counts.edges, violations: violations.length },
|
|
647
|
+
violations: violations.map((v) => ({
|
|
648
|
+
id: v.importSpecifier,
|
|
649
|
+
file: v.fromFile,
|
|
650
|
+
message: `[${v.ruleId}] ${v.suggestedFix}`,
|
|
651
|
+
})),
|
|
652
|
+
coverage: { unit: 'polyglot rules', expected: report.counts.rules, examined: report.counts.rules },
|
|
653
|
+
},
|
|
654
|
+
]
|
|
655
|
+
: [];
|
|
656
|
+
return { report, filtered, violations, errors, warnings, rows };
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* The exit-0 sentence handed to `verdictLine`. The `✓` form ONLY when nothing
|
|
660
|
+
* was reported above: a warning / info violation or a dead selector unit (a
|
|
661
|
+
* pattern that can never fire — the rule it sits in enforces less than it
|
|
662
|
+
* says) is named on the verdict line instead, never followed by a ✓ —
|
|
663
|
+
* policy-lint's convention for warnings.
|
|
664
|
+
*/
|
|
665
|
+
function boundaryCleanSentence(result, polyglotWarnings) {
|
|
666
|
+
const scope = result.changed ? ' introduced by changed files' : '';
|
|
667
|
+
const warnings = result.counts.warning + polyglotWarnings;
|
|
668
|
+
const info = result.counts.info;
|
|
669
|
+
const dead = result.deadUnits.length;
|
|
670
|
+
// Round 13: a LOCAL went-live expectEmpty marker is drift — reported above,
|
|
671
|
+
// never under a ✓. A PACK marker that went live is INFO (its own block above
|
|
672
|
+
// says so: the consumer cannot edit it), so it withholds nothing — the
|
|
673
|
+
// sibling verdicts (`gates coverage`, the asset doctors) count local markers
|
|
674
|
+
// only. An intended-empty unit is not drift: its acceptance prints below the ✓.
|
|
675
|
+
const wentLive = result.wentLive.filter((u) => u.packageName === undefined).length;
|
|
676
|
+
if (warnings === 0 && info === 0 && dead === 0 && wentLive === 0) {
|
|
677
|
+
return `Verdict: OK — no boundary violations${scope}. ✓`;
|
|
678
|
+
}
|
|
679
|
+
const parts = [];
|
|
680
|
+
if (warnings > 0)
|
|
681
|
+
parts.push(`${warnings} warning(s)`);
|
|
682
|
+
if (info > 0)
|
|
683
|
+
parts.push(`${info} info finding(s)`);
|
|
684
|
+
if (dead > 0)
|
|
685
|
+
parts.push(`${dead} dead selector unit(s)`);
|
|
686
|
+
const lead = warnings + info > 0 ? `no blocking boundary violations${scope}` : `no boundary violations${scope}`;
|
|
687
|
+
const clauses = [];
|
|
688
|
+
if (parts.length > 0) {
|
|
689
|
+
const hint = dead > 0 ? ' (--fail-on-dead-units to fail on dead units)' : '';
|
|
690
|
+
clauses.push(`${parts.join(', ')} reported above${hint}`);
|
|
691
|
+
}
|
|
692
|
+
if (wentLive > 0)
|
|
693
|
+
clauses.push(`${wentLive} expectEmpty unit(s) went live (remove each stale expectEmpty marker)`);
|
|
694
|
+
return `Verdict: ${lead} — ${clauses.join('; ')}.`;
|
|
695
|
+
}
|
|
696
|
+
/** The lead sentence of a NOT VERIFIED boundary verdict — says WHY nothing (or not everything) was proved. */
|
|
697
|
+
function boundaryNotVerifiedLead(result, env) {
|
|
698
|
+
if (result.rulesConfigured === 0 && result.loadIssues.length === 0) {
|
|
699
|
+
return 'Verdict: NOT VERIFIED — 0 boundary rules loaded, so nothing was checked.';
|
|
700
|
+
}
|
|
701
|
+
if (result.changed && (result.changed.escalationSuppressed.length ?? 0) > 0) {
|
|
702
|
+
return `Verdict: NOT VERIFIED — the changeset edits the definition of ${result.changed.escalationSuppressed.length} rule(s) and --no-rule-escalation kept them out: changed-only cannot see a rule edit.`;
|
|
703
|
+
}
|
|
704
|
+
if (result.changed && env.coverage.expected === 0) {
|
|
705
|
+
return 'Verdict: NOT VERIFIED — no changed source file is governed by a boundary rule; nothing was checked.';
|
|
706
|
+
}
|
|
707
|
+
const skipped = boundaryRulesCheckedNothing(result);
|
|
708
|
+
const first = skipped[0];
|
|
709
|
+
if (first) {
|
|
710
|
+
return `Verdict: NOT VERIFIED — ${skipped.length} of ${result.rules.length} rules checked nothing (${first.ruleId}: ${first.skipReason ?? 'no governed file'})`;
|
|
711
|
+
}
|
|
712
|
+
return "Verdict: NOT VERIFIED — part of a rule's scope was never examined.";
|
|
713
|
+
}
|
|
328
714
|
async function checkBoundariesOnce(args) {
|
|
329
715
|
const cwd = resolveCwd(args);
|
|
716
|
+
const wantJson = flagBool(args, 'json');
|
|
330
717
|
const inspection = await inspectSharkcraft({ cwd });
|
|
331
|
-
const rules = inspection.boundaryRegistry.list();
|
|
332
718
|
const ruleFilter = flagString(args, 'rule');
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
? filterViolationsToChangedScope(polyglotReport.violations, changedScope)
|
|
344
|
-
: null;
|
|
345
|
-
const polyglotViolations = polyglotFiltered
|
|
346
|
-
? polyglotFiltered.includedViolations
|
|
347
|
-
: polyglotReport.violations;
|
|
348
|
-
const polyglotErrors = polyglotViolations.filter((v) => v.severity === 'error').length;
|
|
349
|
-
const polyglotWarnings = polyglotViolations.filter((v) => v.severity === 'warning').length;
|
|
350
|
-
if (flagBool(args, 'json')) {
|
|
351
|
-
// We still run the TS engine when rules exist, but always tack on the
|
|
352
|
-
// polyglot report so CI/integrators get both surfaces.
|
|
353
|
-
const tsScan = rules.length > 0 ? scanImports({ projectRoot: cwd }) : null;
|
|
354
|
-
const tsEval = tsScan ? evaluateBoundaries(tsScan, rules, {
|
|
355
|
-
...(ruleFilter ? { onlyRuleId: ruleFilter } : {}),
|
|
356
|
-
}) : null;
|
|
357
|
-
const tsFiltered = tsEval && changedScope
|
|
358
|
-
? filterViolationsToChangedScope(tsEval.violations, changedScope)
|
|
359
|
-
: null;
|
|
360
|
-
const tsViolations = tsFiltered ? tsFiltered.includedViolations : tsEval?.violations ?? [];
|
|
361
|
-
const tsErrors = tsViolations.filter((v) => v.severity === 'error').length;
|
|
362
|
-
process.stdout.write(asJson({
|
|
363
|
-
polyglot: {
|
|
364
|
-
counts: { errors: polyglotErrors, warnings: polyglotWarnings, rules: polyglotReport.counts.rules },
|
|
365
|
-
languages: polyglotReport.languages,
|
|
366
|
-
violations: polyglotViolations,
|
|
367
|
-
},
|
|
368
|
-
typescript: tsEval ? {
|
|
369
|
-
counts: { ...tsEval.counts, error: tsErrors },
|
|
370
|
-
violations: tsViolations,
|
|
371
|
-
} : null,
|
|
372
|
-
...(changedScope
|
|
373
|
-
? {
|
|
374
|
-
changedScope: {
|
|
375
|
-
mode: polyglotFiltered?.mode ?? ChangedScopeMode.ChangedOnly,
|
|
376
|
-
changedFiles: polyglotFiltered?.changedFiles ?? [],
|
|
377
|
-
ignoredLegacyCount: (polyglotFiltered?.ignoredLegacyCount ?? 0) + (tsFiltered?.ignoredLegacyCount ?? 0),
|
|
378
|
-
ignoredLegacyByRule: {
|
|
379
|
-
...(polyglotFiltered?.ignoredLegacyByRule ?? {}),
|
|
380
|
-
...(tsFiltered?.ignoredLegacyByRule ?? {}),
|
|
381
|
-
},
|
|
382
|
-
},
|
|
383
|
-
}
|
|
384
|
-
: {}),
|
|
385
|
-
}) + '\n');
|
|
386
|
-
return polyglotErrors > 0 || tsErrors > 0 ? 1 : 0;
|
|
387
|
-
}
|
|
388
|
-
process.stdout.write(header('Polyglot boundaries'));
|
|
389
|
-
process.stdout.write(kv('languages', polyglotReport.languages.join(', ') || '(none)') + '\n');
|
|
390
|
-
process.stdout.write(kv('rules', String(polyglotReport.counts.rules)) + '\n');
|
|
391
|
-
process.stdout.write(kv('violations', `${polyglotErrors} errors, ${polyglotWarnings} warnings`) + '\n');
|
|
392
|
-
if (polyglotFiltered) {
|
|
393
|
-
process.stdout.write(kv('mode', polyglotFiltered.mode) + '\n');
|
|
394
|
-
process.stdout.write(kv('changed files', String(polyglotFiltered.changedFiles.length)) + '\n');
|
|
395
|
-
process.stdout.write(kv('legacy ignored', String(polyglotFiltered.ignoredLegacyCount)) + '\n');
|
|
396
|
-
}
|
|
397
|
-
if (polyglotViolations.length > 0) {
|
|
398
|
-
for (const v of polyglotViolations) {
|
|
399
|
-
process.stdout.write(` ${v.severity.toUpperCase().padEnd(8)} ${v.ruleId} ${v.fromFile}\n`);
|
|
400
|
-
process.stdout.write(` import: "${v.importSpecifier}"\n`);
|
|
401
|
-
process.stdout.write(` ↳ ${v.suggestedFix}\n`);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
else if (polyglotFiltered) {
|
|
405
|
-
process.stdout.write(' No boundary violations introduced by changed files.\n');
|
|
406
|
-
}
|
|
407
|
-
if (polyglotErrors > 0)
|
|
408
|
-
return 1;
|
|
409
|
-
process.stdout.write('\n');
|
|
410
|
-
// Fall through to the TS engine below so callers see both views.
|
|
411
|
-
}
|
|
412
|
-
if (rules.length === 0) {
|
|
413
|
-
if (flagBool(args, 'json')) {
|
|
414
|
-
process.stdout.write(asJson({ rules: 0, violations: [], note: 'no boundary rules configured' }) + '\n');
|
|
415
|
-
return 0;
|
|
719
|
+
const diffAgainst = flagString(args, 'diff-against');
|
|
720
|
+
if (diffAgainst)
|
|
721
|
+
return checkBoundaryRuleDiff(args, inspection, diffAgainst);
|
|
722
|
+
let ruleFile;
|
|
723
|
+
const ruleFileArg = flagString(args, 'rule-file');
|
|
724
|
+
if (ruleFileArg) {
|
|
725
|
+
const loaded = await loadBoundaryRuleFileArg(cwd, inspection.projectRoot, ruleFileArg, 'rule-file');
|
|
726
|
+
if (!loaded.ok) {
|
|
727
|
+
process.stderr.write(`${loaded.message}\n`);
|
|
728
|
+
return ExitCode.UsageError;
|
|
416
729
|
}
|
|
417
|
-
|
|
418
|
-
process.stdout.write(' No boundary rules configured. Add `sharkcraft/boundaries.ts` or install a pack with `boundaryFiles`.\n');
|
|
419
|
-
return 0;
|
|
730
|
+
ruleFile = loaded;
|
|
420
731
|
}
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
732
|
+
const changedScope = readChangedScopeOptions(args, cwd);
|
|
733
|
+
// THE boundary orchestrator — the same call finish, diff-check, quality and
|
|
734
|
+
// both MCP boundary tools make, so none of them can disagree with this.
|
|
735
|
+
const result = runBoundaryCheck(inspection, {
|
|
424
736
|
...(ruleFilter ? { onlyRuleId: ruleFilter } : {}),
|
|
425
|
-
...(
|
|
737
|
+
...(ruleFile ? { ruleFile } : {}),
|
|
738
|
+
...(changedScope ? { changedScope } : {}),
|
|
739
|
+
includeComments: flagBool(args, 'include-comments'),
|
|
740
|
+
escalate: !flagBool(args, 'no-rule-escalation'),
|
|
741
|
+
failOnDeadUnits: flagBool(args, 'fail-on-dead-units'),
|
|
742
|
+
strict: flagBool(args, 'strict'),
|
|
426
743
|
});
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
744
|
+
if (result.unknownRuleId !== undefined) {
|
|
745
|
+
const ids = result.availableRuleIds ?? [];
|
|
746
|
+
const env = buildGateEnvelope('check boundaries', ExitCode.UsageError, [], result.runCoverage);
|
|
747
|
+
if (wantJson) {
|
|
748
|
+
process.stdout.write(asJson({
|
|
749
|
+
schema: result.schema,
|
|
750
|
+
ok: false,
|
|
751
|
+
error: 'unknown-rule',
|
|
752
|
+
ruleId: result.unknownRuleId,
|
|
753
|
+
available: ids,
|
|
754
|
+
verdict: planeVerdictForExit(env.exit),
|
|
755
|
+
exitCode: env.exit,
|
|
756
|
+
gate: env,
|
|
757
|
+
}) + '\n');
|
|
758
|
+
return env.exit;
|
|
440
759
|
}
|
|
441
|
-
:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
760
|
+
process.stderr.write(`No boundary rule "${result.unknownRuleId}". ${ruleFile ? `Rules in ${ruleFile.path}` : 'Configured rules'}: ${ids.length > 0 ? ids.join(', ') : '(none)'}\n`);
|
|
761
|
+
return env.exit;
|
|
762
|
+
}
|
|
763
|
+
// Settle first, render second: one envelope for text AND JSON.
|
|
764
|
+
const polyglot = flagBool(args, 'polyglot') ? buildPolyglotSection(cwd, changedScope) : undefined;
|
|
765
|
+
const polyRows = polyglot?.rows.length ?? 0;
|
|
766
|
+
const ruleResults = [...boundaryGateRules(result), ...(polyglot?.rows ?? [])];
|
|
767
|
+
const expected = result.runCoverage.expected + polyRows;
|
|
768
|
+
const runCoverage = {
|
|
769
|
+
...result.runCoverage,
|
|
770
|
+
expected,
|
|
771
|
+
examined: result.runCoverage.examined + polyRows,
|
|
772
|
+
...allowEmptyValve(args, expected),
|
|
773
|
+
};
|
|
774
|
+
const proposed = polyglot && polyglot.errors > 0 ? ExitCode.Failure : result.proposedExit;
|
|
775
|
+
const env = buildGateEnvelope('check boundaries', proposed, ruleResults, runCoverage);
|
|
776
|
+
const exit = env.exit;
|
|
445
777
|
const wantFixSuggestions = flagBool(args, 'fix-suggestions');
|
|
446
778
|
const fixSuggestions = wantFixSuggestions
|
|
447
|
-
? suggestBoundaryFixes(inspection,
|
|
779
|
+
? suggestBoundaryFixes(inspection, result.violations.map((v) => ({
|
|
448
780
|
ruleId: v.ruleId,
|
|
449
781
|
file: v.file,
|
|
450
782
|
line: v.line,
|
|
@@ -452,63 +784,223 @@ async function checkBoundariesOnce(args) {
|
|
|
452
784
|
...(v.suggestedFix ? { suggestedFix: v.suggestedFix } : {}),
|
|
453
785
|
})))
|
|
454
786
|
: [];
|
|
455
|
-
|
|
787
|
+
const suppressedExempt = result.suppressed.filter((s) => s.reason === 'exempt-file').length;
|
|
788
|
+
const suppressedException = result.suppressed.length - suppressedExempt;
|
|
789
|
+
// THE predicate (`boundaryRuleCheckedNothing`, via the orchestrator's
|
|
790
|
+
// helpers MCP `check_boundaries` and `diff-check` read too): a failOnEmpty
|
|
791
|
+
// rule's row is `failed`, yet it checked nothing and is never counted as
|
|
792
|
+
// evaluated; a rule whose governed files went unread is PARTIAL, never
|
|
793
|
+
// listed as "checked nothing".
|
|
794
|
+
const skippedRules = boundaryRulesCheckedNothing(result);
|
|
795
|
+
const evaluatedRules = boundaryRulesEvaluated(result);
|
|
796
|
+
// Round 13 (K6): a rule accepted as intended-empty examined 0 files — THE
|
|
797
|
+
// count above leaves it out, and it is printed apart, never dropped.
|
|
798
|
+
const acceptedEmptyRules = boundaryRulesAcceptedEmpty(result);
|
|
799
|
+
if (wantJson) {
|
|
456
800
|
process.stdout.write(asJson({
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
801
|
+
schema: result.schema,
|
|
802
|
+
passed: exit === ExitCode.VerifiedPass,
|
|
803
|
+
verdict: planeVerdictForExit(exit, result.counts.warning > 0 ? 'warnings' : undefined),
|
|
804
|
+
exitCode: exit,
|
|
805
|
+
ruleSource: result.ruleSource,
|
|
806
|
+
rulesConfigured: result.rulesConfigured,
|
|
807
|
+
rulesSelected: result.selectedRuleIds.length,
|
|
808
|
+
// Rules that examined at least one governed file — never a skipped one,
|
|
809
|
+
// never one accepted as intended-empty (counted in rulesAcceptedEmpty).
|
|
810
|
+
rulesEvaluated: evaluatedRules,
|
|
811
|
+
rulesAcceptedEmpty: acceptedEmptyRules.length,
|
|
812
|
+
edgesEvaluated: result.scan.edges.length,
|
|
813
|
+
zone: result.scan.zone ?? 'code',
|
|
814
|
+
counts: result.counts,
|
|
815
|
+
violations: result.violations,
|
|
816
|
+
suppressed: result.suppressed,
|
|
817
|
+
suppressedCounts: { exemptFile: suppressedExempt, exception: suppressedException },
|
|
818
|
+
staleExceptions: result.staleExceptions,
|
|
819
|
+
skipped: boundarySkippedRuleRows(result),
|
|
820
|
+
deadUnits: result.deadUnits,
|
|
821
|
+
// Round 13: every expectEmpty unit's state — accepted (intended empty),
|
|
822
|
+
// stale (went live) — the units that fail this run, and the settled
|
|
823
|
+
// acceptances (at exit 0; `gate.accepted` carries the same lines).
|
|
824
|
+
intendedEmpty: result.intendedEmpty,
|
|
825
|
+
wentLive: result.wentLive,
|
|
826
|
+
failingUnits: result.failingUnits,
|
|
827
|
+
accepted: env.accepted,
|
|
828
|
+
coverage: result.rules.map((r) => r.detail),
|
|
829
|
+
// Files the scan matched but could not read — each is folded into the
|
|
830
|
+
// coverage of every rule whose scope it is in (never "examined").
|
|
831
|
+
unreadFiles: result.scan.unread ?? [],
|
|
832
|
+
loadIssues: result.loadIssues,
|
|
833
|
+
...(result.configuration ? { configuration: result.configuration } : {}),
|
|
834
|
+
...(result.rulesConfigured === 0 && result.loadIssues.length === 0
|
|
835
|
+
? { rules: 0, note: 'no boundary rules configured' }
|
|
836
|
+
: {}),
|
|
837
|
+
importGraph: summarizeImports(result.scan),
|
|
463
838
|
...(wantFixSuggestions ? { fixSuggestions } : {}),
|
|
464
|
-
...(
|
|
839
|
+
...(result.changed
|
|
465
840
|
? {
|
|
466
841
|
changedScope: {
|
|
467
|
-
mode:
|
|
468
|
-
changedFiles:
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
842
|
+
mode: result.changed.mode,
|
|
843
|
+
changedFiles: result.changed.changedFiles,
|
|
844
|
+
governedFiles: result.changed.governedFiles,
|
|
845
|
+
includedViolations: result.violations,
|
|
846
|
+
ignoredLegacyCount: result.changed.ignoredLegacyCount,
|
|
847
|
+
ignoredLegacyByRule: result.changed.ignoredLegacyByRule,
|
|
848
|
+
escalation: {
|
|
849
|
+
ruleIds: result.changed.escalatedRuleIds,
|
|
850
|
+
reasons: result.changed.escalation.reasons,
|
|
851
|
+
},
|
|
852
|
+
escalationSuppressed: result.changed.escalationSuppressed,
|
|
853
|
+
},
|
|
854
|
+
}
|
|
855
|
+
: {}),
|
|
856
|
+
...(polyglot
|
|
857
|
+
? {
|
|
858
|
+
polyglot: {
|
|
859
|
+
counts: { errors: polyglot.errors, warnings: polyglot.warnings, rules: polyglot.report.counts.rules },
|
|
860
|
+
languages: polyglot.report.languages,
|
|
861
|
+
violations: polyglot.violations,
|
|
472
862
|
},
|
|
473
863
|
}
|
|
474
864
|
: {}),
|
|
865
|
+
gate: env,
|
|
475
866
|
}) + '\n');
|
|
476
|
-
return
|
|
867
|
+
return exit;
|
|
477
868
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
869
|
+
if (polyglot) {
|
|
870
|
+
process.stdout.write(header('Polyglot boundaries'));
|
|
871
|
+
process.stdout.write(kv('languages', polyglot.report.languages.join(', ') || '(none)') + '\n');
|
|
872
|
+
process.stdout.write(kv('rules', String(polyglot.report.counts.rules)) + '\n');
|
|
873
|
+
process.stdout.write(kv('violations', `${polyglot.errors} errors, ${polyglot.warnings} warnings`) + '\n');
|
|
874
|
+
if (polyglot.filtered) {
|
|
875
|
+
process.stdout.write(kv('mode', polyglot.filtered.mode) + '\n');
|
|
876
|
+
process.stdout.write(kv('changed files', String(polyglot.filtered.changedFiles.length)) + '\n');
|
|
877
|
+
process.stdout.write(kv('legacy ignored', String(polyglot.filtered.ignoredLegacyCount)) + '\n');
|
|
878
|
+
}
|
|
879
|
+
if (polyglot.report.counts.rules === 0) {
|
|
880
|
+
process.stdout.write(' (no polyglot language detected — the polyglot engine evaluated nothing)\n');
|
|
881
|
+
}
|
|
882
|
+
for (const v of polyglot.violations) {
|
|
883
|
+
process.stdout.write(` ${v.severity.toUpperCase().padEnd(8)} ${v.ruleId} ${v.fromFile}\n`);
|
|
884
|
+
process.stdout.write(` import: "${v.importSpecifier}"\n`);
|
|
885
|
+
process.stdout.write(` ↳ ${v.suggestedFix}\n`);
|
|
886
|
+
}
|
|
887
|
+
process.stdout.write('\n');
|
|
487
888
|
}
|
|
488
|
-
process.stdout.write('
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
889
|
+
process.stdout.write(header('Boundaries'));
|
|
890
|
+
const ruleParts = [
|
|
891
|
+
`${result.rulesConfigured} configured${result.ruleSource.kind === 'rule-file' ? ` in ${result.ruleSource.path} (--rule-file)` : ''}`,
|
|
892
|
+
];
|
|
893
|
+
if (result.changed)
|
|
894
|
+
ruleParts.push(`${result.selectedRuleIds.length} in the changed scope`);
|
|
895
|
+
ruleParts.push(`${evaluatedRules} evaluated`);
|
|
896
|
+
if (acceptedEmptyRules.length > 0)
|
|
897
|
+
ruleParts.push(`${acceptedEmptyRules.length} accepted as intended-empty`);
|
|
898
|
+
if (skippedRules.length > 0)
|
|
899
|
+
ruleParts.push(`${skippedRules.length} checked nothing`);
|
|
900
|
+
if (result.loadIssues.length > 0)
|
|
901
|
+
ruleParts.push(`${result.loadIssues.length} errored (NOT evaluated)`);
|
|
902
|
+
process.stdout.write(kv('rules', ruleParts.join(', ')) + '\n');
|
|
903
|
+
process.stdout.write(kv('files scanned', String(result.scan.filesScanned)) + '\n');
|
|
904
|
+
process.stdout.write(kv('imports', `${result.scan.edges.length}${result.scan.zone === 'all' ? ' (raw text — comments included)' : ''}`) + '\n');
|
|
905
|
+
process.stdout.write(kv('violations', `${result.counts.error} errors, ${result.counts.warning} warnings, ${result.counts.info} info` +
|
|
906
|
+
(result.suppressed.length > 0
|
|
907
|
+
? `, ${result.suppressed.length} suppressed (${suppressedExempt} exempt-file, ${suppressedException} exception)`
|
|
908
|
+
: '')) + '\n');
|
|
909
|
+
if (result.changed) {
|
|
910
|
+
process.stdout.write(kv('mode', result.changed.mode) + '\n');
|
|
911
|
+
process.stdout.write(kv('changed files', `${result.changed.changedFiles.length} (${result.changed.governedFiles.length} governed by a rule)`) + '\n');
|
|
912
|
+
process.stdout.write(kv('legacy ignored', String(result.changed.ignoredLegacyCount)) + '\n');
|
|
913
|
+
if (result.changed.escalatedRuleIds.length > 0) {
|
|
914
|
+
const why = result.changed.escalation.reasons.map((r) => `${r.file} changed (${r.kind})`).join('; ');
|
|
915
|
+
process.stdout.write(kv('escalated', `${result.changed.escalatedRuleIds.length} rule(s) — ${why}: evaluated against the whole tree`) + '\n');
|
|
492
916
|
}
|
|
493
|
-
|
|
494
|
-
process.stdout.write('
|
|
917
|
+
if (result.changed.escalationSuppressed.length > 0) {
|
|
918
|
+
process.stdout.write(kv('escalation', `SUPPRESSED by --no-rule-escalation for ${result.changed.escalationSuppressed.length} rule(s) — the violations their edit created are NOT examined`) + '\n');
|
|
495
919
|
}
|
|
496
|
-
return 0;
|
|
497
920
|
}
|
|
498
|
-
|
|
921
|
+
const config = result.configuration;
|
|
922
|
+
if (config &&
|
|
923
|
+
(!config.configured || config.unlistedDefaultFile !== undefined || config.missingListedFiles.length > 0)) {
|
|
924
|
+
process.stdout.write(kv('sharkcraft dir', config.sharkcraftDir ?? '(none found)') + '\n');
|
|
925
|
+
for (const d of config.diagnostics)
|
|
926
|
+
process.stdout.write(` ! ${d}\n`);
|
|
927
|
+
}
|
|
928
|
+
process.stdout.write('\n');
|
|
929
|
+
for (const v of result.violations) {
|
|
499
930
|
const tag = v.severity.toUpperCase().padEnd(8);
|
|
500
931
|
process.stdout.write(` ${tag} ${v.ruleId.padEnd(28)} ${v.file}:${v.line}\n`);
|
|
501
932
|
process.stdout.write(` import: "${v.importSpecifier}"\n`);
|
|
502
|
-
if (v.matchedForbidden) {
|
|
933
|
+
if (v.matchedForbidden && v.matchKind === 'subpath') {
|
|
934
|
+
process.stdout.write(` matched forbidden package: ${v.matchedForbidden} (subpath import — bare patterns cover subpaths; set forbiddenMatch: 'exact' for entrypoint-only)\n`);
|
|
935
|
+
}
|
|
936
|
+
else if (v.matchedForbidden) {
|
|
503
937
|
process.stdout.write(` matched forbidden pattern: ${v.matchedForbidden}\n`);
|
|
504
938
|
}
|
|
505
939
|
else if (v.notAllowed) {
|
|
506
940
|
process.stdout.write(` not in allowed list for ${v.ruleId}\n`);
|
|
507
941
|
}
|
|
942
|
+
if (v.resolvedVia)
|
|
943
|
+
process.stdout.write(` resolved via tsconfig alias: ${v.resolvedVia}\n`);
|
|
508
944
|
process.stdout.write(` ${v.message}\n`);
|
|
509
945
|
if (v.suggestedFix)
|
|
510
946
|
process.stdout.write(` ↳ ${v.suggestedFix}\n`);
|
|
511
947
|
}
|
|
948
|
+
for (const issue of result.loadIssues) {
|
|
949
|
+
const what = issue.kind === 'invalid-rule'
|
|
950
|
+
? `rule '${issue.ruleId ?? `#${issue.index ?? '?'}`}' failed validation`
|
|
951
|
+
: issue.kind === 'missing-file'
|
|
952
|
+
? 'listed rule file does not exist'
|
|
953
|
+
: 'rule file failed to load';
|
|
954
|
+
process.stdout.write(` ERROR ${issue.file}: ${what} — NOT evaluated\n`);
|
|
955
|
+
for (const line of issue.issues)
|
|
956
|
+
process.stdout.write(` ${line}\n`);
|
|
957
|
+
}
|
|
958
|
+
for (const s of result.staleExceptions) {
|
|
959
|
+
process.stdout.write(` ERROR ${s.ruleId.padEnd(28)} ${s.file}\n`);
|
|
960
|
+
process.stdout.write(` stale exception: ${s.message}\n`);
|
|
961
|
+
}
|
|
962
|
+
for (const r of skippedRules) {
|
|
963
|
+
process.stdout.write(` – ${r.ruleId} checked nothing — ${r.skipReason ?? 'no governed file'}${r.failedOnEmpty ? ' (failOnEmpty: counts as a failure)' : ''}\n`);
|
|
964
|
+
}
|
|
965
|
+
for (const r of env.rules.filter((x) => x.status === 'partial')) {
|
|
966
|
+
process.stdout.write(` ~ ${r.id} PARTIAL — ${r.shortfall ?? 'part of its scope was never examined'}\n`);
|
|
967
|
+
}
|
|
968
|
+
if (result.deadUnits.length > 0) {
|
|
969
|
+
process.stdout.write(`\nDead selector units (${result.deadUnits.length}) — each matches nothing, so it enforces nothing:\n`);
|
|
970
|
+
for (const d of result.deadUnits) {
|
|
971
|
+
process.stdout.write(` • [${d.unit}] ${d.ruleId}: ${d.selector} — ${d.reason}\n`);
|
|
972
|
+
}
|
|
973
|
+
if (!flagBool(args, 'fail-on-dead-units')) {
|
|
974
|
+
process.stdout.write(' Pass --fail-on-dead-units to fail the run on them.\n');
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// Round 13: a marked unit whose target appeared — the fence went live, the
|
|
978
|
+
// expectEmpty marker is stale. A LOCAL marker is drift, not a pass: ✓
|
|
979
|
+
// withheld; it fails under --fail-on-dead-units / --strict. A PACK marker is
|
|
980
|
+
// INFO in its own block — never a failure, and never the consumer's to remove
|
|
981
|
+
// (the pack's author drops it in a release).
|
|
982
|
+
const localWentLive = result.wentLive.filter((u) => u.packageName === undefined);
|
|
983
|
+
const packWentLive = result.wentLive.filter((u) => u.packageName !== undefined);
|
|
984
|
+
if (localWentLive.length > 0) {
|
|
985
|
+
process.stdout.write(`\nexpectEmpty markers that went live (${localWentLive.length}) — each fence now has a target; remove the marker:\n`);
|
|
986
|
+
for (const u of localWentLive) {
|
|
987
|
+
process.stdout.write(` • [${u.unit}] ${u.ruleId}: ${u.selector} — ${u.reason}\n`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (packWentLive.length > 0) {
|
|
991
|
+
process.stdout.write(`\nINFO — pack expectEmpty markers that went live (${packWentLive.length}) — the fence now has a target; the pack's author removes the marker, and it never fails this run:\n`);
|
|
992
|
+
for (const u of packWentLive) {
|
|
993
|
+
process.stdout.write(` • [${u.unit}] ${u.ruleId}: ${u.selector} — ${u.reason}\n`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
// R12-5.6 — INFO, never a finding (no verdict, no dead unit): a forbidden
|
|
997
|
+
// pattern a sibling already covers, e.g. the `pkg` + `pkg/**` helper a bare
|
|
998
|
+
// pattern used to need. The engine's coverage is the one answer.
|
|
999
|
+
const redundant = result.rules.flatMap((r) => r.detail.forbidden.flatMap((f) => f.subsumedBy !== undefined ? [{ ruleId: r.ruleId, pattern: f.pattern, by: f.subsumedBy }] : []));
|
|
1000
|
+
if (redundant.length > 0) {
|
|
1001
|
+
const e = redundant[0];
|
|
1002
|
+
process.stdout.write(`\nnote: ${redundant.length} forbidden pattern(s) already covered by a sibling pattern of the same rule — redundant, safe to delete (e.g. ${e.ruleId}: '${e.pattern}' is covered by '${e.by}'); \`shrk boundaries explain <ruleId>\` lists them.\n`);
|
|
1003
|
+
}
|
|
512
1004
|
if (wantFixSuggestions && fixSuggestions.length > 0) {
|
|
513
1005
|
process.stdout.write('\nFix suggestions:\n');
|
|
514
1006
|
for (const s of fixSuggestions) {
|
|
@@ -517,8 +1009,229 @@ async function checkBoundariesOnce(args) {
|
|
|
517
1009
|
process.stdout.write(` ↳ ${sug}\n`);
|
|
518
1010
|
}
|
|
519
1011
|
}
|
|
520
|
-
|
|
521
|
-
|
|
1012
|
+
// The final line comes from the SETTLED verdict — the ✓ sentence only at 0.
|
|
1013
|
+
if (exit === ExitCode.Failure) {
|
|
1014
|
+
const parts = [];
|
|
1015
|
+
if (result.counts.error > 0)
|
|
1016
|
+
parts.push(`${result.counts.error} error violation(s)`);
|
|
1017
|
+
if (flagBool(args, 'strict') && result.counts.warning > 0)
|
|
1018
|
+
parts.push(`${result.counts.warning} warning(s) under --strict`);
|
|
1019
|
+
if (polyglot && polyglot.errors > 0)
|
|
1020
|
+
parts.push(`${polyglot.errors} polyglot error(s)`);
|
|
1021
|
+
if (result.loadIssues.length > 0)
|
|
1022
|
+
parts.push(`${result.loadIssues.length} errored rule(s)`);
|
|
1023
|
+
if (result.staleExceptions.length > 0)
|
|
1024
|
+
parts.push(`${result.staleExceptions.length} stale exception(s)`);
|
|
1025
|
+
const onEmpty = skippedRules.filter((r) => r.failedOnEmpty).length;
|
|
1026
|
+
if (onEmpty > 0)
|
|
1027
|
+
parts.push(`${onEmpty} rule(s) matched nothing (failOnEmpty)`);
|
|
1028
|
+
// THE units that fail this run (core `selectorUnitFails`, settled by the
|
|
1029
|
+
// orchestrator): an unmarked dead unit under --fail-on-dead-units, a local
|
|
1030
|
+
// went-live expectEmpty marker under it or --strict.
|
|
1031
|
+
const failingDead = result.failingUnits.filter((u) => u.state === UnitLivenessState.Dead).length;
|
|
1032
|
+
const failingLive = result.failingUnits.filter((u) => u.state === UnitLivenessState.WentLive).length;
|
|
1033
|
+
if (failingDead > 0)
|
|
1034
|
+
parts.push(`${failingDead} dead unit(s) (--fail-on-dead-units)`);
|
|
1035
|
+
if (failingLive > 0) {
|
|
1036
|
+
const why = flagBool(args, 'fail-on-dead-units') ? '--fail-on-dead-units' : '--strict';
|
|
1037
|
+
parts.push(`${failingLive} went-live expectEmpty unit(s) (${why})`);
|
|
1038
|
+
}
|
|
1039
|
+
// Round 13 (V1-U6): "violations" only when there are violations — a stale
|
|
1040
|
+
// exception or a dead unit over zero violations is the CHECK's attention.
|
|
1041
|
+
const violated = result.counts.error > 0 || (flagBool(args, 'strict') && result.counts.warning > 0) || (polyglot?.errors ?? 0) > 0;
|
|
1042
|
+
const lead = violated ? 'boundary violations need attention' : 'boundary check needs attention';
|
|
1043
|
+
process.stdout.write(`\nVerdict: ${lead} — ${parts.join(', ') || 'see above'}\n`);
|
|
1044
|
+
const tail = verdictLine(env, '');
|
|
1045
|
+
if (tail)
|
|
1046
|
+
process.stdout.write(`${tail}\n`);
|
|
1047
|
+
return exit;
|
|
1048
|
+
}
|
|
1049
|
+
const line = verdictLine(env, boundaryCleanSentence(result, polyglot?.warnings ?? 0), exit === ExitCode.NotVerified ? boundaryNotVerifiedLead(result, env) : undefined);
|
|
1050
|
+
if (line)
|
|
1051
|
+
process.stdout.write(`\n${line}\n`);
|
|
1052
|
+
if (exit === ExitCode.NotVerified && env.coverage.expected === 0) {
|
|
1053
|
+
process.stdout.write(` Pass --${ALLOW_EMPTY_FLAG} to accept an empty selection explicitly.\n`);
|
|
1054
|
+
}
|
|
1055
|
+
return exit;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* `check boundaries --diff-against <candidate>` — what a proposed rule set would
|
|
1059
|
+
* ADD and REMOVE, before anyone commits a tightening. The same engine as the
|
|
1060
|
+
* gate, one scan for both sides (the orchestrator's `diffBoundaryRuleSets`).
|
|
1061
|
+
* Exit 1 when the proposal adds an error-severity violation, 2 when a
|
|
1062
|
+
* candidate rule's scope is dead (its delta proves nothing), 0 otherwise.
|
|
1063
|
+
*/
|
|
1064
|
+
async function checkBoundaryRuleDiff(args, inspection, raw) {
|
|
1065
|
+
const cwd = resolveCwd(args);
|
|
1066
|
+
const wantJson = flagBool(args, 'json');
|
|
1067
|
+
const loaded = await loadBoundaryRuleFileArg(cwd, inspection.projectRoot, raw, 'diff-against');
|
|
1068
|
+
if (!loaded.ok) {
|
|
1069
|
+
process.stderr.write(`${loaded.message}\n`);
|
|
1070
|
+
return ExitCode.UsageError;
|
|
1071
|
+
}
|
|
1072
|
+
const onlyRuleId = flagString(args, 'rule');
|
|
1073
|
+
const diff = diffBoundaryRuleSets(inspection, { path: loaded.path, rules: loaded.rules, loadIssues: loaded.loadIssues }, {
|
|
1074
|
+
...(onlyRuleId ? { onlyRuleId } : {}),
|
|
1075
|
+
includeComments: flagBool(args, 'include-comments'),
|
|
1076
|
+
failOnDeadUnits: flagBool(args, 'fail-on-dead-units'),
|
|
1077
|
+
});
|
|
1078
|
+
if (diff.unknownRuleId !== undefined) {
|
|
1079
|
+
process.stderr.write(`No boundary rule "${diff.unknownRuleId}" in the active or the candidate set.\n`);
|
|
1080
|
+
return ExitCode.UsageError;
|
|
1081
|
+
}
|
|
1082
|
+
const skippedCandidates = diff.candidateCoverage.filter((c) => c.status === 'skipped');
|
|
1083
|
+
const rows = [
|
|
1084
|
+
...diff.candidateCoverage.map((c) => {
|
|
1085
|
+
const added = diff.added.filter((v) => v.ruleId === c.ruleId);
|
|
1086
|
+
const per = diff.perRule.find((p) => p.ruleId === c.ruleId);
|
|
1087
|
+
return {
|
|
1088
|
+
id: c.ruleId,
|
|
1089
|
+
type: 'boundary',
|
|
1090
|
+
// A candidate that checked nothing and fails on empty WOULD fail the
|
|
1091
|
+
// gate: `failed` (skipReason kept), the same mapping as the gate's own
|
|
1092
|
+
// rows and the wiring plane — and the orchestrator proposes 1 for it.
|
|
1093
|
+
status: c.status === 'skipped'
|
|
1094
|
+
? c.failedOnEmpty
|
|
1095
|
+
? 'failed'
|
|
1096
|
+
: 'skipped'
|
|
1097
|
+
: added.length > 0
|
|
1098
|
+
? 'failed'
|
|
1099
|
+
: 'passed',
|
|
1100
|
+
severity: c.severity === 'error' ? 'error' : 'warning',
|
|
1101
|
+
counts: {
|
|
1102
|
+
filesInScope: c.filesInScope,
|
|
1103
|
+
added: per?.added ?? 0,
|
|
1104
|
+
removed: per?.removed ?? 0,
|
|
1105
|
+
unchanged: per?.unchanged ?? 0,
|
|
1106
|
+
},
|
|
1107
|
+
violations: added.map((v) => ({ id: v.importSpecifier, file: v.file, line: v.line, message: v.message })),
|
|
1108
|
+
...(c.skipReason ? { skipReason: c.skipReason } : {}),
|
|
1109
|
+
coverage: c.coverage,
|
|
1110
|
+
// Round 13: the candidate's expectEmpty acceptance and unit lines, as
|
|
1111
|
+
// on the gate's own rows.
|
|
1112
|
+
...(c.unitAcceptance ? { unitAcceptance: c.unitAcceptance } : {}),
|
|
1113
|
+
...(c.units ? { units: c.units } : {}),
|
|
1114
|
+
};
|
|
1115
|
+
}),
|
|
1116
|
+
...diff.loadIssues.map(boundaryLoadIssueRow),
|
|
1117
|
+
];
|
|
1118
|
+
// The candidate's dead selector units and expectEmpty units — settled by THE
|
|
1119
|
+
// unread re-settle `check boundaries` uses (round 13, P2) — and the units the
|
|
1120
|
+
// one predicate fails under `--fail-on-dead-units` (the orchestrator folds
|
|
1121
|
+
// them into `proposedExit`): the same valve and the same "no ✓ over a dead
|
|
1122
|
+
// unit" clean line as the main path (round 11 review R11-GAP-10).
|
|
1123
|
+
const candidateDead = diff.candidateCoverage.flatMap((c) => c.deadUnits);
|
|
1124
|
+
const proposed = diff.proposedExit === ExitCode.Failure
|
|
1125
|
+
? ExitCode.Failure
|
|
1126
|
+
: skippedCandidates.length > 0
|
|
1127
|
+
? ExitCode.NotVerified
|
|
1128
|
+
: ExitCode.VerifiedPass;
|
|
1129
|
+
const candidates = diff.candidateCoverage.length;
|
|
1130
|
+
const runCoverage = {
|
|
1131
|
+
unit: 'candidate rules',
|
|
1132
|
+
expected: candidates,
|
|
1133
|
+
examined: candidates - skippedCandidates.length,
|
|
1134
|
+
...(skippedCandidates.length > 0
|
|
1135
|
+
? {
|
|
1136
|
+
unexamined: skippedCandidates.map((c) => c.ruleId),
|
|
1137
|
+
reason: 'their from globs reach no governed file — the delta for them proves nothing',
|
|
1138
|
+
}
|
|
1139
|
+
: {}),
|
|
1140
|
+
...(candidates === 0 ? { reason: 'no candidate rule is in the selected scope' } : {}),
|
|
1141
|
+
...allowEmptyValve(args, candidates),
|
|
1142
|
+
};
|
|
1143
|
+
const env = buildGateEnvelope('check boundaries', proposed, rows, runCoverage);
|
|
1144
|
+
if (wantJson) {
|
|
1145
|
+
process.stdout.write(asJson({ ...diff, exitCode: env.exit, accepted: env.accepted, gate: env }) + '\n');
|
|
1146
|
+
return env.exit;
|
|
1147
|
+
}
|
|
1148
|
+
const fmt = (c) => `${c.error} errors, ${c.warning} warnings, ${c.info} info`;
|
|
1149
|
+
process.stdout.write(header('Boundary rule diff (dry run — nothing written)'));
|
|
1150
|
+
process.stdout.write(kv('candidate', diff.candidateFile) + '\n');
|
|
1151
|
+
process.stdout.write(kv('rules added', diff.rulesAdded.join(', ') || '(none)') + '\n');
|
|
1152
|
+
process.stdout.write(kv('rules replaced', diff.rulesReplaced.join(', ') || '(none)') + '\n');
|
|
1153
|
+
process.stdout.write(kv('before', `${diff.before.rules} rule(s) — ${fmt(diff.before.counts)}`) + '\n');
|
|
1154
|
+
process.stdout.write(kv('after', `${diff.after.rules} rule(s) — ${fmt(diff.after.counts)}`) + '\n');
|
|
1155
|
+
process.stdout.write(kv('delta', `+${diff.added.length} added / -${diff.removed.length} removed / =${diff.unchanged} unchanged`) + '\n');
|
|
1156
|
+
process.stdout.write(kv('edges', `${diff.edgeLevel.newlyFlagged.length} newly flagged, ${diff.edgeLevel.noLongerFlagged.length} no longer flagged (any rule)`) + '\n');
|
|
1157
|
+
process.stdout.write('\n Per rule:\n');
|
|
1158
|
+
for (const p of diff.perRule) {
|
|
1159
|
+
process.stdout.write(` +${p.added} -${p.removed} =${p.unchanged} ${p.ruleId}\n`);
|
|
1160
|
+
}
|
|
1161
|
+
if (diff.added.length > 0) {
|
|
1162
|
+
process.stdout.write('\n Added:\n');
|
|
1163
|
+
for (const v of diff.added.slice(0, 20)) {
|
|
1164
|
+
process.stdout.write(` + ${v.severity.toUpperCase().padEnd(8)} ${v.ruleId} ${v.file}:${v.line} "${v.importSpecifier}"\n`);
|
|
1165
|
+
}
|
|
1166
|
+
if (diff.added.length > 20)
|
|
1167
|
+
process.stdout.write(` … (${diff.added.length - 20} more — --json has every one)\n`);
|
|
1168
|
+
}
|
|
1169
|
+
if (diff.removed.length > 0) {
|
|
1170
|
+
process.stdout.write('\n Removed:\n');
|
|
1171
|
+
for (const v of diff.removed.slice(0, 20)) {
|
|
1172
|
+
process.stdout.write(` - ${v.severity.toUpperCase().padEnd(8)} ${v.ruleId} ${v.file}:${v.line} "${v.importSpecifier}"\n`);
|
|
1173
|
+
}
|
|
1174
|
+
if (diff.removed.length > 20)
|
|
1175
|
+
process.stdout.write(` … (${diff.removed.length - 20} more — --json has every one)\n`);
|
|
1176
|
+
}
|
|
1177
|
+
for (const c of skippedCandidates) {
|
|
1178
|
+
process.stdout.write(` – ${c.ruleId} checked nothing — ${c.skipReason ?? 'no governed file'}\n`);
|
|
1179
|
+
}
|
|
1180
|
+
for (const c of diff.candidateCoverage) {
|
|
1181
|
+
for (const d of c.deadUnits)
|
|
1182
|
+
process.stdout.write(` • dead [${d.unit}] ${d.ruleId}: ${d.selector} — ${d.reason}\n`);
|
|
1183
|
+
}
|
|
1184
|
+
// Round 13: a candidate's expectEmpty marker whose target already exists.
|
|
1185
|
+
for (const u of diff.wentLive) {
|
|
1186
|
+
process.stdout.write(` • went live [${u.unit}] ${u.ruleId}: ${u.selector} — ${u.reason}\n`);
|
|
1187
|
+
}
|
|
1188
|
+
for (const issue of diff.loadIssues) {
|
|
1189
|
+
process.stdout.write(` ERROR ${issue.file}: ${boundaryLoadIssueLabel(issue)} — ${issue.issues.join('; ')} — NOT evaluated\n`);
|
|
1190
|
+
}
|
|
1191
|
+
if (env.exit === ExitCode.Failure) {
|
|
1192
|
+
const addedErrors = diff.added.filter((v) => v.severity === 'error').length;
|
|
1193
|
+
const onEmpty = diff.candidateCoverage.filter((c) => c.failedOnEmpty === true).length;
|
|
1194
|
+
const failingDead = diff.failingUnits.filter((u) => u.state === UnitLivenessState.Dead).length;
|
|
1195
|
+
const failingLive = diff.failingUnits.filter((u) => u.state === UnitLivenessState.WentLive).length;
|
|
1196
|
+
const parts = [];
|
|
1197
|
+
if (addedErrors > 0)
|
|
1198
|
+
parts.push(`ADDS ${addedErrors} error-severity violation(s)`);
|
|
1199
|
+
if (diff.loadIssues.length > 0)
|
|
1200
|
+
parts.push(`has ${diff.loadIssues.length} invalid rule(s)`);
|
|
1201
|
+
if (onEmpty > 0)
|
|
1202
|
+
parts.push(`has ${onEmpty} rule(s) matching nothing (failOnEmpty — it would fail the gate)`);
|
|
1203
|
+
if (failingDead > 0)
|
|
1204
|
+
parts.push(`has ${failingDead} dead selector unit(s) (--fail-on-dead-units)`);
|
|
1205
|
+
if (failingLive > 0)
|
|
1206
|
+
parts.push(`has ${failingLive} went-live expectEmpty unit(s) (--fail-on-dead-units)`);
|
|
1207
|
+
process.stdout.write(`\nVerdict: the candidate ${parts.join(' and ') || 'fails'}.\n`);
|
|
1208
|
+
const tail = verdictLine(env, '');
|
|
1209
|
+
if (tail)
|
|
1210
|
+
process.stdout.write(`${tail}\n`);
|
|
1211
|
+
return env.exit;
|
|
1212
|
+
}
|
|
1213
|
+
// The ✓ ONLY when nothing was reported above: a dead unit or a went-live
|
|
1214
|
+
// marker printed a line earlier names what the candidate enforces less than
|
|
1215
|
+
// it says — the main path's convention (`boundaryCleanSentence`), never a ✓
|
|
1216
|
+
// under it. An intended-empty unit prints its acceptance below the ✓.
|
|
1217
|
+
const clauses = [];
|
|
1218
|
+
if (candidateDead.length > 0) {
|
|
1219
|
+
clauses.push(`${candidateDead.length} dead selector unit(s) reported above (--fail-on-dead-units to fail on them)`);
|
|
1220
|
+
}
|
|
1221
|
+
if (diff.wentLive.length > 0) {
|
|
1222
|
+
clauses.push(`${diff.wentLive.length} expectEmpty unit(s) went live (remove each stale expectEmpty marker)`);
|
|
1223
|
+
}
|
|
1224
|
+
const clean = clauses.length > 0
|
|
1225
|
+
? `Verdict: the candidate adds no error-severity violation — ${clauses.join('; ')}.`
|
|
1226
|
+
: 'Verdict: the candidate adds no error-severity violation. ✓';
|
|
1227
|
+
const line = verdictLine(env, clean, env.exit === ExitCode.NotVerified
|
|
1228
|
+
? skippedCandidates.length > 0
|
|
1229
|
+
? 'Verdict: NOT VERIFIED — a candidate rule checked nothing, so its delta proves nothing.'
|
|
1230
|
+
: "Verdict: NOT VERIFIED — part of a candidate rule's scope could not be read, so its delta proves nothing."
|
|
1231
|
+
: undefined);
|
|
1232
|
+
if (line)
|
|
1233
|
+
process.stdout.write(`\n${line}\n`);
|
|
1234
|
+
return env.exit;
|
|
522
1235
|
}
|
|
523
1236
|
// ────────────────────────────────────────────────────────────────────────
|
|
524
1237
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -539,23 +1252,28 @@ const WIRING_CHECK_USAGE = 'shrk check wiring [--changed-only] [--since <ref>] [
|
|
|
539
1252
|
' --write apply the planned edits (only meaningful with --fix)\n' +
|
|
540
1253
|
' Exit: 0 verified pass · 1 violations · 2 not-verified (0 rules evaluated in scope).';
|
|
541
1254
|
/**
|
|
542
|
-
* The
|
|
1255
|
+
* The PROPOSED exit for a wiring run — the envelope then settles it against
|
|
1256
|
+
* every rule's coverage and the run's.
|
|
543
1257
|
*
|
|
544
1258
|
* The banner already said "Not a full green" when some rules were skipped, but
|
|
545
1259
|
* the code an agent chains on returned `0` — a rule that enforced NOTHING was
|
|
546
1260
|
* masked by its passing siblings. The verdict must match the sentence:
|
|
547
1261
|
*
|
|
548
1262
|
* `1` violations (or a `failOnEmpty` skip, which the engine folds into the verdict)
|
|
549
|
-
* `2`
|
|
550
|
-
*
|
|
1263
|
+
* `2` rules were selected but none ran, OR anything skipped — partially
|
|
1264
|
+
* verified is not verified
|
|
1265
|
+
* `0` otherwise. That includes an EMPTY selection (no rule's footprint
|
|
1266
|
+
* intersects the changeset): its run coverage (expected 0) settles it to
|
|
1267
|
+
* 2 unless `--allow-empty` accepted it. Proposing 2 there made the valve
|
|
1268
|
+
* unreachable on exactly the case it exists for.
|
|
551
1269
|
*/
|
|
552
|
-
function wiringExitCode(report, evaluated) {
|
|
1270
|
+
function wiringExitCode(report, evaluated, selected) {
|
|
553
1271
|
if (report.verdict === 'errors')
|
|
554
1272
|
return ExitCode.Failure;
|
|
555
|
-
if (evaluated === 0)
|
|
556
|
-
return ExitCode.NotVerified;
|
|
557
1273
|
if (report.skipped.length > 0)
|
|
558
1274
|
return ExitCode.NotVerified;
|
|
1275
|
+
if (evaluated === 0 && selected > 0)
|
|
1276
|
+
return ExitCode.NotVerified;
|
|
559
1277
|
return ExitCode.VerifiedPass;
|
|
560
1278
|
}
|
|
561
1279
|
/**
|
|
@@ -567,13 +1285,20 @@ function wiringExitCode(report, evaluated) {
|
|
|
567
1285
|
* with the reason, because a gate that silently half-fixes is worse than one
|
|
568
1286
|
* that does nothing — the user must be able to see what was left.
|
|
569
1287
|
*/
|
|
570
|
-
function runWiringFix(cwd, rules, report, write, wantJson) {
|
|
1288
|
+
function runWiringFix(cwd, rules, report, write, wantJson, settle, excludeDirs = []) {
|
|
571
1289
|
const allEdits = [];
|
|
572
1290
|
const allSkips = [];
|
|
573
|
-
// Read every file a sink could live in, once
|
|
1291
|
+
// Read every file a sink could live in, once — over THE plane scan scope
|
|
1292
|
+
// the check itself walked (`planeScanExcludeDirs`). Reading more (the
|
|
1293
|
+
// SharkCraft dir) made a sink the check never saw look like a second
|
|
1294
|
+
// candidate, so a fixable token was refused as `ambiguous-sink-file`.
|
|
574
1295
|
const globs = [...new Set(rules.flatMap((r) => wiringGlobsOf(r)))];
|
|
575
|
-
|
|
576
|
-
|
|
1296
|
+
// A sink file over the read cap is not in `files`, so the planner cannot
|
|
1297
|
+
// find its array and refuses that edit (listed below); it never guesses.
|
|
1298
|
+
const files = [...readMatchingFiles(cwd, globs, new Set(excludeDirs)).files.entries()].map(([path, content]) => ({
|
|
1299
|
+
path,
|
|
1300
|
+
content,
|
|
1301
|
+
}));
|
|
577
1302
|
for (const rule of rules) {
|
|
578
1303
|
const violations = report.violations.filter((v) => v.ruleId === rule.id);
|
|
579
1304
|
if (violations.length === 0)
|
|
@@ -594,6 +1319,12 @@ function runWiringFix(cwd, rules, report, write, wantJson) {
|
|
|
594
1319
|
written += 1;
|
|
595
1320
|
}
|
|
596
1321
|
}
|
|
1322
|
+
// ONE proposed exit for text and JSON (they used to differ: JSON only failed
|
|
1323
|
+
// when nothing at all was fixable). An unfixable violation still means the
|
|
1324
|
+
// gate is red. Then SETTLED against the check's own rule + run coverage: the
|
|
1325
|
+
// fix cannot examine what the rules never examined.
|
|
1326
|
+
const fixExit = allSkips.length > 0 ? ExitCode.Failure : ExitCode.VerifiedPass;
|
|
1327
|
+
const env = settle(fixExit);
|
|
597
1328
|
if (wantJson) {
|
|
598
1329
|
process.stdout.write(asJson({
|
|
599
1330
|
schema: 'sharkcraft.wiring-fix/v1',
|
|
@@ -601,13 +1332,18 @@ function runWiringFix(cwd, rules, report, write, wantJson) {
|
|
|
601
1332
|
filesWritten: written,
|
|
602
1333
|
edits: allEdits.map(({ nextContent: _drop, ...rest }) => rest),
|
|
603
1334
|
skipped: allSkips,
|
|
1335
|
+
exitCode: env.exit,
|
|
1336
|
+
gate: env,
|
|
604
1337
|
}) + '\n');
|
|
605
|
-
return
|
|
1338
|
+
return env.exit;
|
|
606
1339
|
}
|
|
607
1340
|
process.stdout.write(header(write ? 'Wiring fix (applied)' : 'Wiring fix (dry run)'));
|
|
608
1341
|
if (allEdits.length === 0 && allSkips.length === 0) {
|
|
609
1342
|
process.stdout.write(' Nothing to fix — no declared-but-unregistered tokens.\n');
|
|
610
|
-
|
|
1343
|
+
const line = verdictLine(env, '', '--fix repairs declared-but-unregistered tokens only; it cannot examine what the rules never examined.');
|
|
1344
|
+
if (line)
|
|
1345
|
+
process.stdout.write(`\n${line}\n`);
|
|
1346
|
+
return env.exit;
|
|
611
1347
|
}
|
|
612
1348
|
for (const e of allEdits) {
|
|
613
1349
|
process.stdout.write(` ${write ? 'wrote ' : 'would add'} ${e.token} → ${e.file}:${e.line}\n`);
|
|
@@ -629,18 +1365,16 @@ function runWiringFix(cwd, rules, report, write, wantJson) {
|
|
|
629
1365
|
process.stdout.write(write
|
|
630
1366
|
? `\n${written} file(s) written. Re-run \`shrk check wiring\` to confirm, and review the diff.\n`
|
|
631
1367
|
: '\nDry run — nothing written. Re-run with `--write` to apply.\n');
|
|
632
|
-
//
|
|
633
|
-
|
|
1368
|
+
// The final line and the exit come from the SETTLED verdict: an unfixable
|
|
1369
|
+
// violation is 1, and a fix over a partially-examined scope is never 0.
|
|
1370
|
+
const tail = verdictLine(env, '');
|
|
1371
|
+
if (tail)
|
|
1372
|
+
process.stdout.write(`\n${tail}\n`);
|
|
1373
|
+
return env.exit;
|
|
634
1374
|
}
|
|
635
1375
|
async function checkWiring(args) {
|
|
636
1376
|
const cwd = resolveCwd(args);
|
|
637
|
-
// `--help
|
|
638
|
-
// silently running the bare check (a25 §3.2). The check dispatcher doesn't
|
|
639
|
-
// intercept a subverb-level `--help`, so handle it here.
|
|
640
|
-
if (flagBool(args, 'help') || flagBool(args, 'h')) {
|
|
641
|
-
process.stdout.write(WIRING_CHECK_USAGE + '\n');
|
|
642
|
-
return 0;
|
|
643
|
-
}
|
|
1377
|
+
// `--help` / `-h` never reaches here: the dispatcher answers it (dispatch/help-intercept.ts).
|
|
644
1378
|
const wantJson = flagBool(args, 'json');
|
|
645
1379
|
const changedOnly = flagBool(args, 'changed-only');
|
|
646
1380
|
// `--base <ref>` is an accepted synonym for `--since <ref>` (a25 §3.2) —
|
|
@@ -652,17 +1386,31 @@ async function checkWiring(args) {
|
|
|
652
1386
|
// misleading "no rules configured" + exit 0.
|
|
653
1387
|
const loaded = await resolveProjectConfig(cwd);
|
|
654
1388
|
if (!loaded.ok) {
|
|
1389
|
+
// The gate never STARTED — its rules live in the config it could not load —
|
|
1390
|
+
// so this is a usage error (3), never `1`: a CI step reads 1 as "violations
|
|
1391
|
+
// found". `policy-lint` and `gates check` answer the same config the same way.
|
|
655
1392
|
const msg = loaded.error.message;
|
|
656
1393
|
if (wantJson) {
|
|
657
|
-
process.stdout.write(asJson({
|
|
658
|
-
|
|
1394
|
+
process.stdout.write(asJson({
|
|
1395
|
+
schema: 'sharkcraft.wiring/v1',
|
|
1396
|
+
error: msg,
|
|
1397
|
+
rules: [],
|
|
1398
|
+
violations: [],
|
|
1399
|
+
diagnostics: [msg],
|
|
1400
|
+
verdict: 'usage-error',
|
|
1401
|
+
exitCode: ExitCode.UsageError,
|
|
1402
|
+
}) + '\n');
|
|
1403
|
+
return ExitCode.UsageError;
|
|
659
1404
|
}
|
|
660
1405
|
process.stdout.write(header('Wiring check'));
|
|
661
1406
|
process.stdout.write(` ✗ Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
|
|
662
|
-
return
|
|
1407
|
+
return ExitCode.UsageError;
|
|
663
1408
|
}
|
|
664
1409
|
const rules = loaded.value.config.wiringRules ?? [];
|
|
665
1410
|
const planeDiagnostics = loaded.value.planeDiagnostics;
|
|
1411
|
+
// A pack wiring rule the merge seam rejected is a configured rule that did
|
|
1412
|
+
// NOT run — an errored row, exit 1 (round 12 review, R12-X1).
|
|
1413
|
+
const rejectedAll = seamRejectedRules(loaded.value, ['wiring']);
|
|
666
1414
|
// `--explain <ruleId>`: dry-run ONE rule and print the declared + registered
|
|
667
1415
|
// sets it extracts (file:line), the set-difference, and the verdict — the
|
|
668
1416
|
// author-loop view of what the gate sees, without re-running the whole gate.
|
|
@@ -673,6 +1421,19 @@ async function checkWiring(args) {
|
|
|
673
1421
|
return ExitCode.UsageError;
|
|
674
1422
|
}
|
|
675
1423
|
const rule = rules.find((r) => r.id === explainId);
|
|
1424
|
+
const rejectedRule = rule ? undefined : rejectedAll.find((r) => r.id === explainId);
|
|
1425
|
+
if (rejectedRule) {
|
|
1426
|
+
// Declared by a pack, refused by the merge seam: there is nothing to
|
|
1427
|
+
// dry-run, and the rule never runs — a failure, never a not-found.
|
|
1428
|
+
const message = rejectedRule.error ?? 'failed validation';
|
|
1429
|
+
if (wantJson) {
|
|
1430
|
+
process.stdout.write(asJson({ ok: false, error: 'rejected', ruleId: explainId, message, exitCode: ExitCode.Failure }) + '\n');
|
|
1431
|
+
}
|
|
1432
|
+
else {
|
|
1433
|
+
process.stdout.write(`Wiring rule "${explainId}" was REJECTED — ${message}\n`);
|
|
1434
|
+
}
|
|
1435
|
+
return ExitCode.Failure;
|
|
1436
|
+
}
|
|
676
1437
|
if (!rule) {
|
|
677
1438
|
const ids = rules.map((r) => r.id);
|
|
678
1439
|
if (wantJson) {
|
|
@@ -682,17 +1443,42 @@ async function checkWiring(args) {
|
|
|
682
1443
|
process.stderr.write(`No wiring rule "${explainId}". Configured rules: ${ids.length > 0 ? ids.join(', ') : '(none)'}\n`);
|
|
683
1444
|
return ExitCode.UsageError;
|
|
684
1445
|
}
|
|
685
|
-
|
|
1446
|
+
// `check wiring` is a verdict verb, so its `--explain` returns the explained
|
|
1447
|
+
// rule's SETTLED exit (0 / 1 / 2) — never a 0 over a rule the gate reports
|
|
1448
|
+
// partial. `wiring explain` / `gates explain` stay informational (0).
|
|
1449
|
+
const explained = explainWiring(cwd, rule, { excludeDirs: planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir) });
|
|
1450
|
+
renderWiringExplain(explained, wantJson);
|
|
1451
|
+
return settleWiringExplain(explained).exit;
|
|
686
1452
|
}
|
|
687
|
-
if (rules.length === 0) {
|
|
1453
|
+
if (rules.length === 0 && rejectedAll.length === 0) {
|
|
1454
|
+
// Nothing declared is NOT a pass — the request covered zero rules, so it
|
|
1455
|
+
// proved nothing: `2`, unless the caller accepts the empty set explicitly.
|
|
1456
|
+
const env = buildGateEnvelope('check wiring', ExitCode.VerifiedPass, [], {
|
|
1457
|
+
unit: 'wiring rules',
|
|
1458
|
+
expected: 0,
|
|
1459
|
+
examined: 0,
|
|
1460
|
+
reason: 'no wiringRules[] declared',
|
|
1461
|
+
...allowEmptyValve(args, 0),
|
|
1462
|
+
});
|
|
688
1463
|
if (wantJson) {
|
|
689
|
-
process.stdout.write(asJson({
|
|
690
|
-
|
|
1464
|
+
process.stdout.write(asJson({
|
|
1465
|
+
schema: 'sharkcraft.wiring/v1',
|
|
1466
|
+
rules: [],
|
|
1467
|
+
violations: [],
|
|
1468
|
+
verdict: env.exit === ExitCode.VerifiedPass ? 'pass' : 'not-verified',
|
|
1469
|
+
exitCode: env.exit,
|
|
1470
|
+
gate: env,
|
|
1471
|
+
}) + '\n');
|
|
1472
|
+
return env.exit;
|
|
691
1473
|
}
|
|
692
1474
|
process.stdout.write(header('Wiring check'));
|
|
693
1475
|
process.stdout.write(' No wiring rules configured. Declare `wiringRules[]` in sharkcraft.config.ts to enable\n' +
|
|
694
1476
|
' cross-file "declared but not wired" checks (see docs/wiring.md).\n');
|
|
695
|
-
|
|
1477
|
+
process.stdout.write(`\n${verdictLine(env, 'Nothing declared — accepted.')}\n`);
|
|
1478
|
+
if (env.exit !== ExitCode.VerifiedPass) {
|
|
1479
|
+
process.stdout.write(` Pass --${ALLOW_EMPTY_FLAG} to accept an empty rule set explicitly.\n`);
|
|
1480
|
+
}
|
|
1481
|
+
return env.exit;
|
|
696
1482
|
}
|
|
697
1483
|
let changedFiles;
|
|
698
1484
|
if (changedOnly || since) {
|
|
@@ -704,6 +1490,8 @@ async function checkWiring(args) {
|
|
|
704
1490
|
changedFiles = changed.files;
|
|
705
1491
|
}
|
|
706
1492
|
const reportRaw = runWiring(cwd, rules, {
|
|
1493
|
+
// THE plane scan scope, so this verb and `gates check` walk the same tree.
|
|
1494
|
+
excludeDirs: planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir),
|
|
707
1495
|
...(changedOnly || since ? { changedOnly: true, changedFiles: changedFiles ?? [] } : {}),
|
|
708
1496
|
...(only ? { only: only.split(',').map((s) => s.trim()).filter(Boolean) } : {}),
|
|
709
1497
|
});
|
|
@@ -718,11 +1506,21 @@ async function checkWiring(args) {
|
|
|
718
1506
|
// evaluated — rules that actually ran a comparison (globs matched >0 files).
|
|
719
1507
|
// skippedByScope — configured − selected (dropped by the diff/only narrowing).
|
|
720
1508
|
// matchedNothing — selected − evaluated (in scope but matched 0 files).
|
|
721
|
-
|
|
722
|
-
const
|
|
1509
|
+
// rejected — pack rules the merge seam refused: declared, never run.
|
|
1510
|
+
const onlyIds = only ? only.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
|
|
1511
|
+
const rejected = onlyIds ? rejectedAll.filter((r) => onlyIds.includes(r.id)) : rejectedAll;
|
|
1512
|
+
const configured = rules.length + rejectedAll.length;
|
|
1513
|
+
const selected = report.rules.length + rejected.length;
|
|
723
1514
|
const evaluated = report.evaluated;
|
|
1515
|
+
// Round 13 (K6): `report.evaluated` counts a rule accepted as intended-empty
|
|
1516
|
+
// (so the `evaluated === 0` guard never reads an accepted plan as "nothing
|
|
1517
|
+
// ran"), but that rule examined 0 files — every PRINTED "evaluated" count
|
|
1518
|
+
// leaves it out and names it apart: `N evaluated, M accepted as intended-empty`.
|
|
1519
|
+
const acceptedEmpty = report.acceptedEmpty;
|
|
1520
|
+
const evaluatedShown = evaluated - acceptedEmpty;
|
|
1521
|
+
const acceptedNote = acceptedEmptyNote(acceptedEmpty);
|
|
724
1522
|
const skippedByScope = Math.max(0, configured - selected);
|
|
725
|
-
const matchedNothing = Math.max(0,
|
|
1523
|
+
const matchedNothing = Math.max(0, report.rules.length - evaluated);
|
|
726
1524
|
const notVerified = Math.max(0, configured - evaluated);
|
|
727
1525
|
const allEvaluated = evaluated === configured;
|
|
728
1526
|
const scopeNote = changedOnly || since ? ' by --changed-only' : '';
|
|
@@ -731,21 +1529,77 @@ async function checkWiring(args) {
|
|
|
731
1529
|
parts.push(`${skippedByScope} skipped${scopeNote}`);
|
|
732
1530
|
if (matchedNothing > 0)
|
|
733
1531
|
parts.push(`${matchedNothing} matched no files`);
|
|
1532
|
+
if (rejected.length > 0)
|
|
1533
|
+
parts.push(`${rejected.length} rejected at the pack-plane merge seam`);
|
|
734
1534
|
const breakdown = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
735
1535
|
// Rule ids selected by footprint (declared OR registered side intersecting the
|
|
736
1536
|
// diff) — positive evidence the gate ran the RIGHT rules for the change's
|
|
737
1537
|
// blast radius, not just the rules whose literal files were edited (a25 §3.2).
|
|
738
1538
|
const selectedRuleIds = report.rules.map((r) => r.ruleId);
|
|
739
1539
|
const scoped = changedOnly || since !== undefined;
|
|
1540
|
+
// Settle first, render second. The envelope is built for text AND JSON (and
|
|
1541
|
+
// --fix), so every path returns the SAME settled exit, and a subset rule that
|
|
1542
|
+
// passed over registered tokens its declared selector never produced reads
|
|
1543
|
+
// `partial` — exit 2, naming them — instead of a green 0.
|
|
1544
|
+
const proposed = rejected.length > 0 ? ExitCode.Failure : wiringExitCode(report, evaluated, report.rules.length);
|
|
1545
|
+
const unexaminedRules = [
|
|
1546
|
+
...report.skipped.map((s) => s.ruleId),
|
|
1547
|
+
...report.rules.filter((r) => r.status === 'error').map((r) => r.ruleId),
|
|
1548
|
+
...rejected.map((r) => r.id),
|
|
1549
|
+
];
|
|
1550
|
+
const ruleResults = [...report.rules.map((r) => {
|
|
1551
|
+
const skip = report.skipped.find((s) => s.ruleId === r.ruleId);
|
|
1552
|
+
return {
|
|
1553
|
+
id: r.ruleId,
|
|
1554
|
+
type: 'wiring',
|
|
1555
|
+
status: r.status,
|
|
1556
|
+
severity: r.severity,
|
|
1557
|
+
counts: { declared: r.declaredCount, registered: r.registeredCount },
|
|
1558
|
+
violations: r.violations.map((v) => ({
|
|
1559
|
+
id: v.token,
|
|
1560
|
+
file: v.file,
|
|
1561
|
+
line: v.line,
|
|
1562
|
+
...(v.message ? { message: v.message } : {}),
|
|
1563
|
+
...(v.hint ? { hint: v.hint } : {}),
|
|
1564
|
+
})),
|
|
1565
|
+
...(skip ? { skipReason: skip.reason } : {}),
|
|
1566
|
+
...(r.error ? { error: r.error } : {}),
|
|
1567
|
+
coverage: r.coverage,
|
|
1568
|
+
// The rule's `expectEmpty` acceptance and unit lines (round 13, lane G),
|
|
1569
|
+
// folded into the envelope's one settle — the accepted line comes from it.
|
|
1570
|
+
...(r.unitAcceptance !== undefined ? { unitAcceptance: r.unitAcceptance } : {}),
|
|
1571
|
+
...(r.units !== undefined ? { units: r.units } : {}),
|
|
1572
|
+
};
|
|
1573
|
+
}), ...rejected];
|
|
1574
|
+
const runCoverage = {
|
|
1575
|
+
unit: 'wiring rules',
|
|
1576
|
+
expected: selected,
|
|
1577
|
+
examined: selected - unexaminedRules.length,
|
|
1578
|
+
...(unexaminedRules.length > 0
|
|
1579
|
+
? { unexamined: unexaminedRules, reason: 'checked nothing or could not run' }
|
|
1580
|
+
: {}),
|
|
1581
|
+
...(selected === 0
|
|
1582
|
+
? { reason: scoped ? 'no rule footprint intersects the changeset' : 'no rule selected' }
|
|
1583
|
+
: {}),
|
|
1584
|
+
...allowEmptyValve(args, selected),
|
|
1585
|
+
};
|
|
1586
|
+
const env = buildGateEnvelope('check wiring', proposed, ruleResults, runCoverage);
|
|
1587
|
+
const exit = env.exit;
|
|
740
1588
|
// ── --fix: deterministic, heavily-guarded autofix ─────────────────────
|
|
1589
|
+
// The fix repairs declared-but-unregistered tokens; it cannot examine what
|
|
1590
|
+
// the rules never examined. So its exit is settled against the SAME rule and
|
|
1591
|
+
// run coverage: "Nothing to fix" over a partial rule is 2, never 0.
|
|
741
1592
|
if (flagBool(args, 'fix')) {
|
|
742
|
-
return runWiringFix(cwd, rules, report, flagBool(args, 'write'), wantJson);
|
|
1593
|
+
return runWiringFix(cwd, rules, report, flagBool(args, 'write'), wantJson, (fixExit) => buildGateEnvelope('check wiring', fixExit, ruleResults, runCoverage), planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir));
|
|
743
1594
|
}
|
|
744
1595
|
if (wantJson) {
|
|
745
1596
|
// Carry the honest counts so a machine consumer can tell "0 evaluated" from
|
|
746
1597
|
// a real green, and see how many rules the scope skipped + which fired.
|
|
747
1598
|
process.stdout.write(asJson({
|
|
748
1599
|
...report,
|
|
1600
|
+
// The plane verdict derives from the SETTLED exit — never `pass` next
|
|
1601
|
+
// to exitCode 2.
|
|
1602
|
+
verdict: planeVerdictForExit(exit, report.verdict),
|
|
749
1603
|
configured,
|
|
750
1604
|
selected,
|
|
751
1605
|
evaluated,
|
|
@@ -753,44 +1607,49 @@ async function checkWiring(args) {
|
|
|
753
1607
|
matchedNothing,
|
|
754
1608
|
notVerified,
|
|
755
1609
|
selectedRuleIds,
|
|
1610
|
+
// Pack wiring rules the merge seam refused — errored rows in `gate.rules`.
|
|
1611
|
+
rejected: rejected.map((r) => ({ id: r.id, error: r.error ?? null })),
|
|
756
1612
|
// Distinguish verified-pass / failure / not-verified for a chained gate.
|
|
757
|
-
exitCode:
|
|
1613
|
+
exitCode: exit,
|
|
758
1614
|
// One shape across every plane — see docs/gate-json.md.
|
|
759
|
-
gate:
|
|
760
|
-
const skip = report.skipped.find((s) => s.ruleId === r.ruleId);
|
|
761
|
-
return {
|
|
762
|
-
id: r.ruleId,
|
|
763
|
-
type: 'wiring',
|
|
764
|
-
status: r.status,
|
|
765
|
-
severity: r.severity,
|
|
766
|
-
counts: { declared: r.declaredCount, registered: r.registeredCount },
|
|
767
|
-
violations: r.violations.map((v) => ({
|
|
768
|
-
id: v.token,
|
|
769
|
-
file: v.file,
|
|
770
|
-
line: v.line,
|
|
771
|
-
...(v.message ? { message: v.message } : {}),
|
|
772
|
-
...(v.hint ? { hint: v.hint } : {}),
|
|
773
|
-
})),
|
|
774
|
-
...(skip ? { skipReason: skip.reason } : {}),
|
|
775
|
-
...(r.error ? { error: r.error } : {}),
|
|
776
|
-
};
|
|
777
|
-
})),
|
|
1615
|
+
gate: env,
|
|
778
1616
|
}) + '\n');
|
|
779
|
-
|
|
780
|
-
// silent `0` an agent's `&& next` would march past (a25 §1.1).
|
|
781
|
-
return wiringExitCode(report, evaluated);
|
|
1617
|
+
return exit;
|
|
782
1618
|
}
|
|
783
1619
|
process.stdout.write(header('Wiring check'));
|
|
1620
|
+
if (rejected.length > 0) {
|
|
1621
|
+
process.stdout.write(` ✗ ${rejected.length} pack wiring rule(s) failed validation at the pack-plane merge seam — NOT evaluated, FAILED:\n`);
|
|
1622
|
+
for (const r of rejected)
|
|
1623
|
+
process.stdout.write(` ✗ ${r.id} — ${r.error ?? 'failed validation'}\n`);
|
|
1624
|
+
process.stdout.write(' `shrk packs contributions` names every rejected entry.\n');
|
|
1625
|
+
}
|
|
784
1626
|
// `evaluated` counts rules that actually ran a comparison (globs matched >0
|
|
785
|
-
// files). When 0 rules evaluated
|
|
786
|
-
//
|
|
787
|
-
//
|
|
1627
|
+
// files). When 0 rules evaluated, say so loudly — "checked nothing" must
|
|
1628
|
+
// never read as the green "every token is wired" pass, and the exit code
|
|
1629
|
+
// says NOT verified (`2`) too, not a lying `0` — unless the caller accepted
|
|
1630
|
+
// an empty scope with --allow-empty, which is printed as such.
|
|
788
1631
|
if (evaluated === 0) {
|
|
789
|
-
process.stdout.write(
|
|
790
|
-
|
|
791
|
-
|
|
1632
|
+
process.stdout.write(exit === ExitCode.VerifiedPass
|
|
1633
|
+
? ` 0 rules in scope — ${configured} rule(s) configured${breakdown}.\n`
|
|
1634
|
+
: ` ! 0 rules evaluated — NOT verified. ${configured} rule(s) configured${breakdown}; ` +
|
|
1635
|
+
'none ran a comparison in scope. Wiring was not checked — this is not a pass.\n');
|
|
1636
|
+
for (const sk of report.skipped) {
|
|
1637
|
+
process.stdout.write(` ${sk.failed ? '✗' : '–'} ${sk.ruleId} ${sk.failed ? 'FAILED' : 'SKIPPED'} — ${sk.reason}\n`);
|
|
1638
|
+
}
|
|
1639
|
+
// THE empty-rule advice (round 13) — the 0-evaluated branch returned
|
|
1640
|
+
// before the only advice site, so a rule that matched nothing got none.
|
|
1641
|
+
for (const a of emptyRuleAdviceLines(report.skipped.map((sk) => ({ fails: sk.failed === true })))) {
|
|
1642
|
+
process.stdout.write(` ${a}.\n`);
|
|
1643
|
+
}
|
|
1644
|
+
const line = verdictLine(env, 'Nothing in scope — accepted.');
|
|
1645
|
+
if (line)
|
|
1646
|
+
process.stdout.write(`\n${line}\n`);
|
|
1647
|
+
if (selected === 0 && exit === ExitCode.NotVerified) {
|
|
1648
|
+
process.stdout.write(` Pass --${ALLOW_EMPTY_FLAG} to accept an empty changeset explicitly.\n`);
|
|
1649
|
+
}
|
|
1650
|
+
return exit;
|
|
792
1651
|
}
|
|
793
|
-
process.stdout.write(kv('rules evaluated', `${
|
|
1652
|
+
process.stdout.write(kv('rules evaluated', `${evaluatedShown} of ${configured}${acceptedNote}${breakdown}`) + '\n');
|
|
794
1653
|
if (scoped && selectedRuleIds.length > 0) {
|
|
795
1654
|
// Show WHICH rules the diff's footprint selected so the scoping is provable,
|
|
796
1655
|
// not indistinguishable from plain changed-file scoping.
|
|
@@ -799,6 +1658,27 @@ async function checkWiring(args) {
|
|
|
799
1658
|
const errors = report.violations.filter((v) => v.severity === 'error').length;
|
|
800
1659
|
const warnings = report.violations.filter((v) => v.severity === 'warning').length;
|
|
801
1660
|
process.stdout.write(kv('violations', `${errors} error(s), ${warnings} warning(s)`) + '\n');
|
|
1661
|
+
// Every rule that ran clean, WITH both counts, so a pass can be compared to
|
|
1662
|
+
// what it covered. A subset rule whose declared selector never produced some
|
|
1663
|
+
// registered tokens is `partial`, and says which ones it never examined.
|
|
1664
|
+
// THE shared unit-state block (round 13, K2): a dead glob of a rule that
|
|
1665
|
+
// still matched, and a LOCAL expectEmpty marker whose target appeared, are
|
|
1666
|
+
// listed below and withhold the ✓ (exit unchanged); a pack marker is INFO.
|
|
1667
|
+
const unitNotes = unitStateNotes(report.rules.map((r) => ({
|
|
1668
|
+
id: r.ruleId,
|
|
1669
|
+
...(r.unitLiveness !== undefined ? { unitLiveness: r.unitLiveness } : {}),
|
|
1670
|
+
reportedEmpty: report.skipped.some((s) => s.ruleId === r.ruleId),
|
|
1671
|
+
})));
|
|
1672
|
+
const cleanRules = env.rules.filter((r) => r.status === 'passed' || r.status === 'partial');
|
|
1673
|
+
if (cleanRules.length > 0) {
|
|
1674
|
+
process.stdout.write('\n');
|
|
1675
|
+
for (const r of cleanRules) {
|
|
1676
|
+
const counts = `declared ${r.counts['declared'] ?? 0} / registered ${r.counts['registered'] ?? 0}`;
|
|
1677
|
+
process.stdout.write(r.status === 'passed'
|
|
1678
|
+
? ` ${unitNotes.staleIds.has(r.id) ? '⚠' : '✓'} ${r.id} (${counts})\n`
|
|
1679
|
+
: ` ~ ${r.id} (${counts}) — PARTIAL: ${r.shortfall ?? 'not fully examined'}\n`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
802
1682
|
// Misconfigured rules (uncompilable pattern / no capture group) — surface
|
|
803
1683
|
// them loudly; a broken rule must never read as a silent green.
|
|
804
1684
|
if (report.diagnostics.length > 0) {
|
|
@@ -814,10 +1694,12 @@ async function checkWiring(args) {
|
|
|
814
1694
|
for (const sk of report.skipped) {
|
|
815
1695
|
process.stdout.write(` ${sk.failed ? '✗' : '–'} ${sk.ruleId} ${sk.failed ? 'FAILED' : 'SKIPPED'} — ${sk.reason}\n`);
|
|
816
1696
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1697
|
+
// THE empty-rule advice (round 13) — one sentence per REAL `fails` value,
|
|
1698
|
+
// from the shared renderer: a failing (failOnEmpty) rule was told nothing.
|
|
1699
|
+
for (const a of emptyRuleAdviceLines(report.skipped.map((sk) => ({ fails: sk.failed === true })))) {
|
|
1700
|
+
process.stdout.write(` ${a}.\n`);
|
|
820
1701
|
}
|
|
1702
|
+
process.stdout.write(' Run `shrk gates coverage` to audit every plane at once.\n');
|
|
821
1703
|
}
|
|
822
1704
|
// A sink that extracted 0 ids while the source had some is a real failure, but
|
|
823
1705
|
// almost always a stale SINK glob — say so instead of listing N "unwired"
|
|
@@ -833,24 +1715,24 @@ async function checkWiring(args) {
|
|
|
833
1715
|
if (r.sinkHint)
|
|
834
1716
|
process.stdout.write(` → ${r.sinkHint}\n`);
|
|
835
1717
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
return
|
|
1718
|
+
// The final line comes from the SETTLED verdict only — never a ✓ sentence
|
|
1719
|
+
// printed directly, so the banner cannot disagree with `$?`.
|
|
1720
|
+
process.stdout.write(unitNotes.text);
|
|
1721
|
+
const notFull = `No wiring violations among the ${evaluatedShown} rule(s) evaluated${acceptedNote} — ` +
|
|
1722
|
+
`${notVerified} of ${configured} NOT verified${breakdown}. Not a full green.`;
|
|
1723
|
+
// A failOnEmpty skip produces no violation object but IS a failure, so it
|
|
1724
|
+
// falls through to the failure tail below.
|
|
1725
|
+
if (report.violations.length === 0 &&
|
|
1726
|
+
report.diagnostics.length === 0 &&
|
|
1727
|
+
report.verdict !== 'errors') {
|
|
1728
|
+
// Every configured rule ran — the earned full green. A subset ran — no
|
|
1729
|
+
// violations AMONG WHAT RAN, never the unqualified success sentence.
|
|
1730
|
+
const line = verdictLine(env, allEvaluated
|
|
1731
|
+
? qualifyCleanForUnits('No wiring violations — every declared token is registered. ✓', unitNotes)
|
|
1732
|
+
: notFull, proposed === ExitCode.NotVerified ? notFull : undefined);
|
|
1733
|
+
if (line)
|
|
1734
|
+
process.stdout.write(`\n${line}\n`);
|
|
1735
|
+
return exit;
|
|
854
1736
|
}
|
|
855
1737
|
// Group by rule for a readable report.
|
|
856
1738
|
for (const r of report.rules) {
|
|
@@ -870,7 +1752,10 @@ async function checkWiring(args) {
|
|
|
870
1752
|
if (hint)
|
|
871
1753
|
process.stdout.write(` → ${hint}\n`);
|
|
872
1754
|
}
|
|
873
|
-
|
|
1755
|
+
const tail = verdictLine(env, '');
|
|
1756
|
+
if (tail)
|
|
1757
|
+
process.stdout.write(`\n${tail}\n`);
|
|
1758
|
+
return exit;
|
|
874
1759
|
}
|
|
875
1760
|
/**
|
|
876
1761
|
* `shrk check orphans [--since <ref>] [--staged]`: first-class, diff-robust
|
|
@@ -909,9 +1794,19 @@ async function checkOrphans(args) {
|
|
|
909
1794
|
return 2;
|
|
910
1795
|
}
|
|
911
1796
|
const scopeLabel = staged ? 'staged' : `vs ${scan.ref}`;
|
|
912
|
-
//
|
|
913
|
-
//
|
|
1797
|
+
// What the scan examined against what the delete asked it to — ONE
|
|
1798
|
+
// authority (deletedOrphanCoverage): every deleted source file the graph
|
|
1799
|
+
// indexes is expected; one the index does not know is unexamined.
|
|
1800
|
+
const coverage = deletedOrphanCoverage(scan);
|
|
1801
|
+
// Nothing deleted → there is nothing to check, and "checked nothing" must
|
|
1802
|
+
// never read as "verified clean": a LOUD skip that exits 2 (not verified), in
|
|
1803
|
+
// text AND JSON. A hook that runs this on every commit accepts the empty diff
|
|
1804
|
+
// EXPLICITLY with --allow-empty, which is printed as an acceptance.
|
|
914
1805
|
if (scan.deleted.length === 0) {
|
|
1806
|
+
const emptyEnv = buildGateEnvelope('check orphans', ExitCode.VerifiedPass, [], {
|
|
1807
|
+
...coverage,
|
|
1808
|
+
...allowEmptyValve(args, coverage.expected),
|
|
1809
|
+
});
|
|
915
1810
|
if (wantJson) {
|
|
916
1811
|
process.stdout.write(asJson({
|
|
917
1812
|
schema: 'sharkcraft.deleted-orphans/v1',
|
|
@@ -921,25 +1816,56 @@ async function checkOrphans(args) {
|
|
|
921
1816
|
unresolvedDeleted: [],
|
|
922
1817
|
orphans: [],
|
|
923
1818
|
diagnostics: [`no deleted files (${scopeLabel}) — nothing to check`],
|
|
1819
|
+
exitCode: emptyEnv.exit,
|
|
1820
|
+
gate: emptyEnv,
|
|
924
1821
|
}) + '\n');
|
|
925
|
-
return
|
|
1822
|
+
return emptyEnv.exit;
|
|
926
1823
|
}
|
|
927
1824
|
process.stdout.write(header('Orphan check'));
|
|
928
1825
|
process.stdout.write(` ! Nothing deleted (${scopeLabel}) — orphan check skipped.\n`);
|
|
929
|
-
|
|
1826
|
+
process.stdout.write(`\n${verdictLine(emptyEnv, 'Nothing deleted — accepted.')}\n`);
|
|
1827
|
+
if (emptyEnv.exit !== ExitCode.VerifiedPass) {
|
|
1828
|
+
process.stdout.write(` Pass --${ALLOW_EMPTY_FLAG} to accept an empty diff explicitly (e.g. in a per-commit hook).\n`);
|
|
1829
|
+
}
|
|
1830
|
+
return emptyEnv.exit;
|
|
930
1831
|
}
|
|
931
1832
|
const report = scan.report;
|
|
1833
|
+
// The index can only answer for the deleted SOURCE files it knows. One it
|
|
1834
|
+
// does not know (the index predates it, or was rebuilt after the delete)
|
|
1835
|
+
// had its importers never checked: it is expected but unexamined, so a clean
|
|
1836
|
+
// result settles to 2 and names it. A deleted README is outside the scope.
|
|
1837
|
+
const env = buildGateEnvelope('check orphans', report.orphans.length > 0 ? ExitCode.Failure : ExitCode.VerifiedPass, [], { ...coverage, ...allowEmptyValve(args, coverage.expected) });
|
|
1838
|
+
const exit = env.exit;
|
|
932
1839
|
if (wantJson) {
|
|
933
|
-
process.stdout.write(asJson(
|
|
934
|
-
|
|
1840
|
+
process.stdout.write(asJson({
|
|
1841
|
+
...report,
|
|
1842
|
+
...(scan.indexDivergence ? { indexDivergence: scan.indexDivergence } : {}),
|
|
1843
|
+
exitCode: exit,
|
|
1844
|
+
gate: env,
|
|
1845
|
+
}) + '\n');
|
|
1846
|
+
return exit;
|
|
935
1847
|
}
|
|
936
1848
|
process.stdout.write(header('Orphan check'));
|
|
937
1849
|
process.stdout.write(kv('deleted files', `${scan.deleted.length} (${scopeLabel})`) + '\n');
|
|
1850
|
+
process.stdout.write(kv('coverage', formatCoverage(coverage)) + '\n');
|
|
1851
|
+
// The importer side (a file added or edited since the index was built was
|
|
1852
|
+
// never read for imports) and the lead explaining a partial scope — worded
|
|
1853
|
+
// once and shared with `impact --deleted`, which answers the same question
|
|
1854
|
+
// from the same scan.
|
|
1855
|
+
const notes = deletedOrphanScopeNotes(scan, coverage, 'shrk check orphans');
|
|
1856
|
+
if (notes.index !== undefined)
|
|
1857
|
+
process.stdout.write(kv('index', notes.index) + '\n');
|
|
1858
|
+
const unindexedLead = notes.lead;
|
|
938
1859
|
if (report.orphans.length === 0) {
|
|
939
|
-
process.stdout.write('\nNo orphaned importers — nothing still references the deleted code. ✓\n');
|
|
940
1860
|
for (const d of report.diagnostics.slice(0, 5))
|
|
941
1861
|
process.stdout.write(` ! ${d}\n`);
|
|
942
|
-
|
|
1862
|
+
const line = verdictLine(env, 'No orphaned importers — nothing still references the deleted code. ✓', unindexedLead);
|
|
1863
|
+
if (line)
|
|
1864
|
+
process.stdout.write(`\n${line}\n`);
|
|
1865
|
+
if (exit === ExitCode.NotVerified && coverage.expected === 0) {
|
|
1866
|
+
process.stdout.write(` Pass --${ALLOW_EMPTY_FLAG} to accept a delete with no indexed source files explicitly.\n`);
|
|
1867
|
+
}
|
|
1868
|
+
return exit;
|
|
943
1869
|
}
|
|
944
1870
|
process.stdout.write(`\n${report.orphans.length} surviving importer(s) still reference deleted code:\n`);
|
|
945
1871
|
for (const o of report.orphans.slice(0, 100)) {
|
|
@@ -952,14 +1878,55 @@ async function checkOrphans(args) {
|
|
|
952
1878
|
}
|
|
953
1879
|
for (const d of report.diagnostics.slice(0, 5))
|
|
954
1880
|
process.stdout.write(` ! ${d}\n`);
|
|
955
|
-
|
|
1881
|
+
const tail = verdictLine(env, '');
|
|
1882
|
+
if (tail)
|
|
1883
|
+
process.stdout.write(`${tail}\n`);
|
|
1884
|
+
return exit;
|
|
956
1885
|
}
|
|
957
1886
|
// Main shrk check + subcommands
|
|
958
1887
|
// ────────────────────────────────────────────────────────────────────────
|
|
959
1888
|
export const checkCommand = {
|
|
960
1889
|
name: 'check',
|
|
1890
|
+
// Declared for the dispatcher guard, `help` and the command index (round 11
|
|
1891
|
+
// §5.2): any other bare token is an unknown subcommand (`check rules` →
|
|
1892
|
+
// "`rules` is a command of its own"), never a silent full sweep at exit 0.
|
|
1893
|
+
positionals: PositionalMode.None,
|
|
1894
|
+
subverbs: [
|
|
1895
|
+
{ name: 'boundaries', description: 'Enforce the layer / import boundary rules (alias-aware).', usage: BOUNDARIES_CHECK_USAGE },
|
|
1896
|
+
{ name: 'wiring', description: 'The cross-file "declared but not wired" completeness gate (wiringRules[]).', usage: WIRING_CHECK_USAGE },
|
|
1897
|
+
{
|
|
1898
|
+
name: 'orphans',
|
|
1899
|
+
description: 'After a delete: surviving importers of the removed files / exports (alias-resolved).',
|
|
1900
|
+
usage: 'shrk check orphans [--since <ref>] [--staged] [--allow-empty] [--json]',
|
|
1901
|
+
},
|
|
1902
|
+
{
|
|
1903
|
+
name: 'imports',
|
|
1904
|
+
aliases: ['import-hygiene'],
|
|
1905
|
+
description: 'Import hygiene over the changed or selected files.',
|
|
1906
|
+
usage: 'shrk check imports [--changed-only | --since <ref> | --staged | --files a,b] [--emit-allowlist [--emit-allowlist-kind <kind>]] [--only-allowlist-candidates] [--fail-on-unexplained-allowlist] [--allow-empty] [--json]',
|
|
1907
|
+
},
|
|
1908
|
+
{
|
|
1909
|
+
name: 'registry-lifecycle',
|
|
1910
|
+
description: 'Registry lifecycle symmetry across the tree (register ↔ unregister).',
|
|
1911
|
+
usage: 'shrk check registry-lifecycle [--scope <glob>] [--changed-only | --since <ref>] [--limit N] [--offset N] [--budget-ms N] [--allow-empty] [--json]',
|
|
1912
|
+
},
|
|
1913
|
+
{
|
|
1914
|
+
name: 'generation',
|
|
1915
|
+
description: 'Dry-run one template generation and check its target paths.',
|
|
1916
|
+
usage: 'shrk check generation <templateId> <name> [--var k=v ...] [--changed-only | --since <ref> | --staged | --files a,b] [--json]',
|
|
1917
|
+
positionals: PositionalMode.Free,
|
|
1918
|
+
},
|
|
1919
|
+
{ name: 'packs', description: 'Validate the installed packs.', usage: 'shrk check packs [--strict] [--min-score <0-100>] [--allow-empty] [--json]' },
|
|
1920
|
+
{ name: 'pipelines', description: 'Validate the pipelines.', usage: 'shrk check pipelines [--strict] [--min-score <0-100>] [--allow-empty] [--json]' },
|
|
1921
|
+
{ name: 'knowledge', description: 'Validate the knowledge entries.', usage: 'shrk check knowledge [--strict] [--min-score <0-100>] [--allow-empty] [--json]' },
|
|
1922
|
+
{ name: 'templates', description: 'Validate the templates.', usage: 'shrk check templates [--strict] [--min-score <0-100>] [--allow-empty] [--json]' },
|
|
1923
|
+
],
|
|
961
1924
|
description: 'Run SharkCraft-level validation across knowledge / rules / templates / pipelines / packs / action hints / doctor. `check boundaries [--watch [--paths a,b] [--debounce N] [--once]]` re-runs the boundary scan on file changes.',
|
|
962
|
-
usage: 'shrk [--cwd <dir>] check [packs|pipelines|knowledge|generation|boundaries|imports|wiring|orphans] [--strict] [--min-score <0-100>] [--changed-only] [--since <ref>] [--staged] [--only <ids>] [--json] [--watch [--paths <list>] [--debounce N] [--once]]',
|
|
1925
|
+
usage: 'shrk [--cwd <dir>] check [packs|pipelines|knowledge|templates|generation|boundaries|imports|wiring|orphans|registry-lifecycle] [--strict] [--min-score <0-100>] [--changed-only] [--since <ref>] [--staged] [--only <ids>] [--allow-empty] [--json] [--watch [--paths <list>] [--debounce N] [--once]]',
|
|
1926
|
+
// `--allow-empty` is the shared verdict valve (wiring / orphans / boundaries):
|
|
1927
|
+
// declared boolean so it can never swallow the positional subverb that
|
|
1928
|
+
// follows it. The boundary toggles (round 11) likewise never take a value.
|
|
1929
|
+
booleanFlags: new Set([ALLOW_EMPTY_FLAG, 'include-comments', 'fail-on-dead-units', 'no-rule-escalation']),
|
|
963
1930
|
async run(args) {
|
|
964
1931
|
const sub = args.positional[0];
|
|
965
1932
|
// `check generation <id> <name>` legitimately takes extra positionals;
|
|
@@ -984,83 +1951,35 @@ export const checkCommand = {
|
|
|
984
1951
|
return checkWiring(args);
|
|
985
1952
|
if (sub === 'orphans')
|
|
986
1953
|
return checkOrphans(args);
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
const { buildRegistryLifecycleReport, renderRegistryLifecycleReportText } = await import('@shrkcrft/inspector');
|
|
993
|
-
// `--changed-only` scopes the scan to the diff (tracked + untracked), so it
|
|
994
|
-
// runs inline in seconds; the full-tree scan is bounded by a wall-clock
|
|
995
|
-
// budget and flushes partial results on timeout instead of hanging.
|
|
996
|
-
let files;
|
|
997
|
-
if (changedOnly || since) {
|
|
998
|
-
const changed = resolveChangedFiles({
|
|
999
|
-
projectRoot: cwd,
|
|
1000
|
-
...(since ? { since } : {}),
|
|
1001
|
-
...(changedOnly && !since ? { includeWorktree: true } : {}),
|
|
1002
|
-
});
|
|
1003
|
-
files = changed.files;
|
|
1004
|
-
}
|
|
1005
|
-
// Full-tree walk honors the project's skipDirs override (so a repo that
|
|
1006
|
-
// registers code under tools/ etc. isn't blinded by the default set).
|
|
1007
|
-
let skipDirs;
|
|
1008
|
-
if (files === undefined) {
|
|
1009
|
-
const loaded = await resolveProjectConfig(cwd);
|
|
1010
|
-
if (loaded.ok)
|
|
1011
|
-
skipDirs = loaded.value.config.registryLifecycle?.skipDirs;
|
|
1012
|
-
}
|
|
1013
|
-
// Only the slow full-tree path needs the heartbeat (JSON keeps stdout clean).
|
|
1014
|
-
if (!flagBool(args, 'json') && files === undefined) {
|
|
1015
|
-
process.stderr.write('⏳ Scanning source + registries for lifecycle coverage (bounded by a wall-clock budget)…\n');
|
|
1016
|
-
}
|
|
1017
|
-
const report = buildRegistryLifecycleReport({
|
|
1018
|
-
projectRoot: cwd,
|
|
1019
|
-
...(files !== undefined ? { files } : {}),
|
|
1020
|
-
...(scope ? { scope } : {}),
|
|
1021
|
-
...(skipDirs ? { skipDirs } : {}),
|
|
1022
|
-
});
|
|
1023
|
-
// Honest exit-code contract (a25 §1 / §2.5):
|
|
1024
|
-
// timedOut → NOT verified (2): the budget flushed partial results.
|
|
1025
|
-
// registersFound=0 → NOT verified (2): nothing to check in scope — a
|
|
1026
|
-
// "nothing to verify" run must never read as a green
|
|
1027
|
-
// pass (an agent's `&& next` would march past it).
|
|
1028
|
-
// missingRemovers>0 → failure (1).
|
|
1029
|
-
// otherwise → verified pass (0).
|
|
1030
|
-
const exit = report.timedOut || report.registersFound === 0
|
|
1031
|
-
? ExitCode.NotVerified
|
|
1032
|
-
: report.missingRemovers.length === 0
|
|
1033
|
-
? ExitCode.VerifiedPass
|
|
1034
|
-
: ExitCode.Failure;
|
|
1035
|
-
if (flagBool(args, 'json')) {
|
|
1036
|
-
process.stdout.write(asJson(report) + '\n');
|
|
1037
|
-
return exit;
|
|
1038
|
-
}
|
|
1039
|
-
process.stdout.write(renderRegistryLifecycleReportText(report));
|
|
1040
|
-
return exit;
|
|
1041
|
-
}
|
|
1954
|
+
// One body for both lifecycle verbs (registry-lifecycle-run.ts): engine-
|
|
1955
|
+
// owned coverage settled through the gate envelope — a capped / over-budget
|
|
1956
|
+
// / interrupted scan is 2 with the `--offset` that reaches the remainder.
|
|
1957
|
+
if (sub === 'registry-lifecycle')
|
|
1958
|
+
return runRegistryLifecycle(args, 'check registry-lifecycle');
|
|
1042
1959
|
const inspection = await inspectSharkcraft({ cwd: resolveCwd(args) });
|
|
1043
1960
|
const readiness = buildAiReadinessReport(inspection);
|
|
1044
1961
|
const readinessLine = `AI-readiness: ${readiness.score}/100 (${readiness.grade})`;
|
|
1962
|
+
// One group requested alone settles against that group's own doctor
|
|
1963
|
+
// coverage (`alone`): `check packs` over zero packs is 2, as `packs doctor`.
|
|
1045
1964
|
if (sub === 'packs') {
|
|
1046
|
-
return renderReport(args, [packsGroup(inspection)], readinessLine);
|
|
1965
|
+
return renderReport(args, [await packsGroup(inspection, args)], readinessLine, true);
|
|
1047
1966
|
}
|
|
1048
1967
|
if (sub === 'pipelines') {
|
|
1049
|
-
return renderReport(args, [pipelinesGroup(inspection)], readinessLine);
|
|
1968
|
+
return renderReport(args, [pipelinesGroup(inspection, args)], readinessLine, true);
|
|
1050
1969
|
}
|
|
1051
1970
|
if (sub === 'knowledge') {
|
|
1052
|
-
return renderReport(args, [knowledgeGroup(inspection)], readinessLine);
|
|
1971
|
+
return renderReport(args, [knowledgeGroup(inspection, args)], readinessLine, true);
|
|
1053
1972
|
}
|
|
1054
1973
|
if (sub === 'templates') {
|
|
1055
|
-
return renderReport(args, [templatesGroup(inspection)], readinessLine);
|
|
1974
|
+
return renderReport(args, [templatesGroup(inspection, args)], readinessLine, true);
|
|
1056
1975
|
}
|
|
1057
1976
|
// Default: full sweep.
|
|
1058
1977
|
return renderReport(args, [
|
|
1059
1978
|
doctorGroup(inspection),
|
|
1060
|
-
knowledgeGroup(inspection),
|
|
1061
|
-
templatesGroup(inspection),
|
|
1062
|
-
pipelinesGroup(inspection),
|
|
1063
|
-
packsGroup(inspection),
|
|
1979
|
+
knowledgeGroup(inspection, args),
|
|
1980
|
+
templatesGroup(inspection, args),
|
|
1981
|
+
pipelinesGroup(inspection, args),
|
|
1982
|
+
await packsGroup(inspection, args),
|
|
1064
1983
|
actionHintsGroup(inspection),
|
|
1065
1984
|
], readinessLine);
|
|
1066
1985
|
},
|