@shrkcrft/cli 0.1.0-alpha.29 → 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 +76 -0
- package/dist/commands/baseline.command.d.ts.map +1 -1
- package/dist/commands/baseline.command.js +599 -75
- 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 +164 -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 +1260 -324
- 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 +8 -1
- 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 +20 -0
- package/dist/commands/docs-references.command.d.ts.map +1 -0
- package/dist/commands/docs-references.command.js +520 -0
- package/dist/commands/doctor.command.d.ts.map +1 -1
- package/dist/commands/doctor.command.js +60 -5
- 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 +54 -0
- package/dist/commands/gates.command.d.ts.map +1 -1
- package/dist/commands/gates.command.js +1314 -64
- 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 +62 -0
- package/dist/commands/generated.command.d.ts.map +1 -1
- package/dist/commands/generated.command.js +639 -156
- 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 +203 -25
- 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 +362 -74
- 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 +67 -0
- package/dist/gates/gate-rule-globs.d.ts.map +1 -0
- package/dist/gates/gate-rule-globs.js +139 -0
- package/dist/gates/gate-rule-view.d.ts +25 -6
- package/dist/gates/gate-rule-view.d.ts.map +1 -1
- package/dist/gates/gate-rule-view.js +105 -77
- 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 +225 -4
- package/dist/gates/rule-coverage.d.ts.map +1 -1
- package/dist/gates/rule-coverage.js +610 -43
- package/dist/gates/run-gate-planes.d.ts +58 -0
- package/dist/gates/run-gate-planes.d.ts.map +1 -0
- package/dist/gates/run-gate-planes.js +403 -0
- 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 +204 -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
|
@@ -14,22 +14,46 @@
|
|
|
14
14
|
* Distinct from `shrk gate` (singular), which RUNS the quality-gate pipeline.
|
|
15
15
|
* This verb inspects the data-defined RULES themselves.
|
|
16
16
|
*/
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
18
|
import * as nodePath from 'node:path';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
19
|
+
import { coverageShortfall, formatCoverage, DEAD_SELECTOR_CAUSES, formatEmptyRuleAdvice, formatUnitLiveness, resolvePlaneExtractors, RuleEmptiness, ruleVerdictRecords, UnitLivenessState, validateResolvedPlaneSources, } from '@shrkcrft/core';
|
|
20
|
+
import { explainWiring, inspectSource, scanRegistry, settleGlobLists, sourceLivenessRequest } from '@shrkcrft/boundaries';
|
|
21
|
+
import { BaselineRuleSchema, DocReferenceRuleSchema, GATE_PLANE_CONFIG_KEY, GeneratedArtifactRuleSchema, normalizePlaneRule, PolicyRuleSchema, RegistrationIdiomSchema, RegistryDeclarationSchema, WiringRuleSchema, } from '@shrkcrft/config';
|
|
22
|
+
import { warmCliReferenceRegistries } from "../surface/cli-command-resolver.js";
|
|
23
|
+
import { inspectSharkcraft, refExists, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
|
|
24
|
+
import { clearFileReadCache, planeScanExcludeDirs } from '@shrkcrft/boundaries';
|
|
20
25
|
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
26
|
+
import { GLOBAL_FLAGS } from "../dispatch/global-flags.js";
|
|
27
|
+
import { PositionalMode } from "../dispatch/positional-mode.js";
|
|
21
28
|
import { ExitCode } from "../exit-codes.js";
|
|
22
29
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
23
30
|
import { collectGateRules, GATE_PLANES, } from "../gates/gate-rule-view.js";
|
|
24
|
-
import { buildGateCoverage } from "../gates/rule-coverage.js";
|
|
31
|
+
import { buildGateCoverage, coverageDeadUnits, coverageWentLiveUnits, formatDeadUnits, formatNegations, reportsDeadGlobs, settleGateCoverage, withRejectedRules, } from "../gates/rule-coverage.js";
|
|
32
|
+
import { seamRejectedRules } from "../gates/seam-rejected-rules.js";
|
|
33
|
+
import { settleVerdict } from "../gates/settle-verdict.js";
|
|
34
|
+
import { insertSelfTest, scaffoldSelfTest } from "../gates/scaffold-selftest.js";
|
|
25
35
|
import { buildGateEnvelope } from "../gates/gate-envelope.js";
|
|
36
|
+
import { ALLOW_EMPTY_FLAG, allowEmptyValve } from "../gates/allow-empty.js";
|
|
37
|
+
import { verdictLine } from "../gates/verdict-line.js";
|
|
38
|
+
import { acceptedEmptyNote } from "../gates/accepted-empty-note.js";
|
|
39
|
+
import { emptyRuleAdviceLines } from "../gates/empty-rule-advice-lines.js";
|
|
40
|
+
import { qualifyCleanForUnits } from "../gates/qualify-clean-for-units.js";
|
|
41
|
+
import { unitStateNotes } from "../gates/unit-state-notes.js";
|
|
42
|
+
import { gateRuleLabeledSources, ruleTouchedBy } from "../gates/gate-rule-globs.js";
|
|
43
|
+
import { runGatePlanes } from "../gates/run-gate-planes.js";
|
|
26
44
|
import { baselineExplainCommand } from "./baseline.command.js";
|
|
27
45
|
import { generatedExplainCommand } from "./generated.command.js";
|
|
46
|
+
import { docsReferencesExplainCommand } from "./docs-references.command.js";
|
|
28
47
|
import { renderPolicyExplain, runPolicyExplain } from "./policy-lint.command.js";
|
|
29
48
|
import { renderWiringExplain } from "./wiring.command.js";
|
|
30
49
|
const SCHEMA = 'sharkcraft.gates/v1';
|
|
31
|
-
async function prepare(args) {
|
|
50
|
+
export async function prepare(args) {
|
|
32
51
|
const cwd = resolveCwd(args);
|
|
52
|
+
// Coverage memoizes its tree reads inside one call (see `withFileReadCache`).
|
|
53
|
+
// `--no-cache` drops anything already memoized so a suspected caching bug can
|
|
54
|
+
// be ruled out without a code change.
|
|
55
|
+
if (flagBool(args, 'no-cache'))
|
|
56
|
+
clearFileReadCache();
|
|
33
57
|
const json = flagBool(args, 'json');
|
|
34
58
|
const loaded = await resolveProjectConfig(cwd);
|
|
35
59
|
if (!loaded.ok) {
|
|
@@ -40,14 +64,17 @@ async function prepare(args) {
|
|
|
40
64
|
process.stderr.write(`Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
|
|
41
65
|
return { ok: false, code: ExitCode.UsageError };
|
|
42
66
|
}
|
|
43
|
-
const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
|
|
44
67
|
return {
|
|
45
68
|
ok: true,
|
|
46
69
|
value: {
|
|
47
70
|
cwd,
|
|
48
71
|
rules: collectGateRules(loaded.value.config),
|
|
49
|
-
|
|
72
|
+
// THE plane scan scope — the one authority every plane verb reads too.
|
|
73
|
+
excludeDirs: planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir),
|
|
50
74
|
planeDiagnostics: loaded.value.planeDiagnostics,
|
|
75
|
+
rejectedRules: seamRejectedRules(loaded.value),
|
|
76
|
+
extractors: loaded.value.config.extractors ?? {},
|
|
77
|
+
configFile: loaded.value.configFile,
|
|
51
78
|
},
|
|
52
79
|
};
|
|
53
80
|
}
|
|
@@ -79,6 +106,136 @@ function writeNoRules(json) {
|
|
|
79
106
|
' generatedArtifacts[] generated files that must not be hand-edited\n');
|
|
80
107
|
return ExitCode.NotVerified;
|
|
81
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve `--changed-only` / `--since <ref>` (`--base` is a synonym) into the
|
|
111
|
+
* changed-file set, or `undefined` for a whole-tree run.
|
|
112
|
+
*
|
|
113
|
+
* `error` is distinct from "no files changed": an unresolvable ref must not
|
|
114
|
+
* silently degrade to an empty diff, which would narrow every rule out of
|
|
115
|
+
* scope and produce a green run that checked nothing.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveScope(args, cwd) {
|
|
118
|
+
const changedOnly = flagBool(args, 'changed-only');
|
|
119
|
+
const since = flagString(args, 'since') ?? flagString(args, 'base');
|
|
120
|
+
if (!changedOnly && !since)
|
|
121
|
+
return {};
|
|
122
|
+
// The SAME helper `check wiring --changed-only` uses, so bare
|
|
123
|
+
// `--changed-only` means the working tree here too. Two surfaces disagreeing
|
|
124
|
+
// about what "changed" means is how a pre-commit hook silently checks a
|
|
125
|
+
// different set than the CI step it is supposed to mirror.
|
|
126
|
+
if (since !== undefined && !refExists(cwd, since)) {
|
|
127
|
+
return { error: `cannot resolve ref '${since}' — not a valid commit/branch` };
|
|
128
|
+
}
|
|
129
|
+
const changed = resolveChangedFiles({
|
|
130
|
+
projectRoot: cwd,
|
|
131
|
+
...(since ? { since } : { includeWorktree: true }),
|
|
132
|
+
});
|
|
133
|
+
return { files: changed.files };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Narrow rules to those whose FOOTPRINT intersects the change.
|
|
137
|
+
*
|
|
138
|
+
* Returns the surviving rules plus how many were dropped, because a scoped run
|
|
139
|
+
* must never print the same headline as a full one — "0 violations across 2 of
|
|
140
|
+
* 9 rules" and "0 violations across 9 of 9" are different facts.
|
|
141
|
+
*/
|
|
142
|
+
export function narrowToScope(rules, files) {
|
|
143
|
+
if (files === undefined)
|
|
144
|
+
return { selected: rules, skippedByScope: 0 };
|
|
145
|
+
const selected = rules.filter((r) => ruleTouchedBy(r, files));
|
|
146
|
+
return { selected, skippedByScope: rules.length - selected.length };
|
|
147
|
+
}
|
|
148
|
+
/** Flags every `gates` verb accepts; anything else is a typo, not an opt-in. */
|
|
149
|
+
const GATES_FLAGS = new Set([
|
|
150
|
+
'json',
|
|
151
|
+
'margin',
|
|
152
|
+
'write',
|
|
153
|
+
'strict',
|
|
154
|
+
'plane',
|
|
155
|
+
'only',
|
|
156
|
+
'changed-only',
|
|
157
|
+
'since',
|
|
158
|
+
'base',
|
|
159
|
+
'no-spawn',
|
|
160
|
+
'no-cache',
|
|
161
|
+
'full',
|
|
162
|
+
'limit',
|
|
163
|
+
'id',
|
|
164
|
+
'rule-file',
|
|
165
|
+
'wiring',
|
|
166
|
+
// Every dispatcher global (THE list) — a direct handler call may carry them.
|
|
167
|
+
...GLOBAL_FLAGS,
|
|
168
|
+
]);
|
|
169
|
+
/**
|
|
170
|
+
* `--fail-on-dead-units`: a dead glob inside a CONNECTED rule — an inclusion
|
|
171
|
+
* glob that selects nothing, or a negation that excludes nothing — fails the
|
|
172
|
+
* run. A live negation is never dead, so the flag is usable in a repo whose
|
|
173
|
+
* rules exclude their tests. The same flag name the boundary plane uses for
|
|
174
|
+
* its dead scope globs, so one CI switch means one thing across every plane.
|
|
175
|
+
*/
|
|
176
|
+
const FAIL_ON_DEAD_UNITS_FLAG = 'fail-on-dead-units';
|
|
177
|
+
/** Flags only `gates coverage` accepts on top of {@link GATES_FLAGS}. */
|
|
178
|
+
const COVERAGE_ONLY_FLAGS = new Set([FAIL_ON_DEAD_UNITS_FLAG]);
|
|
179
|
+
/** Flags only `gates try` accepts on top of {@link GATES_FLAGS}. */
|
|
180
|
+
const TRY_ONLY_FLAGS = new Set(['flags']);
|
|
181
|
+
/** Flags only `gates check` accepts on top of {@link GATES_FLAGS}. */
|
|
182
|
+
const CHECK_ONLY_FLAGS = new Set([ALLOW_EMPTY_FLAG]);
|
|
183
|
+
// An unrecognized flag is refused rather than ignored: a mistyped
|
|
184
|
+
// `--changed-only` would otherwise parse as an unrelated `true`, run the
|
|
185
|
+
// UNSCOPED form, and exit `0` as though the scoped check had passed. Each
|
|
186
|
+
// guarded verb DECLARES its complete set (`flags`: GATES_FLAGS plus its own
|
|
187
|
+
// extras — the identical sets the inline `rejectUnknownFlags` guard used), and
|
|
188
|
+
// the dispatcher refuses anything else before `run`, exiting 3 (UsageError).
|
|
189
|
+
/**
|
|
190
|
+
* Apply `--plane` and `--only` narrowing.
|
|
191
|
+
*
|
|
192
|
+
* An unknown `--only` id is REFUSED rather than silently matching nothing: a
|
|
193
|
+
* typo'd rule id would otherwise narrow the run to zero rules and report a
|
|
194
|
+
* confident pass over an empty set.
|
|
195
|
+
*/
|
|
196
|
+
function filterRules(all, planes, only, rejectedAll = []) {
|
|
197
|
+
let rules = planes ? all.filter((r) => planes.has(r.plane)) : all;
|
|
198
|
+
let rejected = planes ? rejectedAll.filter((r) => planes.has(r.type)) : rejectedAll;
|
|
199
|
+
if (!only)
|
|
200
|
+
return { ok: true, rules, rejected };
|
|
201
|
+
const wanted = only.split(',').map((x) => x.trim()).filter(Boolean);
|
|
202
|
+
// A rejected rule is a DECLARED rule (it did not run), so `--only <its id>`
|
|
203
|
+
// selects its errored row rather than refusing the id as unknown.
|
|
204
|
+
const known = new Set([...all.map((r) => r.id), ...rejectedAll.map((r) => r.id)]);
|
|
205
|
+
const unknown = wanted.filter((w) => !known.has(w));
|
|
206
|
+
if (unknown.length > 0) {
|
|
207
|
+
process.stderr.write(`Unknown rule id(s) in --only: ${unknown.join(', ')}. Run \`shrk gates list\` to see the ${all.length} declared rule(s).\n`);
|
|
208
|
+
return { ok: false };
|
|
209
|
+
}
|
|
210
|
+
rules = rules.filter((r) => wanted.includes(r.id));
|
|
211
|
+
rejected = rejected.filter((r) => wanted.includes(r.id));
|
|
212
|
+
return { ok: true, rules, rejected };
|
|
213
|
+
}
|
|
214
|
+
/** ` ✗ [policy] pk-x — REJECTED: <why>` — one line per merge-seam-rejected rule. */
|
|
215
|
+
function writeRejectedRules(rejected) {
|
|
216
|
+
for (const r of rejected)
|
|
217
|
+
process.stdout.write(` ✗ [${r.type}] ${r.id} REJECTED — ${r.error ?? 'failed validation'}\n`);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Build the registries the doc-reference plane resolves against — only when a
|
|
221
|
+
* rule of that plane is actually in scope.
|
|
222
|
+
*
|
|
223
|
+
* Every other plane reads files; this one reads shrk's own registries, which
|
|
224
|
+
* cost a workspace inspection. Paying that on every `gates coverage` in a repo
|
|
225
|
+
* with no doc-reference rules would be a tax on the common case.
|
|
226
|
+
*/
|
|
227
|
+
async function inspectionIfNeeded(cwd, rules) {
|
|
228
|
+
if (!rules.some((r) => r.plane === 'doc-reference'))
|
|
229
|
+
return undefined;
|
|
230
|
+
// Playbook / construct ids come from a cache an ASYNC load populates; the
|
|
231
|
+
// resolver is sync. Warming here is what makes a correct pack playbook cited
|
|
232
|
+
// in prose actually resolve.
|
|
233
|
+
const inspection = await inspectSharkcraft({ cwd });
|
|
234
|
+
// WITH the command resolver, so a doc-reference rule resolving `command`
|
|
235
|
+
// ids checks them against the live command index.
|
|
236
|
+
await warmCliReferenceRegistries(inspection);
|
|
237
|
+
return inspection;
|
|
238
|
+
}
|
|
82
239
|
export const gatesListCommand = {
|
|
83
240
|
name: 'list',
|
|
84
241
|
description: 'Every data-defined rule across every plane, with its severity and empty-match policy.',
|
|
@@ -95,7 +252,10 @@ export const gatesListCommand = {
|
|
|
95
252
|
const rules = planes.planes
|
|
96
253
|
? prep.value.rules.filter((r) => planes.planes.has(r.plane))
|
|
97
254
|
: prep.value.rules;
|
|
98
|
-
|
|
255
|
+
const rejected = planes.planes
|
|
256
|
+
? prep.value.rejectedRules.filter((r) => planes.planes.has(r.type))
|
|
257
|
+
: prep.value.rejectedRules;
|
|
258
|
+
if (rules.length === 0 && rejected.length === 0)
|
|
99
259
|
return writeNoRules(json);
|
|
100
260
|
if (json) {
|
|
101
261
|
process.stdout.write(asJson({
|
|
@@ -109,6 +269,8 @@ export const gatesListCommand = {
|
|
|
109
269
|
failOnEmpty: r.failOnEmpty,
|
|
110
270
|
selfTest: r.selfTest ?? null,
|
|
111
271
|
})),
|
|
272
|
+
// Declared by a pack, refused by the merge seam: they never run.
|
|
273
|
+
rejected: rejected.map((r) => ({ id: r.id, plane: r.type, error: r.error ?? null })),
|
|
112
274
|
diagnostics: prep.value.planeDiagnostics,
|
|
113
275
|
}) + '\n');
|
|
114
276
|
return ExitCode.VerifiedPass;
|
|
@@ -130,6 +292,10 @@ export const gatesListCommand = {
|
|
|
130
292
|
process.stdout.write(` ${r.description}\n`);
|
|
131
293
|
}
|
|
132
294
|
}
|
|
295
|
+
if (rejected.length > 0) {
|
|
296
|
+
process.stdout.write(`\nrejected at the pack-plane merge seam — never run (${rejected.length})\n`);
|
|
297
|
+
writeRejectedRules(rejected);
|
|
298
|
+
}
|
|
133
299
|
for (const d of prep.value.planeDiagnostics)
|
|
134
300
|
process.stdout.write(` ! ${d}\n`);
|
|
135
301
|
process.stdout.write('\nRun `shrk gates coverage` to see what each one actually matches.\n');
|
|
@@ -139,8 +305,9 @@ export const gatesListCommand = {
|
|
|
139
305
|
export const gatesCoverageCommand = {
|
|
140
306
|
name: 'coverage',
|
|
141
307
|
description: 'What every rule MATCHED against the live tree — the stale-selector detector. A rule matching 0 files/ids is a bug in the rule, never a pass. Also runs each rule\'s declared selfTest expectations.',
|
|
142
|
-
usage: 'shrk gates coverage [--plane <p>] [--strict] [--json]',
|
|
143
|
-
booleanFlags: new Set(['json', 'strict']),
|
|
308
|
+
usage: 'shrk gates coverage [--plane <p>] [--changed-only | --since <ref>] [--only <ids>] [--fail-on-dead-units] [--strict] [--json]',
|
|
309
|
+
booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-cache', FAIL_ON_DEAD_UNITS_FLAG]),
|
|
310
|
+
flags: new Set([...GATES_FLAGS, ...COVERAGE_ONLY_FLAGS]),
|
|
144
311
|
async run(args) {
|
|
145
312
|
const prep = await prepare(args);
|
|
146
313
|
if (!prep.ok)
|
|
@@ -149,85 +316,1025 @@ export const gatesCoverageCommand = {
|
|
|
149
316
|
if (!planes.ok)
|
|
150
317
|
return ExitCode.UsageError;
|
|
151
318
|
const json = flagBool(args, 'json');
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
319
|
+
const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'), prep.value.rejectedRules);
|
|
320
|
+
if (!filtered.ok)
|
|
321
|
+
return ExitCode.UsageError;
|
|
322
|
+
// A rule the merge seam rejected is a configured rule that never ran — it
|
|
323
|
+
// is never "no rules declared", and it is never narrowed out by scope.
|
|
324
|
+
const rejected = filtered.rejected;
|
|
325
|
+
if (filtered.rules.length === 0 && rejected.length === 0) {
|
|
326
|
+
return writeNoRulesVerdict('gates coverage', args, json, false);
|
|
327
|
+
}
|
|
328
|
+
// Scoping the STALE-SELECTOR check to the diff is what moves it from a
|
|
329
|
+
// CI-only report to something you can afford on every save — a stale glob
|
|
330
|
+
// is then caught the moment you cause it, not the next morning.
|
|
331
|
+
const scope = resolveScope(args, prep.value.cwd);
|
|
332
|
+
if (scope.error) {
|
|
333
|
+
process.stderr.write(`Cannot scope to the changeset: ${scope.error}\n`);
|
|
334
|
+
return ExitCode.UsageError;
|
|
335
|
+
}
|
|
336
|
+
const { selected, skippedByScope } = narrowToScope(filtered.rules, scope.files);
|
|
337
|
+
if (selected.length === 0 && rejected.length === 0) {
|
|
338
|
+
// Nothing in scope means nothing was PROVEN — never a green.
|
|
339
|
+
const emptyEnv = buildGateEnvelope('gates coverage', ExitCode.VerifiedPass, [], {
|
|
340
|
+
unit: 'rules',
|
|
341
|
+
expected: 0,
|
|
342
|
+
examined: 0,
|
|
343
|
+
reason: `no rule's footprint intersects the changeset (${skippedByScope} skipped by scope)`,
|
|
344
|
+
});
|
|
345
|
+
if (json) {
|
|
346
|
+
process.stdout.write(asJson({
|
|
347
|
+
schema: 'sharkcraft.gate-coverage/v1',
|
|
348
|
+
rules: [],
|
|
349
|
+
total: 0,
|
|
350
|
+
scoped: true,
|
|
351
|
+
skippedByScope,
|
|
352
|
+
exitCode: emptyEnv.exit,
|
|
353
|
+
gate: emptyEnv,
|
|
354
|
+
}) + '\n');
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
process.stdout.write(header('Gate-rule coverage'));
|
|
358
|
+
process.stdout.write(` No rule's footprint intersects the changeset (${skippedByScope} skipped by scope).\n` +
|
|
359
|
+
' Nothing was checked — this is NOT a pass.\n');
|
|
360
|
+
}
|
|
361
|
+
return emptyEnv.exit;
|
|
362
|
+
}
|
|
363
|
+
const rules = selected;
|
|
364
|
+
const report = withRejectedRules(buildGateCoverage(prep.value.cwd, rules, prep.value.excludeDirs, prep.value.extractors, false, await inspectionIfNeeded(prep.value.cwd, rules)), rejected);
|
|
365
|
+
const failOnDeadUnits = flagBool(args, FAIL_ON_DEAD_UNITS_FLAG);
|
|
158
366
|
// A rule that matched nothing is NOT-VERIFIED (2) by default — it neither
|
|
159
367
|
// passed nor failed, it never ran. `failOnEmpty` on the rule (or the global
|
|
160
|
-
// --strict promotion) turns that into a hard failure
|
|
161
|
-
|
|
368
|
+
// --strict promotion) turns that into a hard failure, as does a dead glob
|
|
369
|
+
// under --fail-on-dead-units. Settle first, render second: a rule that is
|
|
370
|
+
// connected but only PARTIALLY is `partial` here exactly as it is in
|
|
371
|
+
// `gates check`. `settleGateCoverage` is the ONE derivation — `shrk
|
|
372
|
+
// quality`'s coverage item reads the same one.
|
|
373
|
+
const env = settleGateCoverage(report, { failOnDeadUnits });
|
|
374
|
+
const exit = env.exit;
|
|
162
375
|
const softEmpty = report.rules.filter((r) => r.status === 'empty' && !r.failOnEmpty);
|
|
163
|
-
const exit = hardFailures.length > 0
|
|
164
|
-
? ExitCode.Failure
|
|
165
|
-
: softEmpty.length > 0
|
|
166
|
-
? ExitCode.NotVerified
|
|
167
|
-
: ExitCode.VerifiedPass;
|
|
168
376
|
if (json) {
|
|
169
377
|
process.stdout.write(asJson({
|
|
170
378
|
...report,
|
|
171
|
-
|
|
379
|
+
// Rules the settled envelope fails (errored, broken selfTest, a
|
|
380
|
+
// failOnEmpty rule matching nothing, a dead glob under the flag).
|
|
381
|
+
hardFailures: env.failed,
|
|
382
|
+
failOnDeadUnits,
|
|
383
|
+
...(scope.files ? { scoped: true, skippedByScope } : {}),
|
|
172
384
|
exitCode: exit,
|
|
173
|
-
gate:
|
|
174
|
-
id: r.id,
|
|
175
|
-
type: r.plane,
|
|
176
|
-
status: r.status === 'ok'
|
|
177
|
-
? 'passed'
|
|
178
|
-
: r.status === 'empty'
|
|
179
|
-
? r.failOnEmpty
|
|
180
|
-
? 'failed'
|
|
181
|
-
: 'skipped'
|
|
182
|
-
: r.status === 'error'
|
|
183
|
-
? 'error'
|
|
184
|
-
: 'failed',
|
|
185
|
-
severity: r.failOnEmpty ? 'error' : 'warning',
|
|
186
|
-
counts: { files: r.filesMatched, units: r.unitsMatched },
|
|
187
|
-
violations: r.expectationFailures.map((f) => ({ id: r.id, message: f })),
|
|
188
|
-
...(r.status === 'empty' ? { skipReason: `matched 0 ${r.unitLabel}` } : {}),
|
|
189
|
-
...(r.error ? { error: r.error } : {}),
|
|
190
|
-
}))),
|
|
385
|
+
gate: env,
|
|
191
386
|
}) + '\n');
|
|
192
387
|
return exit;
|
|
193
388
|
}
|
|
194
389
|
process.stdout.write(header('Gate-rule coverage'));
|
|
195
|
-
process.stdout.write(kv('rules',
|
|
196
|
-
|
|
390
|
+
process.stdout.write(kv('rules', `${report.total}${skippedByScope > 0 ? ` (${skippedByScope} skipped by scope)` : ''}`) + '\n');
|
|
391
|
+
if (scope.files) {
|
|
392
|
+
process.stdout.write(kv('scope', `changed-only (${scope.files.length} file(s))`) + '\n');
|
|
393
|
+
}
|
|
394
|
+
// A rule its own negations emptied is not a stale-selector suspect, so the
|
|
395
|
+
// header counts it apart — agreeing with the row it draws below.
|
|
396
|
+
const emptiedByNegations = report.rules.filter((r) => r.status === 'empty' && r.excludedByNegations !== undefined).length;
|
|
397
|
+
const staleSuspects = report.empty - emptiedByNegations;
|
|
398
|
+
const emptyNote = report.empty === 0
|
|
399
|
+
? ''
|
|
400
|
+
: emptiedByNegations === 0
|
|
401
|
+
? ' ← stale selector suspects'
|
|
402
|
+
: staleSuspects === 0
|
|
403
|
+
? ' ← emptied by their own negations, not stale'
|
|
404
|
+
: ` ← ${staleSuspects} stale selector suspect(s), ${emptiedByNegations} emptied by their own negations`;
|
|
405
|
+
process.stdout.write(kv('matched nothing', `${report.empty}${emptyNote}`) + '\n');
|
|
197
406
|
if (report.errored > 0)
|
|
198
407
|
process.stdout.write(kv('misconfigured', String(report.errored)) + '\n');
|
|
199
408
|
if (report.expectationFailures > 0) {
|
|
200
409
|
process.stdout.write(kv('broken selfTest', String(report.expectationFailures)) + '\n');
|
|
201
410
|
}
|
|
411
|
+
if (report.deadGlobCount > 0) {
|
|
412
|
+
process.stdout.write(kv('dead globs', `${report.deadGlobCount} ← inside rules that still match something` +
|
|
413
|
+
(failOnDeadUnits ? ` (--${FAIL_ON_DEAD_UNITS_FLAG}: failing)` : '')) + '\n');
|
|
414
|
+
}
|
|
415
|
+
// The shared extractors, ONCE. Their whole value is that N consumers cannot
|
|
416
|
+
// disagree about which set they check — so one line proving the shared set
|
|
417
|
+
// is live and non-empty covers all N.
|
|
418
|
+
if (report.extractors.length > 0) {
|
|
419
|
+
process.stdout.write('\n shared extractors\n');
|
|
420
|
+
for (const e of report.extractors) {
|
|
421
|
+
const mark = e.error ? '!' : e.idsMatched === 0 ? '✗' : '✓';
|
|
422
|
+
process.stdout.write(` ${mark} $use:${e.id} — ${e.idsMatched} ids across ${e.filesMatched} file(s), ` +
|
|
423
|
+
`shared by ${e.consumers.length} rule(s): ${e.consumers.join(', ')}\n`);
|
|
424
|
+
if (e.sampleIds.length > 0)
|
|
425
|
+
process.stdout.write(` e.g. ${e.sampleIds.join(', ')}\n`);
|
|
426
|
+
if (e.error)
|
|
427
|
+
process.stdout.write(` ! ${e.error}\n`);
|
|
428
|
+
if (!e.error && e.idsMatched === 0) {
|
|
429
|
+
process.stdout.write(' matched nothing — every consumer of this extractor is checking an empty set\n');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
202
433
|
process.stdout.write('\n');
|
|
203
|
-
for (const r of report.rules) {
|
|
204
|
-
|
|
205
|
-
|
|
434
|
+
for (const [i, r] of report.rules.entries()) {
|
|
435
|
+
// The settled status: `partial` is derived by the envelope builder alone.
|
|
436
|
+
const settled = env.rules[i];
|
|
437
|
+
const partial = settled?.status === 'partial';
|
|
438
|
+
const failedSettled = settled?.status === 'failed' || settled?.status === 'error';
|
|
439
|
+
const mark = partial
|
|
440
|
+
? '~'
|
|
441
|
+
: failedSettled
|
|
442
|
+
? '✗'
|
|
443
|
+
: r.status === 'ok'
|
|
444
|
+
? reportsDeadGlobs(r) || coverageWentLiveUnits(r).some((u) => u.mark?.packageName === undefined)
|
|
445
|
+
? '⚠'
|
|
446
|
+
: '✓'
|
|
447
|
+
: r.status === 'empty'
|
|
448
|
+
? '–'
|
|
449
|
+
: '✗';
|
|
450
|
+
process.stdout.write(` ${mark} [${r.plane}] ${r.id}${r.viaExtractor ? ` (via $use:${r.viaExtractor})` : ''}` +
|
|
451
|
+
` — ${r.unitsMatched} ${r.unitLabel} across ${r.filesMatched} file(s)\n`);
|
|
452
|
+
if (partial && settled?.shortfall) {
|
|
453
|
+
process.stdout.write(` PARTIAL — ${settled.shortfall}\n`);
|
|
454
|
+
}
|
|
455
|
+
// One glob of a connected rule matching nothing is the rename that
|
|
456
|
+
// silently halves a rule while its siblings keep it green. A rule
|
|
457
|
+
// already reported as matching nothing gets no second line for it.
|
|
458
|
+
if (reportsDeadGlobs(r)) {
|
|
459
|
+
const dead = coverageDeadUnits(r);
|
|
460
|
+
process.stdout.write(` ⚠ ${dead.length} of ${r.globsChecked} glob(s) dead: ${formatDeadUnits(dead)}\n`);
|
|
461
|
+
}
|
|
462
|
+
// Round 13: a glob marked `expectEmpty` whose target now exists — the
|
|
463
|
+
// fence went live. The ✓ is withheld (a LOCAL marker is a stale
|
|
464
|
+
// assertion; `--fail-on-dead-units` fails it); a PACK marker reads as
|
|
465
|
+
// INFO, never a failure — the consumer cannot edit it.
|
|
466
|
+
for (const u of coverageWentLiveUnits(r)) {
|
|
467
|
+
process.stdout.write(` ${u.mark?.packageName !== undefined ? 'i' : '⚠'} ${formatUnitLiveness(u, { causes: false })}\n`);
|
|
468
|
+
}
|
|
469
|
+
// …and the globs it asserts are intended-empty, each as its ONE per-unit
|
|
470
|
+
// line (`formatUnitLiveness`) under a neutral bullet — never a ✓: the ✓
|
|
471
|
+
// and the acceptance belong to `verdictLine`, which prints them only at
|
|
472
|
+
// exit 0 (a FAILED or SKIPPED rule's marked unit is no acceptance).
|
|
473
|
+
for (const u of (r.unitLiveness ?? []).filter((x) => x.state === UnitLivenessState.IntendedEmpty)) {
|
|
474
|
+
process.stdout.write(` · ${formatUnitLiveness(u, { causes: false })}\n`);
|
|
475
|
+
}
|
|
476
|
+
// A LIVE negation is load-bearing narrowing, not a warning: print what
|
|
477
|
+
// it excludes, so the scope the rule does NOT measure is on the page.
|
|
478
|
+
const negations = r.negations ?? [];
|
|
479
|
+
if (negations.length > 0)
|
|
480
|
+
process.stdout.write(` excludes: ${formatNegations(negations)}\n`);
|
|
206
481
|
if (r.sampleIds.length > 0) {
|
|
207
482
|
process.stdout.write(` e.g. ${r.sampleIds.join(', ')}\n`);
|
|
208
483
|
}
|
|
209
484
|
if (r.status === 'empty') {
|
|
210
|
-
|
|
485
|
+
// A rule its own `!` emptied is not stale: its inclusion globs matched
|
|
486
|
+
// files and every one was excluded as written. Say that, never "stale".
|
|
487
|
+
const emptiedBy = r.excludedByNegations;
|
|
488
|
+
process.stdout.write(emptiedBy !== undefined
|
|
489
|
+
? ` ${failedSettled ? 'FAILED' : 'SKIPPED'} — matched nothing after its own negations: ` +
|
|
490
|
+
`every file its inclusion globs select is excluded (${formatNegations(emptiedBy)})\n`
|
|
491
|
+
: r.emptyReason !== undefined
|
|
492
|
+
? // A fence over a DEAD input (round 13): the settle's own reason.
|
|
493
|
+
` ${failedSettled ? 'FAILED' : 'SKIPPED'} — ${r.emptyReason}\n`
|
|
494
|
+
: ` ${failedSettled ? 'FAILED' : 'SKIPPED'} — matched nothing; the selector is probably stale\n`);
|
|
495
|
+
// When the engine knows WHY a correct zero-match is probably not what
|
|
496
|
+
// the author meant, say so here rather than leaving them to rediscover
|
|
497
|
+
// it — this is the one dead end `import-edges` reliably produces.
|
|
498
|
+
if (r.hint)
|
|
499
|
+
process.stdout.write(` → ${r.hint}\n`);
|
|
211
500
|
}
|
|
501
|
+
// A hint on a rule that DID match — e.g. a zoned pattern's blank-run
|
|
502
|
+
// backtracking hazard (findBlankRunHazards) — is worth seeing before it bites.
|
|
503
|
+
if (r.status !== 'empty' && r.hint)
|
|
504
|
+
process.stdout.write(` → ${r.hint}\n`);
|
|
212
505
|
if (r.error)
|
|
213
506
|
process.stdout.write(` ! ${r.error}\n`);
|
|
214
507
|
for (const f of r.expectationFailures)
|
|
215
508
|
process.stdout.write(` ! selfTest: ${f}\n`);
|
|
216
509
|
}
|
|
217
|
-
|
|
218
|
-
|
|
510
|
+
// The dead-selector causes ONCE, as a footer — the ⚠ rows above name each
|
|
511
|
+
// dead unit without them, and `check boundaries` appends them to every
|
|
512
|
+
// dead unit (round 13 review: they were printed nowhere here).
|
|
513
|
+
if (report.rules.some(reportsDeadGlobs)) {
|
|
514
|
+
process.stdout.write(`\n Dead selectors: ${DEAD_SELECTOR_CAUSES}.\n`);
|
|
515
|
+
}
|
|
516
|
+
// THE empty-rule advice with each rule's REAL `fails` (the shared
|
|
517
|
+
// renderer): a FAILING empty rule (failOnEmpty) was told nothing, and a
|
|
518
|
+
// soft one only on a NOT VERIFIED run (the lead below carries it there).
|
|
519
|
+
for (const a of emptyRuleAdviceLines([
|
|
520
|
+
...report.rules.filter((r) => r.status === 'empty' && r.failOnEmpty).map(() => ({ fails: true })),
|
|
521
|
+
...(exit === ExitCode.NotVerified ? [] : softEmpty.map(() => ({ fails: false }))),
|
|
522
|
+
])) {
|
|
523
|
+
process.stdout.write(`\n ${a}.\n`);
|
|
219
524
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
525
|
+
// THE empty-rule advice (round 13) — one sentence on every plane verb; a
|
|
526
|
+
// soft-empty rule is told how to make it fail, a marker how to say "planned".
|
|
527
|
+
const lead = exit === ExitCode.NotVerified && softEmpty.length > 0
|
|
528
|
+
? `${softEmpty.length} rule(s) matched nothing — NOT a pass. ${formatEmptyRuleAdvice({ fails: false })}.`
|
|
529
|
+
: undefined;
|
|
530
|
+
// "Every rule is connected" is only the whole truth when no glob inside a
|
|
531
|
+
// connected rule is dead and no LOCAL `expectEmpty` marker went live —
|
|
532
|
+
// otherwise the clean line says so (the ✓ is withheld). A pack marker that
|
|
533
|
+
// went live is INFO: the consumer cannot edit it.
|
|
534
|
+
const localWentLive = report.rules.reduce((n, r) => n + coverageWentLiveUnits(r).filter((u) => u.mark?.packageName === undefined).length, 0);
|
|
535
|
+
const stale = [
|
|
536
|
+
...(report.deadGlobCount > 0
|
|
537
|
+
? [`${report.deadGlobCount} glob(s) inside connected rules select or exclude nothing`]
|
|
538
|
+
: []),
|
|
539
|
+
...(localWentLive > 0 ? [`${localWentLive} expectEmpty unit(s) went live — remove the markers`] : []),
|
|
540
|
+
];
|
|
541
|
+
const clean = stale.length > 0
|
|
542
|
+
? `Every rule is connected to something, but ${stale.join(', and ')} (⚠ above).\n` +
|
|
543
|
+
` Fix them — or pass --${FAIL_ON_DEAD_UNITS_FLAG} to make a dead or went-live unit fail the run.`
|
|
544
|
+
: 'Every rule is connected to something. ✓';
|
|
545
|
+
const line = verdictLine(env, clean, lead);
|
|
546
|
+
if (line)
|
|
547
|
+
process.stdout.write(`\n${line}\n`);
|
|
548
|
+
if (exit === ExitCode.Failure && failOnDeadUnits && stale.length > 0) {
|
|
549
|
+
process.stdout.write(`\n${stale.join('; ')} — and --${FAIL_ON_DEAD_UNITS_FLAG} is set — FAILED.\n`);
|
|
223
550
|
}
|
|
224
551
|
return exit;
|
|
225
552
|
},
|
|
226
553
|
};
|
|
554
|
+
export const gatesCheckCommand = {
|
|
555
|
+
name: 'check',
|
|
556
|
+
description: 'Run EVERY data-defined rule plane\'s violation check in one pass — one exit code, one JSON envelope. The CI / pre-commit primitive. Distinct from `gates coverage` (are the rules still connected) and `shrk quality` (the whole pre-PR bundle).',
|
|
557
|
+
usage: 'shrk gates check [--plane <p>] [--only <ids>] [--changed-only | --since <ref>] [--no-spawn] [--allow-empty] [--strict] [--json]',
|
|
558
|
+
booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-spawn', 'no-cache', ALLOW_EMPTY_FLAG]),
|
|
559
|
+
flags: new Set([...GATES_FLAGS, ...CHECK_ONLY_FLAGS]),
|
|
560
|
+
async run(args) {
|
|
561
|
+
const prep = await prepare(args);
|
|
562
|
+
if (!prep.ok)
|
|
563
|
+
return prep.code;
|
|
564
|
+
const planes = parsePlanes(args);
|
|
565
|
+
if (!planes.ok)
|
|
566
|
+
return ExitCode.UsageError;
|
|
567
|
+
const json = flagBool(args, 'json');
|
|
568
|
+
const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'), prep.value.rejectedRules);
|
|
569
|
+
if (!filtered.ok)
|
|
570
|
+
return ExitCode.UsageError;
|
|
571
|
+
// A pack rule the merge seam rejected is a configured rule that did NOT run
|
|
572
|
+
// (round 12 review, R12-X1): an ERRORED row in the results and the run
|
|
573
|
+
// coverage — never dropped with an advisory `!` line under a ✓. Scope never
|
|
574
|
+
// narrows it out: it has no footprint, and a broken config is broken
|
|
575
|
+
// whatever changed.
|
|
576
|
+
const rejected = filtered.rejected;
|
|
577
|
+
if (filtered.rules.length === 0 && rejected.length === 0) {
|
|
578
|
+
return writeNoRulesVerdict('gates check', args, json, true);
|
|
579
|
+
}
|
|
580
|
+
const scope = resolveScope(args, prep.value.cwd);
|
|
581
|
+
if (scope.error) {
|
|
582
|
+
process.stderr.write(`Cannot scope to the changeset: ${scope.error}\n`);
|
|
583
|
+
return ExitCode.UsageError;
|
|
584
|
+
}
|
|
585
|
+
const { selected, skippedByScope } = narrowToScope(filtered.rules, scope.files);
|
|
586
|
+
const noSpawn = flagBool(args, 'no-spawn');
|
|
587
|
+
const planeRun = selected.length === 0
|
|
588
|
+
? { results: [], diagnostics: [] }
|
|
589
|
+
: runGatePlanes(selected, {
|
|
590
|
+
cwd: prep.value.cwd,
|
|
591
|
+
excludeDirs: prep.value.excludeDirs,
|
|
592
|
+
...(scope.files ? { changedFiles: scope.files } : {}),
|
|
593
|
+
...(noSpawn ? { noSpawn: true } : {}),
|
|
594
|
+
...(await inspectionIfNeeded(prep.value.cwd, selected).then((i) => i ? { inspection: i } : {})),
|
|
595
|
+
});
|
|
596
|
+
const run = { ...planeRun, results: [...planeRun.results, ...rejected] };
|
|
597
|
+
const inScope = selected.length + rejected.length;
|
|
598
|
+
// Only an ERROR-severity failure blocks. A warning-severity rule reports
|
|
599
|
+
// without failing, exactly as its own plane's verb does.
|
|
600
|
+
//
|
|
601
|
+
// `--strict` promotes those warnings to blocking — the documented switch for
|
|
602
|
+
// a zero-warning CI. It reuses the established local meaning of `--strict`
|
|
603
|
+
// (`shrk check --strict` already does exactly this) rather than inventing a
|
|
604
|
+
// second severity model; the GLOBAL `--strict` promotion of not-verified
|
|
605
|
+
// (`2` → `1`) is applied once in `runCli` and composes with this.
|
|
606
|
+
const strict = flagBool(args, 'strict');
|
|
607
|
+
const failedRules = run.results.filter((r) => r.status === 'failed' || r.status === 'error');
|
|
608
|
+
const blocking = failedRules.filter((r) => strict || r.severity === 'error');
|
|
609
|
+
const failedWarnings = failedRules.filter((r) => r.severity !== 'error');
|
|
610
|
+
// An ERRORED rule proved nothing — it never reached a subject. It is
|
|
611
|
+
// therefore not `evaluated`, whatever its severity; otherwise a
|
|
612
|
+
// warning-severity rule that could not run exits 0 with a green banner,
|
|
613
|
+
// which is the silent-green this engine exists to prevent.
|
|
614
|
+
const evaluated = run.results.filter((r) => r.status !== 'skipped' && r.status !== 'error').length;
|
|
615
|
+
const skipped = run.results.length - evaluated;
|
|
616
|
+
// Exit must match the banner. Two kinds of "didn't run" are NOT the same
|
|
617
|
+
// thing, and conflating them breaks the surface either way:
|
|
618
|
+
// • skipped BY SCOPE — the user asked for the narrowing (`--changed-only`),
|
|
619
|
+
// so the rules outside it are deliberately out of the question. Failing
|
|
620
|
+
// here would make a pre-commit hook exit non-zero on every commit.
|
|
621
|
+
// • skipped BY ACCIDENT — a selector matched nothing, or --no-spawn
|
|
622
|
+
// dropped the drift half. Nobody asked for that, and it is exactly the
|
|
623
|
+
// silent-green this engine exists to prevent, so it is `2`.
|
|
624
|
+
// Matches `check wiring --changed-only`, which draws the same line.
|
|
625
|
+
//
|
|
626
|
+
// An EMPTY selection (no rule's footprint intersects the changeset)
|
|
627
|
+
// proposes 0 and lets the run coverage decide: expected 0 is a shortfall
|
|
628
|
+
// (2) unless --allow-empty accepted it. Proposing 2 here would make the
|
|
629
|
+
// valve unreachable on exactly the case it exists for.
|
|
630
|
+
const proposed = blocking.length > 0
|
|
631
|
+
? ExitCode.Failure
|
|
632
|
+
: (evaluated === 0 && selected.length > 0) || skipped > 0
|
|
633
|
+
? ExitCode.NotVerified
|
|
634
|
+
: ExitCode.VerifiedPass;
|
|
635
|
+
// Settle first, render second. The envelope is built for text AND JSON; a
|
|
636
|
+
// rule that passed over part of its scope (a subset wiring rule whose
|
|
637
|
+
// declared selector never produced some registered tokens) is `partial`, so
|
|
638
|
+
// the run is NOT verified — the scope-narrowed rules are out of `expected`,
|
|
639
|
+
// exactly as they are out of the question.
|
|
640
|
+
const unexamined = run.results
|
|
641
|
+
.filter((r) => r.status === 'skipped' || r.status === 'error')
|
|
642
|
+
.map((r) => r.id);
|
|
643
|
+
const env = buildGateEnvelope('gates check', proposed, run.results, {
|
|
644
|
+
unit: 'rules',
|
|
645
|
+
expected: inScope,
|
|
646
|
+
examined: evaluated,
|
|
647
|
+
...(unexamined.length > 0 ? { unexamined, reason: 'checked nothing or could not run' } : {}),
|
|
648
|
+
...(inScope === 0
|
|
649
|
+
? { reason: `no rule's footprint intersects the changeset (${skippedByScope} skipped by scope)` }
|
|
650
|
+
: {}),
|
|
651
|
+
...allowEmptyValve(args, inScope),
|
|
652
|
+
});
|
|
653
|
+
const exit = env.exit;
|
|
654
|
+
const diagnostics = [...run.diagnostics, ...prep.value.planeDiagnostics];
|
|
655
|
+
if (json) {
|
|
656
|
+
process.stdout.write(asJson({
|
|
657
|
+
schema: SCHEMA,
|
|
658
|
+
configured: filtered.rules.length + rejected.length,
|
|
659
|
+
// Pack rules the merge seam refused — errored rows in `gate.rules`.
|
|
660
|
+
rejected: rejected.length,
|
|
661
|
+
selected: selected.length,
|
|
662
|
+
evaluated,
|
|
663
|
+
skipped,
|
|
664
|
+
skippedByScope,
|
|
665
|
+
...(scope.files ? { scoped: true, changedFiles: scope.files.length } : {}),
|
|
666
|
+
noSpawn,
|
|
667
|
+
strict,
|
|
668
|
+
failed: blocking.length,
|
|
669
|
+
failedWarnings: failedWarnings.length,
|
|
670
|
+
verdict: exit === ExitCode.Failure ? 'errors' : exit === ExitCode.VerifiedPass ? 'pass' : 'not-verified',
|
|
671
|
+
diagnostics,
|
|
672
|
+
exitCode: exit,
|
|
673
|
+
gate: env,
|
|
674
|
+
}) + '\n');
|
|
675
|
+
return exit;
|
|
676
|
+
}
|
|
677
|
+
process.stdout.write(header('Gate check — every rule plane'));
|
|
678
|
+
// Round 13 (K6): a rule accepted as intended-empty examined 0 files — the
|
|
679
|
+
// printed count is the envelope's (`gate.evaluated`, which leaves it out)
|
|
680
|
+
// and the accepted rules are named apart. The local `evaluated` keeps
|
|
681
|
+
// counting them for the run coverage and the "nothing ran" proposal above.
|
|
682
|
+
process.stdout.write(kv('evaluated', `${env.evaluated} of ${filtered.rules.length + rejected.length}` +
|
|
683
|
+
acceptedEmptyNote(env.acceptedEmpty) +
|
|
684
|
+
(skippedByScope > 0 ? ` (${skippedByScope} skipped by scope)` : '') +
|
|
685
|
+
(rejected.length > 0 ? ` (${rejected.length} rejected at the pack-plane merge seam — NOT evaluated)` : '')) + '\n');
|
|
686
|
+
if (scope.files) {
|
|
687
|
+
process.stdout.write(kv('scope', `changed-only (${scope.files.length} file(s))`) + '\n');
|
|
688
|
+
}
|
|
689
|
+
if (noSpawn)
|
|
690
|
+
process.stdout.write(kv('mode', '--no-spawn — shell-executing checks skipped') + '\n');
|
|
691
|
+
process.stdout.write(kv('violations', `${blocking.length} blocking rule(s), ${failedWarnings.length} warning rule(s)` +
|
|
692
|
+
(strict && failedWarnings.length > 0 ? ' — --strict: warnings block' : '')) + '\n\n');
|
|
693
|
+
// THE shared unit-state block (round 13, K2) — the plane verbs' own: a dead
|
|
694
|
+
// unit of a rule that still matched, a LOCAL expectEmpty marker whose
|
|
695
|
+
// target appeared (its row reads ⚠, the ✓ is withheld, the exit is
|
|
696
|
+
// unchanged), a pack marker as INFO.
|
|
697
|
+
const unitNotes = unitStateNotes(planeRun.noteRows ?? []);
|
|
698
|
+
// Rendered from the SETTLED rules, so `partial` and its shortfall show in
|
|
699
|
+
// text exactly as they do in `--json`.
|
|
700
|
+
for (const plane of GATE_PLANES) {
|
|
701
|
+
const inPlane = env.rules.filter((r) => r.type === plane);
|
|
702
|
+
if (inPlane.length === 0)
|
|
703
|
+
continue;
|
|
704
|
+
for (const r of inPlane) {
|
|
705
|
+
const mark = r.status === 'passed'
|
|
706
|
+
? unitNotes.staleIds.has(r.id)
|
|
707
|
+
? '⚠'
|
|
708
|
+
: '✓'
|
|
709
|
+
: r.status === 'partial'
|
|
710
|
+
? '~'
|
|
711
|
+
: r.status === 'skipped'
|
|
712
|
+
? '–'
|
|
713
|
+
: r.severity === 'error'
|
|
714
|
+
? '✗'
|
|
715
|
+
: '!';
|
|
716
|
+
const counts = Object.entries(r.counts).map(([k, v]) => `${k} ${v}`).join(', ');
|
|
717
|
+
process.stdout.write(` ${mark} [${plane}] ${r.id}${counts ? ` (${counts})` : ''}\n`);
|
|
718
|
+
if (r.status === 'skipped' && r.skipReason) {
|
|
719
|
+
process.stdout.write(` SKIPPED — ${r.skipReason}\n`);
|
|
720
|
+
}
|
|
721
|
+
// A failOnEmpty rule that matched nothing FAILS with no violation to
|
|
722
|
+
// list, so its reason is the only line that says why.
|
|
723
|
+
if (r.status === 'failed' && r.skipReason && r.violations.length === 0) {
|
|
724
|
+
process.stdout.write(` FAILED — ${r.skipReason}\n`);
|
|
725
|
+
}
|
|
726
|
+
if (r.status === 'partial' && r.shortfall) {
|
|
727
|
+
process.stdout.write(` PARTIAL — ${r.shortfall}\n`);
|
|
728
|
+
}
|
|
729
|
+
if (r.error)
|
|
730
|
+
process.stdout.write(` ! ${r.error}\n`);
|
|
731
|
+
for (const v of r.violations.slice(0, 10)) {
|
|
732
|
+
const at = v.file ? ` (${v.file}${v.line !== undefined ? `:${v.line}` : ''})` : '';
|
|
733
|
+
process.stdout.write(` • ${v.id}${at}${v.message ? ` — ${v.message}` : ''}\n`);
|
|
734
|
+
}
|
|
735
|
+
if (r.violations.length > 10) {
|
|
736
|
+
process.stdout.write(` … (${r.violations.length - 10} more)\n`);
|
|
737
|
+
}
|
|
738
|
+
const hint = r.violations.find((v) => v.hint)?.hint;
|
|
739
|
+
if (hint && r.violations.length > 0)
|
|
740
|
+
process.stdout.write(` → ${hint}\n`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
for (const d of diagnostics)
|
|
744
|
+
process.stdout.write(` ! ${d}\n`);
|
|
745
|
+
process.stdout.write(unitNotes.text);
|
|
746
|
+
if (strict && failedWarnings.length > 0 && exit === ExitCode.Failure) {
|
|
747
|
+
process.stdout.write(`\n${failedWarnings.length} warning rule(s) reported findings and --strict promoted them to failures.\n`);
|
|
748
|
+
}
|
|
749
|
+
// The final line comes from the SETTLED verdict only. A warning-severity
|
|
750
|
+
// rule reports without blocking, but the banner must still say it FIRED —
|
|
751
|
+
// "everything passed" next to a printed violation is the kind of half-truth
|
|
752
|
+
// that trains people to stop reading the output.
|
|
753
|
+
if (rejected.length > 0) {
|
|
754
|
+
process.stdout.write(`\n${rejected.length} pack rule(s) failed validation at the pack-plane merge seam and never ran — FAILED.\n` +
|
|
755
|
+
' Fix the pack (or override the rule locally); `shrk packs contributions` names every rejected entry.\n');
|
|
756
|
+
}
|
|
757
|
+
// A dead unit or a LOCAL went-live marker listed above withholds the ✓ (K2).
|
|
758
|
+
const clean = qualifyCleanForUnits(inScope === 0
|
|
759
|
+
? 'Nothing in scope — accepted.'
|
|
760
|
+
: failedWarnings.length > 0
|
|
761
|
+
? `No blocking violations, but ${failedWarnings.length} warning rule(s) reported findings.` +
|
|
762
|
+
(skippedByScope > 0 ? `\n(${skippedByScope} rule(s) outside the changeset were not run)` : '')
|
|
763
|
+
: skippedByScope > 0
|
|
764
|
+
? `Every rule in scope passed. ✓ (${skippedByScope} outside the changeset were not run)`
|
|
765
|
+
: 'Every declared rule ran and passed. ✓', unitNotes);
|
|
766
|
+
// A rule its own negations emptied is not a stale selector — its SKIPPED
|
|
767
|
+
// line says so, and the lead must not send the reader to fix a selector
|
|
768
|
+
// that is fine (round 12 review, R12-DOC-2).
|
|
769
|
+
const emptiedByOwn = run.results.filter((r) => r.status === 'skipped' && (r.skipReason ?? '').includes('its own negations')).length;
|
|
770
|
+
const lead = proposed === ExitCode.NotVerified && skipped > 0
|
|
771
|
+
? `${skipped} rule(s) in scope checked NOTHING — this is not a pass.\n` +
|
|
772
|
+
(emptiedByOwn === skipped
|
|
773
|
+
? 'Each was emptied by its own negations (see SKIPPED above) — not a stale selector.'
|
|
774
|
+
: `Run \`shrk gates coverage\` to see which selectors are stale` +
|
|
775
|
+
(emptiedByOwn > 0 ? ` (${emptiedByOwn} emptied by their own negations are not).` : '.'))
|
|
776
|
+
: undefined;
|
|
777
|
+
const line = verdictLine(env, clean, lead);
|
|
778
|
+
if (line)
|
|
779
|
+
process.stdout.write(`\n${line}\n`);
|
|
780
|
+
if (inScope === 0 && exit === ExitCode.NotVerified) {
|
|
781
|
+
process.stdout.write(` Pass --${ALLOW_EMPTY_FLAG} to accept an empty changeset explicitly.\n`);
|
|
782
|
+
}
|
|
783
|
+
return exit;
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
/**
|
|
787
|
+
* The "no rules declared" landing for a VERDICT verb: the same explanation as
|
|
788
|
+
* {@link writeNoRules}, settled through the envelope so the JSON carries `gate`
|
|
789
|
+
* and the exit is `2` — unless the verb honours `--allow-empty` and it was
|
|
790
|
+
* passed, in which case the acceptance is printed.
|
|
791
|
+
*/
|
|
792
|
+
function writeNoRulesVerdict(verb, args, json, honoursAllowEmpty) {
|
|
793
|
+
const env = buildGateEnvelope(verb, ExitCode.VerifiedPass, [], {
|
|
794
|
+
unit: 'rules',
|
|
795
|
+
expected: 0,
|
|
796
|
+
examined: 0,
|
|
797
|
+
reason: 'no data-defined rules are declared (on the selected plane)',
|
|
798
|
+
...(honoursAllowEmpty ? allowEmptyValve(args, 0) : {}),
|
|
799
|
+
});
|
|
800
|
+
if (json) {
|
|
801
|
+
process.stdout.write(asJson({ schema: SCHEMA, rules: [], total: 0, exitCode: env.exit, gate: env }) + '\n');
|
|
802
|
+
return env.exit;
|
|
803
|
+
}
|
|
804
|
+
writeNoRules(false);
|
|
805
|
+
process.stdout.write(`\n${verdictLine(env, 'Nothing declared — accepted.')}\n`);
|
|
806
|
+
if (honoursAllowEmpty && env.exit !== ExitCode.VerifiedPass) {
|
|
807
|
+
process.stdout.write(` Pass --${ALLOW_EMPTY_FLAG} to accept an empty rule set explicitly.\n`);
|
|
808
|
+
}
|
|
809
|
+
return env.exit;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Parse `--wiring 'declared=<glob>:<pattern> registered=<glob>:<pattern>'`.
|
|
813
|
+
*
|
|
814
|
+
* The glob/pattern split is at the FIRST colon: a project-relative POSIX glob
|
|
815
|
+
* never contains one, while a regex often does (`name: '(\w+)'`). Splitting at
|
|
816
|
+
* the last colon turned such a pattern's prefix into the glob, which then
|
|
817
|
+
* matched 0 files — the exact dead end this REPL exists to prevent. `flags`
|
|
818
|
+
* (from `--flags`) applies to BOTH sides, so an anchored inline pattern
|
|
819
|
+
* compiles exactly as the same rule does from config (`flags: 'm'`) — one
|
|
820
|
+
* source shape, one compile path.
|
|
821
|
+
*/
|
|
822
|
+
function parseInlineWiring(spec, flags) {
|
|
823
|
+
const sides = new Map();
|
|
824
|
+
// Split on whitespace that precedes a `<side>=`, so a glob may contain none.
|
|
825
|
+
for (const part of spec.trim().split(/\s+(?=(?:declared|registered)=)/)) {
|
|
826
|
+
const eq = part.indexOf('=');
|
|
827
|
+
if (eq === -1)
|
|
828
|
+
return { error: `"${part}" is not <side>=<glob>:<pattern>` };
|
|
829
|
+
const side = part.slice(0, eq).trim();
|
|
830
|
+
if (side !== 'declared' && side !== 'registered') {
|
|
831
|
+
return { error: `unknown side "${side}" — use declared= or registered=` };
|
|
832
|
+
}
|
|
833
|
+
const rest = part.slice(eq + 1);
|
|
834
|
+
const colon = rest.indexOf(':');
|
|
835
|
+
if (colon <= 0)
|
|
836
|
+
return { error: `"${side}=" needs <glob>:<pattern>` };
|
|
837
|
+
sides.set(side, { files: [rest.slice(0, colon)], match: rest.slice(colon + 1) });
|
|
838
|
+
}
|
|
839
|
+
const declared = sides.get('declared');
|
|
840
|
+
const registered = sides.get('registered');
|
|
841
|
+
if (!declared || !registered) {
|
|
842
|
+
return { error: 'both declared=<glob>:<pattern> and registered=<glob>:<pattern> are required' };
|
|
843
|
+
}
|
|
844
|
+
const toSource = (side) => ({
|
|
845
|
+
files: side.files,
|
|
846
|
+
extract: 'regex-capture',
|
|
847
|
+
pattern: side.match,
|
|
848
|
+
...(flags !== undefined && flags.length > 0 ? { flags } : {}),
|
|
849
|
+
});
|
|
850
|
+
return {
|
|
851
|
+
rule: { id: '(try)', declared: toSource(declared), registered: toSource(registered) },
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/** True when `pattern` holds a `^` or `$` outside a character class (an escape skips the next char). */
|
|
855
|
+
function hasLineAnchor(pattern) {
|
|
856
|
+
let inClass = false;
|
|
857
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
858
|
+
const ch = pattern[i];
|
|
859
|
+
if (ch === '\\') {
|
|
860
|
+
i += 1;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (inClass) {
|
|
864
|
+
if (ch === ']')
|
|
865
|
+
inClass = false;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (ch === '[') {
|
|
869
|
+
inClass = true;
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (ch === '^' || ch === '$')
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Advisory notes on a candidate's patterns. A `^`/`$` anchor without the `m`
|
|
879
|
+
* flag matches only at the start/end of the whole FILE (every engine compiles
|
|
880
|
+
* with `g` plus exactly the flags the rule spells) — the "0 matches" that sends
|
|
881
|
+
* an author off rewriting a correct pattern. Config and the dry-run behave the
|
|
882
|
+
* same way; the note says how to spell the flag on each.
|
|
883
|
+
*/
|
|
884
|
+
function anchorNotes(view, inline) {
|
|
885
|
+
const how = inline ? 'pass --flags m' : "spell flags: 'm' on it, as a config rule would";
|
|
886
|
+
const notes = [];
|
|
887
|
+
const check = (label, pattern, flags) => {
|
|
888
|
+
if (pattern === undefined || !hasLineAnchor(pattern) || (flags ?? '').includes('m'))
|
|
889
|
+
return;
|
|
890
|
+
notes.push(`${label}: /${pattern}/ has a ^/$ anchor but no m flag, so it matches only at file start/end — ${how}`);
|
|
891
|
+
};
|
|
892
|
+
for (const { label, source } of gateRuleLabeledSources(view))
|
|
893
|
+
check(label, source.pattern, source.flags);
|
|
894
|
+
if (view.plane === 'policy') {
|
|
895
|
+
const rule = view.raw;
|
|
896
|
+
check('pattern', rule.pattern, rule.flags);
|
|
897
|
+
}
|
|
898
|
+
return notes;
|
|
899
|
+
}
|
|
900
|
+
/** The `selfTest` summary `gates try --json` carries, for every plane. */
|
|
901
|
+
function selfTestPayload(cov, declared) {
|
|
902
|
+
const checks = cov.selfTestChecks ?? [];
|
|
903
|
+
return {
|
|
904
|
+
declared,
|
|
905
|
+
evaluated: checks.length > 0 && checks.every((c) => c.status !== 'not-evaluable'),
|
|
906
|
+
held: checks.filter((c) => c.status === 'held').length,
|
|
907
|
+
failed: checks.filter((c) => c.status === 'failed').length,
|
|
908
|
+
notEvaluable: checks.filter((c) => c.status === 'not-evaluable').length,
|
|
909
|
+
checks,
|
|
910
|
+
failures: cov.expectationFailures,
|
|
911
|
+
consulted: cov.consulted,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
/** The `selfTest` block of `gates try`: EVERY expectation with its result, from the one evaluator. */
|
|
915
|
+
function writeSelfTestBlock(ruleId, cov, declared) {
|
|
916
|
+
const checks = cov.selfTestChecks ?? [];
|
|
917
|
+
if (!declared) {
|
|
918
|
+
process.stdout.write(`\n selfTest: none declared — once the rule is in config, \`shrk gates scaffold-selftest ${ruleId}\` writes one from what it matches.\n`);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (checks.length === 0) {
|
|
922
|
+
process.stdout.write('\n selfTest: declared with no expectations — it asserts nothing.\n');
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const failed = checks.filter((c) => c.status === 'failed').length;
|
|
926
|
+
const unmeasured = checks.filter((c) => c.status === 'not-evaluable').length;
|
|
927
|
+
process.stdout.write(`\n selfTest (${checks.length} expectation(s)` +
|
|
928
|
+
(failed > 0 ? `, ${failed} FAILED` : '') +
|
|
929
|
+
(unmeasured > 0 ? `, ${unmeasured} NOT evaluable` : '') +
|
|
930
|
+
'):\n');
|
|
931
|
+
for (const c of checks) {
|
|
932
|
+
const tag = c.status === 'held' ? 'held ' : c.status === 'failed' ? 'FAILED ' : 'NOT EVALUABLE';
|
|
933
|
+
process.stdout.write(` ${tag} ${c.field}: ${c.message}\n`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function writeNotes(notes) {
|
|
937
|
+
for (const n of notes)
|
|
938
|
+
process.stdout.write(`\n note: ${n}\n`);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* The dry-run's final line, from its SETTLED verdict: a failure says what
|
|
942
|
+
* broke, a not-verified run names what it did not examine, and only a clean
|
|
943
|
+
* run gets the clean sentence.
|
|
944
|
+
*/
|
|
945
|
+
function writeTryVerdict(settled, cov, lead) {
|
|
946
|
+
// Round 13: the text prints the selector units its `--json` already carries
|
|
947
|
+
// — the rule-authoring REPL is exactly where a planned glob gets written, so
|
|
948
|
+
// a dead glob, a went-live marker or an accepted planned unit is never
|
|
949
|
+
// silent here.
|
|
950
|
+
const dead = reportsDeadGlobs(cov) ? coverageDeadUnits(cov) : [];
|
|
951
|
+
if (dead.length > 0) {
|
|
952
|
+
process.stdout.write(`\n ⚠ ${dead.length} of ${cov.globsChecked} glob(s) dead: ${formatDeadUnits(dead)}\n`);
|
|
953
|
+
// The causes ONCE, as a footer — exactly as `gates coverage` prints them.
|
|
954
|
+
process.stdout.write(` Dead selectors: ${DEAD_SELECTOR_CAUSES}.\n`);
|
|
955
|
+
}
|
|
956
|
+
const wentLive = coverageWentLiveUnits(cov);
|
|
957
|
+
for (const u of wentLive) {
|
|
958
|
+
process.stdout.write(` ${u.mark?.packageName !== undefined ? 'i' : '⚠'} ${formatUnitLiveness(u, { causes: false })}\n`);
|
|
959
|
+
}
|
|
960
|
+
// Each intended-empty unit as its ONE per-unit line under a neutral bullet —
|
|
961
|
+
// never a ✓: the ✓ and the acceptance belong to `verdictLine` (exit 0 only),
|
|
962
|
+
// so a FAILED or SKIPPED candidate never reads "asserted empty ✓".
|
|
963
|
+
for (const u of (cov.unitLiveness ?? []).filter((x) => x.state === UnitLivenessState.IntendedEmpty)) {
|
|
964
|
+
process.stdout.write(` · ${formatUnitLiveness(u, { causes: false })}\n`);
|
|
965
|
+
}
|
|
966
|
+
if (settled.exit === ExitCode.Failure) {
|
|
967
|
+
const why = cov.status === 'error'
|
|
968
|
+
? `the candidate is misconfigured: ${cov.error ?? 'it could not run'}`
|
|
969
|
+
: `${cov.expectationFailures.length} selfTest expectation(s) failed (listed above)`;
|
|
970
|
+
process.stdout.write(`\n FAILED — ${why}.\n`);
|
|
971
|
+
}
|
|
972
|
+
// "Connected over everything" is only the whole truth when no glob inside
|
|
973
|
+
// the candidate is dead and no marker went live.
|
|
974
|
+
const stale = [
|
|
975
|
+
...(dead.length > 0 ? [`${dead.length} glob(s) inside it select or exclude nothing`] : []),
|
|
976
|
+
...(wentLive.length > 0 ? [`${wentLive.length} expectEmpty unit(s) went live`] : []),
|
|
977
|
+
];
|
|
978
|
+
const clean = stale.length > 0
|
|
979
|
+
? `The candidate is connected, but ${stale.join(' and ')} (⚠ above) — fix them before adding it to config.`
|
|
980
|
+
: (cov.selfTestChecks ?? []).length > 0
|
|
981
|
+
? 'The candidate is connected over everything it was asked to examine, and every selfTest expectation held.'
|
|
982
|
+
: 'The candidate is connected over everything it was asked to examine.';
|
|
983
|
+
const line = verdictLine(settled, clean, lead);
|
|
984
|
+
if (line)
|
|
985
|
+
process.stdout.write(`\n ${line.split('\n').join('\n ')}\n`);
|
|
986
|
+
}
|
|
987
|
+
export const gatesTryCommand = {
|
|
988
|
+
name: 'try',
|
|
989
|
+
description: "Dry-run a rule spec against the live tree WITHOUT adding it to config — the rule-authoring REPL. Prints the resolved sets and the diff, and evaluates the candidate's selfTest with the SAME evaluator `gates coverage` uses (exit 1 when an expectation fails), so a selector is tightened before it is committed.",
|
|
990
|
+
usage: "shrk gates try --rule-file <rule.json> [--plane <p>] [--full | --limit N] | --wiring 'declared=<glob>:<pat> registered=<glob>:<pat>' [--flags <f>] [--json]",
|
|
991
|
+
booleanFlags: new Set(['json', 'full']),
|
|
992
|
+
flags: new Set([...GATES_FLAGS, ...TRY_ONLY_FLAGS]),
|
|
993
|
+
async run(args) {
|
|
994
|
+
// `--flags` with no value (or an empty one) would otherwise be dropped in
|
|
995
|
+
// silence: the run goes ahead with no flags, looking as if it took them.
|
|
996
|
+
const flagsRaw = args.flags.get('flags');
|
|
997
|
+
if (flagsRaw === true || flagsRaw === '') {
|
|
998
|
+
process.stderr.write('--flags needs a value, e.g. --flags m\n');
|
|
999
|
+
return ExitCode.UsageError;
|
|
1000
|
+
}
|
|
1001
|
+
const ruleFile = flagString(args, 'rule-file') ?? args.positional[0];
|
|
1002
|
+
const inline = flagString(args, 'wiring');
|
|
1003
|
+
const inlineFlags = flagString(args, 'flags');
|
|
1004
|
+
if (!ruleFile && !inline) {
|
|
1005
|
+
process.stderr.write('Usage: shrk gates try --rule-file <rule.json> [--plane <p>] [--full | --limit N]\n' +
|
|
1006
|
+
" shrk gates try --wiring 'declared=<glob>:<pat> registered=<glob>:<pat>' [--flags <f>]\n" +
|
|
1007
|
+
' Runs the extraction, evaluates the selfTest and prints the resolved sets — config is never touched.\n' +
|
|
1008
|
+
' --full dump the WHOLE extracted set (default: first 5)\n' +
|
|
1009
|
+
' --limit N dump the first N\n' +
|
|
1010
|
+
' --flags <f> regex flags for BOTH inline patterns (m: a ^/$ anchor matches per line)\n');
|
|
1011
|
+
return ExitCode.UsageError;
|
|
1012
|
+
}
|
|
1013
|
+
// A flag that changes nothing must never look like a satisfied request: a
|
|
1014
|
+
// --rule-file spells `flags` per source, exactly as config does.
|
|
1015
|
+
if (inlineFlags !== undefined && !inline) {
|
|
1016
|
+
process.stderr.write("--flags applies to the inline --wiring form only; a --rule-file spells `flags: '<f>'` on each source, as config does.\n");
|
|
1017
|
+
return ExitCode.UsageError;
|
|
1018
|
+
}
|
|
1019
|
+
const cwd = resolveCwd(args);
|
|
1020
|
+
const json = flagBool(args, 'json');
|
|
1021
|
+
// A candidate may `$use` the project's shared extractors, so the real
|
|
1022
|
+
// config is loaded for its `extractors` map (and for nothing else — no
|
|
1023
|
+
// declared rule is read, and nothing is written).
|
|
1024
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
1025
|
+
const extractors = loaded.ok ? (loaded.value.config.extractors ?? {}) : {};
|
|
1026
|
+
const excludeDirs = loaded.ok ? planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir) : [];
|
|
1027
|
+
let raw;
|
|
1028
|
+
let plane = 'wiring';
|
|
1029
|
+
if (inline) {
|
|
1030
|
+
const parsed = parseInlineWiring(inline, inlineFlags);
|
|
1031
|
+
if (parsed.error) {
|
|
1032
|
+
process.stderr.write(`Invalid --wiring spec: ${parsed.error}\n`);
|
|
1033
|
+
return ExitCode.UsageError;
|
|
1034
|
+
}
|
|
1035
|
+
raw = parsed.rule;
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
if (!existsSync(ruleFile)) {
|
|
1039
|
+
process.stderr.write(`Rule file not found: ${ruleFile}\n`);
|
|
1040
|
+
return ExitCode.UsageError;
|
|
1041
|
+
}
|
|
1042
|
+
try {
|
|
1043
|
+
raw = JSON.parse(readFileSync(ruleFile, 'utf8'));
|
|
1044
|
+
}
|
|
1045
|
+
catch (e) {
|
|
1046
|
+
process.stderr.write(`${ruleFile} is not valid JSON: ${e.message}\n`);
|
|
1047
|
+
return ExitCode.UsageError;
|
|
1048
|
+
}
|
|
1049
|
+
const planes = parsePlanes(args);
|
|
1050
|
+
if (!planes.ok)
|
|
1051
|
+
return ExitCode.UsageError;
|
|
1052
|
+
const explicit = planes.planes ? [...planes.planes][0] : undefined;
|
|
1053
|
+
const inferred = explicit ?? inferPlane(raw);
|
|
1054
|
+
if (!inferred) {
|
|
1055
|
+
process.stderr.write('Could not infer the plane from the rule shape. Pass --plane wiring|policy|registry|registration|baseline|generated.\n');
|
|
1056
|
+
return ExitCode.UsageError;
|
|
1057
|
+
}
|
|
1058
|
+
plane = inferred;
|
|
1059
|
+
}
|
|
1060
|
+
// Validate with the SAME schema the loader uses, so a spec that passes here
|
|
1061
|
+
// is a spec that will load — the point of the REPL is that what you see is
|
|
1062
|
+
// what you get once you paste it in.
|
|
1063
|
+
const schema = PLANE_SCHEMAS[plane];
|
|
1064
|
+
const parsed = schema.safeParse(raw);
|
|
1065
|
+
if (!parsed.success) {
|
|
1066
|
+
const summary = (parsed.error?.issues ?? [])
|
|
1067
|
+
.map((iss) => `${iss.path.join('.') || '<root>'}: ${iss.message}`)
|
|
1068
|
+
.join('; ');
|
|
1069
|
+
if (json)
|
|
1070
|
+
process.stdout.write(asJson({ schema: SCHEMA, plane, valid: false, error: summary }) + '\n');
|
|
1071
|
+
else
|
|
1072
|
+
process.stderr.write(`Invalid ${plane} rule: ${summary}\n`);
|
|
1073
|
+
return ExitCode.UsageError;
|
|
1074
|
+
}
|
|
1075
|
+
// Normalise the candidate's markable lists exactly as the loader does
|
|
1076
|
+
// (round 13, `normalizePlaneRule`): plain string lists plus the
|
|
1077
|
+
// `expectEmptyUnits` ledger, so a planned `{ pattern, expectEmpty: true }`
|
|
1078
|
+
// unit is judged here as it will be once pasted into config.
|
|
1079
|
+
const normalizedCandidate = normalizePlaneRule(plane, parsed.data);
|
|
1080
|
+
if (!normalizedCandidate.ok) {
|
|
1081
|
+
const summary = normalizedCandidate.error.message;
|
|
1082
|
+
if (json)
|
|
1083
|
+
process.stdout.write(asJson({ schema: SCHEMA, plane, valid: false, error: summary }) + '\n');
|
|
1084
|
+
else
|
|
1085
|
+
process.stderr.write(`Invalid ${plane} rule: ${summary}\n`);
|
|
1086
|
+
return ExitCode.UsageError;
|
|
1087
|
+
}
|
|
1088
|
+
const candidate = normalizedCandidate.value;
|
|
1089
|
+
// Resolve `$use` exactly as the loader would, so a candidate referencing a
|
|
1090
|
+
// shared extractor is tried against the real shared definition.
|
|
1091
|
+
const planeKey = PLANE_CONFIG_KEY[plane];
|
|
1092
|
+
const resolved = resolvePlaneExtractors({ [planeKey]: [candidate] }, extractors);
|
|
1093
|
+
if (resolved.errors.length > 0) {
|
|
1094
|
+
const summary = resolved.errors.map((e) => e.message).join('; ');
|
|
1095
|
+
if (json)
|
|
1096
|
+
process.stdout.write(asJson({ schema: SCHEMA, plane, valid: false, error: summary }) + '\n');
|
|
1097
|
+
else
|
|
1098
|
+
process.stderr.write(`Unresolvable extractor reference: ${summary}\n`);
|
|
1099
|
+
return ExitCode.UsageError;
|
|
1100
|
+
}
|
|
1101
|
+
// …and judged on its MERGED shape, as the loader judges a local rule (THE
|
|
1102
|
+
// post-resolution check, core) — a candidate that would fail config load is
|
|
1103
|
+
// refused here, never tried as "misconfigured" (round 12 review, R12-X3).
|
|
1104
|
+
const mergedProblems = validateResolvedPlaneSources(resolved);
|
|
1105
|
+
if (mergedProblems.length > 0) {
|
|
1106
|
+
const summary = mergedProblems.map((p) => `${p.path} ${p.message}`).join('; ');
|
|
1107
|
+
if (json)
|
|
1108
|
+
process.stdout.write(asJson({ schema: SCHEMA, plane, valid: false, error: summary }) + '\n');
|
|
1109
|
+
else
|
|
1110
|
+
process.stderr.write(`Invalid ${plane} rule: ${summary}\n`);
|
|
1111
|
+
return ExitCode.UsageError;
|
|
1112
|
+
}
|
|
1113
|
+
const resolvedByKey = resolved;
|
|
1114
|
+
const rule = resolvedByKey[planeKey]?.[0] ?? candidate;
|
|
1115
|
+
// The candidate's view comes from the ONE normaliser the loader's rules go
|
|
1116
|
+
// through, so severity, failOnEmpty and selfTest mean here exactly what
|
|
1117
|
+
// they will mean once the rule is pasted into config. A hand-built view
|
|
1118
|
+
// was how the dry-run discarded `selfTest` without a word.
|
|
1119
|
+
const view = collectGateRules({ [planeKey]: [rule] })[0];
|
|
1120
|
+
if (!view) {
|
|
1121
|
+
process.stderr.write(`The candidate did not normalise to a ${plane} rule.\n`);
|
|
1122
|
+
return ExitCode.UsageError;
|
|
1123
|
+
}
|
|
1124
|
+
const inspection = await inspectionIfNeeded(cwd, [view]);
|
|
1125
|
+
// What the candidate MATCHED, and its selfTest — through the same plane
|
|
1126
|
+
// adapters and the same evaluator `gates coverage` runs.
|
|
1127
|
+
const cov = buildGateCoverage(cwd, [view], excludeDirs, extractors, true, inspection).rules[0];
|
|
1128
|
+
const declared = view.selfTest !== undefined;
|
|
1129
|
+
const notes = anchorNotes(view, inline !== undefined);
|
|
1130
|
+
// A broken selfTest, or a candidate that cannot run, fails the dry-run.
|
|
1131
|
+
// Violations do NOT: finding them is the rule doing its job.
|
|
1132
|
+
const broken = cov.status === 'error' || cov.status === 'failed-expectation';
|
|
1133
|
+
// The wiring plane already has a BOTH-SIDES explainer (resolved declared
|
|
1134
|
+
// set, resolved registered set, and the set-difference between them) — the
|
|
1135
|
+
// exact view an author tightening a selector needs. Reuse it rather than
|
|
1136
|
+
// printing a second, thinner one that could disagree with `gates explain`.
|
|
1137
|
+
if (plane === 'wiring') {
|
|
1138
|
+
const explain = explainWiring(cwd, rule, { excludeDirs });
|
|
1139
|
+
// An empty side means the selector proved nothing — not a pass — unless
|
|
1140
|
+
// the empty result is the INTENDED one (round 13: every source-side glob
|
|
1141
|
+
// marked `expectEmpty`, accepted by the one settle). A partial candidate
|
|
1142
|
+
// settles to 2 on the engine's own coverage, the guard every verdict verb
|
|
1143
|
+
// uses; the candidate's `expectEmpty` acceptance joins it (the one fold,
|
|
1144
|
+
// `ruleVerdictRecords`), so it is printed at exit 0.
|
|
1145
|
+
const acceptedEmpty = cov.emptiness === RuleEmptiness.IntendedEmpty || cov.emptiness === RuleEmptiness.AssertedEmptyOutput;
|
|
1146
|
+
const emptySide = !acceptedEmpty && (explain.declared.distinctCount === 0 || explain.registered.distinctCount === 0);
|
|
1147
|
+
const settled = settleVerdict(broken ? ExitCode.Failure : emptySide ? ExitCode.NotVerified : ExitCode.VerifiedPass,
|
|
1148
|
+
// The engine's own acceptance (the explain now settles a planned rule
|
|
1149
|
+
// exactly as `check wiring` does); coverage's is the same claim.
|
|
1150
|
+
ruleVerdictRecords(explain.coverage, explain.unitAcceptance ?? cov.unitAcceptance));
|
|
1151
|
+
if (json) {
|
|
1152
|
+
// The explain payload stays top-level (existing consumers keep
|
|
1153
|
+
// working); the selfTest and the settled outcome are additive keys.
|
|
1154
|
+
process.stdout.write(asJson({
|
|
1155
|
+
...explain,
|
|
1156
|
+
selfTest: selfTestPayload(cov, declared),
|
|
1157
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
1158
|
+
exitCode: settled.exit,
|
|
1159
|
+
settled,
|
|
1160
|
+
}) + '\n');
|
|
1161
|
+
return settled.exit;
|
|
1162
|
+
}
|
|
1163
|
+
renderWiringExplain(explain, false);
|
|
1164
|
+
writeSelfTestBlock(view.id, cov, declared);
|
|
1165
|
+
writeNotes(notes);
|
|
1166
|
+
process.stdout.write('\n Nothing was written. Paste the rule into `wiringRules[]` to keep it.\n');
|
|
1167
|
+
writeTryVerdict(settled, cov, emptySide ? 'A side of the candidate matched nothing — it proved nothing.' : undefined);
|
|
1168
|
+
return settled.exit;
|
|
1169
|
+
}
|
|
1170
|
+
// `noSpawn` is not a performance choice here: a `--rule-file` is arbitrary
|
|
1171
|
+
// JSON, and honouring a `regen` / `compute.run` from it would turn a
|
|
1172
|
+
// read-only preview into shell execution from an untrusted file.
|
|
1173
|
+
const run = runGatePlanes([view], {
|
|
1174
|
+
cwd,
|
|
1175
|
+
excludeDirs,
|
|
1176
|
+
noSpawn: true,
|
|
1177
|
+
...(inspection ? { inspection } : {}),
|
|
1178
|
+
});
|
|
1179
|
+
const result = run.results[0];
|
|
1180
|
+
const allIds = cov.allIds ?? cov.sampleIds;
|
|
1181
|
+
// Settled over what the candidate matched AND what its check examined (a
|
|
1182
|
+
// skipped shell half, an unreadable ledger), so a dry-run never reads
|
|
1183
|
+
// clean over a half it did not examine.
|
|
1184
|
+
// What the candidate matched and what its check examined are often the
|
|
1185
|
+
// SAME gap (one unread file): settle over each distinct shortfall once, so
|
|
1186
|
+
// the NOT VERIFIED line does not repeat it.
|
|
1187
|
+
// Round 13: each side's `expectEmpty` acceptance joins its record (the one
|
|
1188
|
+
// fold, `ruleVerdictRecords`), and a record both sides carry — the same
|
|
1189
|
+
// acceptance, reached by coverage and by the plane check — is settled once.
|
|
1190
|
+
const seenRecord = new Set();
|
|
1191
|
+
const tryCoverage = [
|
|
1192
|
+
...ruleVerdictRecords(cov.coverage, cov.unitAcceptance),
|
|
1193
|
+
...(result ? ruleVerdictRecords(result.coverage, result.unitAcceptance) : []),
|
|
1194
|
+
].filter((c) => {
|
|
1195
|
+
const { subject: _subject, ...claim } = c;
|
|
1196
|
+
const key = JSON.stringify(claim);
|
|
1197
|
+
if (seenRecord.has(key))
|
|
1198
|
+
return false;
|
|
1199
|
+
seenRecord.add(key);
|
|
1200
|
+
return true;
|
|
1201
|
+
});
|
|
1202
|
+
const settled = settleVerdict(broken ? ExitCode.Failure : cov.status === 'empty' ? ExitCode.NotVerified : ExitCode.VerifiedPass, tryCoverage.filter((c, i) => {
|
|
1203
|
+
const s = coverageShortfall(c);
|
|
1204
|
+
return s === undefined || tryCoverage.findIndex((d) => coverageShortfall(d) === s) === i;
|
|
1205
|
+
}));
|
|
1206
|
+
if (json) {
|
|
1207
|
+
process.stdout.write(asJson({
|
|
1208
|
+
schema: 'sharkcraft.gates-try/v1',
|
|
1209
|
+
plane,
|
|
1210
|
+
valid: true,
|
|
1211
|
+
coverage: cov,
|
|
1212
|
+
...(cov.hint ? { hint: cov.hint } : {}),
|
|
1213
|
+
result: result ?? null,
|
|
1214
|
+
selfTest: selfTestPayload(cov, declared),
|
|
1215
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
1216
|
+
note: 'nothing was written — paste the rule into sharkcraft.config.ts to keep it',
|
|
1217
|
+
exitCode: settled.exit,
|
|
1218
|
+
settled,
|
|
1219
|
+
}) + '\n');
|
|
1220
|
+
return settled.exit;
|
|
1221
|
+
}
|
|
1222
|
+
process.stdout.write(header(`gates try — candidate ${plane} rule "${view.id}"`));
|
|
1223
|
+
// `filesMatched` counts the files the rule READ: a glob-matched file over
|
|
1224
|
+
// the read cap is in the coverage line below, never in this count.
|
|
1225
|
+
process.stdout.write(kv('files read', String(cov.filesMatched)) + '\n');
|
|
1226
|
+
process.stdout.write(kv(cov.unitLabel, String(cov.unitsMatched)) + '\n');
|
|
1227
|
+
process.stdout.write(kv('coverage', formatCoverage(cov.coverage)) + '\n');
|
|
1228
|
+
if (cov.viaExtractor)
|
|
1229
|
+
process.stdout.write(kv('via extractor', `$use:${cov.viaExtractor}`) + '\n');
|
|
1230
|
+
// Tuning a selector against a large tree needs the WHOLE candidate set —
|
|
1231
|
+
// a five-item sample cannot tell you whether the tail is right.
|
|
1232
|
+
const full = flagBool(args, 'full');
|
|
1233
|
+
const limitRaw = flagString(args, 'limit');
|
|
1234
|
+
const limit = limitRaw !== undefined ? Number.parseInt(limitRaw, 10) : undefined;
|
|
1235
|
+
if (limitRaw !== undefined && (!Number.isFinite(limit) || (limit ?? 0) < 1)) {
|
|
1236
|
+
process.stderr.write(`--limit must be a positive integer, got "${limitRaw}".\n`);
|
|
1237
|
+
return ExitCode.UsageError;
|
|
1238
|
+
}
|
|
1239
|
+
const shown = full ? allIds : allIds.slice(0, limit ?? 5);
|
|
1240
|
+
if (shown.length > 0) {
|
|
1241
|
+
const label = shown.length === allIds.length ? `all ${shown.length}` : `first ${shown.length} of ${allIds.length}`;
|
|
1242
|
+
process.stdout.write(`\n extracted (${label}):\n`);
|
|
1243
|
+
for (const id of shown)
|
|
1244
|
+
process.stdout.write(` ${id}\n`);
|
|
1245
|
+
if (shown.length < allIds.length) {
|
|
1246
|
+
process.stdout.write(` … (${allIds.length - shown.length} more — re-run with --full)\n`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (cov.error)
|
|
1250
|
+
process.stdout.write(`\n ! ${cov.error}\n`);
|
|
1251
|
+
if (result) {
|
|
1252
|
+
process.stdout.write(`\n would be: ${result.status}${result.violations.length > 0 ? ` (${result.violations.length} violation(s))` : ''}\n`);
|
|
1253
|
+
for (const v of result.violations.slice(0, 20)) {
|
|
1254
|
+
const at = v.file ? ` (${v.file}${v.line !== undefined ? `:${v.line}` : ''})` : '';
|
|
1255
|
+
process.stdout.write(` • ${v.id}${at}\n`);
|
|
1256
|
+
}
|
|
1257
|
+
if (result.violations.length > 20) {
|
|
1258
|
+
process.stdout.write(` … (${result.violations.length - 20} more)\n`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
writeSelfTestBlock(view.id, cov, declared);
|
|
1262
|
+
writeNotes(notes);
|
|
1263
|
+
if (cov.status === 'empty') {
|
|
1264
|
+
process.stdout.write('\n This selector matched NOTHING — tighten it before adding it to config.\n');
|
|
1265
|
+
if (cov.hint)
|
|
1266
|
+
process.stdout.write(` → ${cov.hint}\n`);
|
|
1267
|
+
process.stdout.write(` Once pasted into config, \`gates coverage\` would exit ` +
|
|
1268
|
+
`${cov.failOnEmpty ? ExitCode.Failure : ExitCode.NotVerified} for it (failOnEmpty=${cov.failOnEmpty}).\n`);
|
|
1269
|
+
}
|
|
1270
|
+
process.stdout.write('\n Nothing was written. Paste the rule into sharkcraft.config.ts to keep it.\n');
|
|
1271
|
+
writeTryVerdict(settled, cov, cov.status === 'empty' ? 'The candidate matched nothing — it proved nothing.' : undefined);
|
|
1272
|
+
return settled.exit;
|
|
1273
|
+
},
|
|
1274
|
+
};
|
|
1275
|
+
/**
|
|
1276
|
+
* Which config key each plane's rules live under, for `$use` resolution — THE
|
|
1277
|
+
* list beside the config schema (`GATE_PLANE_CONFIG_KEY`), never a CLI copy.
|
|
1278
|
+
*/
|
|
1279
|
+
const PLANE_CONFIG_KEY = GATE_PLANE_CONFIG_KEY;
|
|
1280
|
+
/** The loader's own schema for each plane — one validation, not a second one. */
|
|
1281
|
+
const PLANE_SCHEMAS = {
|
|
1282
|
+
wiring: WiringRuleSchema,
|
|
1283
|
+
policy: PolicyRuleSchema,
|
|
1284
|
+
registry: RegistryDeclarationSchema,
|
|
1285
|
+
registration: RegistrationIdiomSchema,
|
|
1286
|
+
baseline: BaselineRuleSchema,
|
|
1287
|
+
generated: GeneratedArtifactRuleSchema,
|
|
1288
|
+
'doc-reference': DocReferenceRuleSchema,
|
|
1289
|
+
};
|
|
1290
|
+
/**
|
|
1291
|
+
* Infer a candidate's plane from its shape.
|
|
1292
|
+
*
|
|
1293
|
+
* Each plane has a field no other plane uses, so inference is exact rather than
|
|
1294
|
+
* a guess — and an ambiguous shape returns `undefined` so the author is asked
|
|
1295
|
+
* with `--plane` instead of being silently run on the wrong engine.
|
|
1296
|
+
*/
|
|
1297
|
+
function inferPlane(raw) {
|
|
1298
|
+
if (typeof raw !== 'object' || raw === null)
|
|
1299
|
+
return undefined;
|
|
1300
|
+
const o = raw;
|
|
1301
|
+
if ('generatedGlob' in o)
|
|
1302
|
+
return 'generated';
|
|
1303
|
+
if ('tokenPattern' in o && 'resolvesAs' in o)
|
|
1304
|
+
return 'doc-reference';
|
|
1305
|
+
if ('compute' in o)
|
|
1306
|
+
return 'baseline';
|
|
1307
|
+
if ('declared' in o && 'provided' in o && 'consumed' in o)
|
|
1308
|
+
return 'registration';
|
|
1309
|
+
if ('declared' in o || 'registered' in o || 'chain' in o)
|
|
1310
|
+
return 'wiring';
|
|
1311
|
+
if ('surface' in o && 'pattern' in o)
|
|
1312
|
+
return 'policy';
|
|
1313
|
+
if ('source' in o && 'name' in o)
|
|
1314
|
+
return 'registry';
|
|
1315
|
+
return undefined;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* THE unit-state block of the registry / registration explain views (round 13,
|
|
1319
|
+
* K6) — the block the wiring and policy explain views print, through the same
|
|
1320
|
+
* shared helper (`unitStateNotes`, each line `formatUnitLiveness`): a dead glob,
|
|
1321
|
+
* a LOCAL marker that went live, a pack marker (INFO) and every intended-empty
|
|
1322
|
+
* unit. The units are settled from THE per-rule request `gates coverage` builds
|
|
1323
|
+
* (`sourceLivenessRequest` over `gateRuleLabeledSources`), so the explain view
|
|
1324
|
+
* cannot name a unit's state differently from the coverage report.
|
|
1325
|
+
*/
|
|
1326
|
+
function writeExplainUnitStates(cwd, view, excludeDirs) {
|
|
1327
|
+
const settled = settleGlobLists(sourceLivenessRequest(cwd, gateRuleLabeledSources(view), excludeDirs, view.id));
|
|
1328
|
+
const unitLiveness = settled.units.filter((u) => u.state !== UnitLivenessState.Live);
|
|
1329
|
+
process.stdout.write(unitStateNotes([{ id: view.id, unitLiveness }], { intendedEmpty: true }).text);
|
|
1330
|
+
}
|
|
227
1331
|
/** Render a registry inventory as the trust-layer explain view. */
|
|
228
|
-
function explainRegistry(cwd,
|
|
1332
|
+
function explainRegistry(cwd, view, excludeDirs) {
|
|
1333
|
+
const decl = view.raw;
|
|
229
1334
|
const inventory = scanRegistry(cwd, decl, { excludeDirs });
|
|
230
1335
|
const insp = inspectSource(cwd, decl.source, excludeDirs);
|
|
1336
|
+
if (decl.source.$use)
|
|
1337
|
+
process.stdout.write(kv('via extractor', `$use:${decl.source.$use}`) + '\n');
|
|
231
1338
|
process.stdout.write(kv('files scanned', String(insp.filesScanned)) + '\n');
|
|
232
1339
|
process.stdout.write(kv('ids', String(inventory.entries.length)) + '\n');
|
|
233
1340
|
for (const e of inventory.entries.slice(0, 60)) {
|
|
@@ -238,16 +1345,19 @@ function explainRegistry(cwd, decl, excludeDirs) {
|
|
|
238
1345
|
}
|
|
239
1346
|
for (const d of inventory.diagnostics)
|
|
240
1347
|
process.stdout.write(` ! ${d}\n`);
|
|
1348
|
+
writeExplainUnitStates(cwd, view, excludeDirs);
|
|
241
1349
|
}
|
|
242
1350
|
/** Render the three sides of a registration idiom. */
|
|
243
|
-
function explainRegistration(cwd,
|
|
1351
|
+
function explainRegistration(cwd, view, excludeDirs) {
|
|
1352
|
+
const idiom = view.raw;
|
|
244
1353
|
for (const [label, source] of [
|
|
245
1354
|
['declared', idiom.declared],
|
|
246
1355
|
['provided', idiom.provided],
|
|
247
1356
|
['consumed', idiom.consumed],
|
|
248
1357
|
]) {
|
|
249
1358
|
const insp = inspectSource(cwd, source, excludeDirs);
|
|
250
|
-
process.stdout.write(kv(label, `${insp.ids.length} token(s) across ${insp.filesScanned} file(s)`
|
|
1359
|
+
process.stdout.write(kv(label, `${insp.ids.length} token(s) across ${insp.filesScanned} file(s)` +
|
|
1360
|
+
(source.$use ? ` (via $use:${source.$use})` : '')) + '\n');
|
|
251
1361
|
if (insp.error)
|
|
252
1362
|
process.stdout.write(` ! ${insp.error}\n`);
|
|
253
1363
|
for (const s of insp.sites.slice(0, 20)) {
|
|
@@ -256,6 +1366,7 @@ function explainRegistration(cwd, idiom, excludeDirs) {
|
|
|
256
1366
|
if (insp.sites.length > 20)
|
|
257
1367
|
process.stdout.write(` … (${insp.sites.length - 20} more)\n`);
|
|
258
1368
|
}
|
|
1369
|
+
writeExplainUnitStates(cwd, view, excludeDirs);
|
|
259
1370
|
process.stdout.write(`\n Query one token's chain with \`shrk wiring chain <token>\`.\n`);
|
|
260
1371
|
}
|
|
261
1372
|
export const gatesExplainCommand = {
|
|
@@ -306,6 +1417,13 @@ export const gatesExplainCommand = {
|
|
|
306
1417
|
args.flags.set('id', view.id);
|
|
307
1418
|
return generatedExplainCommand.run(args);
|
|
308
1419
|
}
|
|
1420
|
+
// The doc-reference plane owns its explain view too — it resolves against
|
|
1421
|
+
// registries, not file globs, so the generic source-based renderer below
|
|
1422
|
+
// has nothing to show for it.
|
|
1423
|
+
if (view.plane === 'doc-reference') {
|
|
1424
|
+
args.flags.set('id', view.id);
|
|
1425
|
+
return docsReferencesExplainCommand.run(args);
|
|
1426
|
+
}
|
|
309
1427
|
if (view.plane === 'wiring') {
|
|
310
1428
|
const explain = explainWiring(prep.value.cwd, view.raw, {
|
|
311
1429
|
excludeDirs: prep.value.excludeDirs,
|
|
@@ -334,10 +1452,10 @@ export const gatesExplainCommand = {
|
|
|
334
1452
|
if (view.description)
|
|
335
1453
|
process.stdout.write(` ${view.description}\n`);
|
|
336
1454
|
if (view.plane === 'registry') {
|
|
337
|
-
explainRegistry(prep.value.cwd, view
|
|
1455
|
+
explainRegistry(prep.value.cwd, view, prep.value.excludeDirs);
|
|
338
1456
|
}
|
|
339
1457
|
else {
|
|
340
|
-
explainRegistration(prep.value.cwd, view
|
|
1458
|
+
explainRegistration(prep.value.cwd, view, prep.value.excludeDirs);
|
|
341
1459
|
}
|
|
342
1460
|
return ExitCode.VerifiedPass;
|
|
343
1461
|
},
|
|
@@ -362,15 +1480,147 @@ export async function tryExplainGateRule(args, id) {
|
|
|
362
1480
|
const forwarded = { ...args, positional: [id] };
|
|
363
1481
|
return gatesExplainCommand.run(forwarded);
|
|
364
1482
|
}
|
|
1483
|
+
export const gatesScaffoldSelfTestCommand = {
|
|
1484
|
+
name: 'scaffold-selftest',
|
|
1485
|
+
description: "Generate a rule's `selfTest` from what it matches TODAY — the fixture the trust layer asks for, turned from a blank page into three lines to review. `--write` inserts it into sharkcraft.config.ts in place.",
|
|
1486
|
+
usage: 'shrk gates scaffold-selftest <ruleId> [--margin N] [--write] [--json]',
|
|
1487
|
+
booleanFlags: new Set(['json', 'write']),
|
|
1488
|
+
async run(args) {
|
|
1489
|
+
const ruleId = args.positional[0];
|
|
1490
|
+
if (!ruleId) {
|
|
1491
|
+
process.stderr.write('Usage: shrk gates scaffold-selftest <ruleId> [--margin N] [--write] [--json]\n');
|
|
1492
|
+
return ExitCode.UsageError;
|
|
1493
|
+
}
|
|
1494
|
+
const prep = await prepare(args);
|
|
1495
|
+
if (!prep.ok)
|
|
1496
|
+
return prep.code;
|
|
1497
|
+
const json = flagBool(args, 'json');
|
|
1498
|
+
const write = flagBool(args, 'write');
|
|
1499
|
+
const marginRaw = flagString(args, 'margin');
|
|
1500
|
+
const margin = marginRaw === undefined ? 20 : Number.parseInt(marginRaw, 10);
|
|
1501
|
+
if (!Number.isFinite(margin) || margin < 0 || margin >= 100) {
|
|
1502
|
+
process.stderr.write(`--margin must be a percentage in [0, 100), got "${marginRaw}".\n`);
|
|
1503
|
+
return ExitCode.UsageError;
|
|
1504
|
+
}
|
|
1505
|
+
const view = prep.value.rules.find((r) => r.id === ruleId);
|
|
1506
|
+
if (!view) {
|
|
1507
|
+
process.stderr.write(`No gate rule with id "${ruleId}". Run \`shrk gates list\` to see every declared rule.\n`);
|
|
1508
|
+
return ExitCode.UsageError;
|
|
1509
|
+
}
|
|
1510
|
+
if (view.selfTest) {
|
|
1511
|
+
process.stderr.write(`Rule "${ruleId}" already declares a selfTest — refusing to overwrite an author's fixture. ` +
|
|
1512
|
+
'Delete it first if you mean to re-scaffold.\n');
|
|
1513
|
+
return ExitCode.UsageError;
|
|
1514
|
+
}
|
|
1515
|
+
const report = buildGateCoverage(prep.value.cwd, [view], prep.value.excludeDirs, prep.value.extractors, true, await inspectionIfNeeded(prep.value.cwd, [view]));
|
|
1516
|
+
const cov = report.rules[0];
|
|
1517
|
+
// Scaffolding from a rule that matches nothing would produce a selfTest
|
|
1518
|
+
// asserting the broken state — pinning the very bug the fixture exists to
|
|
1519
|
+
// catch. Fix the selector first.
|
|
1520
|
+
if (cov.status === 'error' || cov.unitsMatched === 0) {
|
|
1521
|
+
const why = cov.error ?? 'the rule currently matches nothing';
|
|
1522
|
+
if (json) {
|
|
1523
|
+
process.stdout.write(asJson({ schema: SCHEMA, ruleId, error: why }) + '\n');
|
|
1524
|
+
}
|
|
1525
|
+
else {
|
|
1526
|
+
process.stderr.write(`Cannot scaffold from "${ruleId}": ${why}.\n` +
|
|
1527
|
+
` A selfTest built on an empty match set would pin the broken state as correct.\n` +
|
|
1528
|
+
` Fix the selector first — \`shrk gates explain ${ruleId}\` shows what it resolved.\n`);
|
|
1529
|
+
}
|
|
1530
|
+
return ExitCode.NotVerified;
|
|
1531
|
+
}
|
|
1532
|
+
// A count read off an incomplete scan (a file over the read cap, a role
|
|
1533
|
+
// that could not run) is not a measured number: a floor derived from it
|
|
1534
|
+
// pins a set that misses whatever the unexamined part holds. Refuse, and
|
|
1535
|
+
// refuse --write, naming the shortfall: NOT VERIFIED (2).
|
|
1536
|
+
const coverageRecord = { ...cov.coverage, subject: cov.coverage.subject ?? ruleId };
|
|
1537
|
+
if (coverageShortfall(coverageRecord) !== undefined) {
|
|
1538
|
+
const settled = settleVerdict(ExitCode.VerifiedPass, [coverageRecord]);
|
|
1539
|
+
if (json) {
|
|
1540
|
+
process.stdout.write(asJson({
|
|
1541
|
+
schema: SCHEMA,
|
|
1542
|
+
ruleId,
|
|
1543
|
+
error: 'not verified — what the rule matches today was not fully examined',
|
|
1544
|
+
shortfalls: settled.shortfalls,
|
|
1545
|
+
exitCode: settled.exit,
|
|
1546
|
+
}) + '\n');
|
|
1547
|
+
}
|
|
1548
|
+
else {
|
|
1549
|
+
const lead = `Cannot scaffold from "${ruleId}": what it matches today was not fully examined, so ` +
|
|
1550
|
+
`${cov.unitsMatched} ${cov.unitLabel} is not a measured count.${write ? ' --write refused.' : ''}`;
|
|
1551
|
+
process.stdout.write(`${verdictLine(settled, '', lead)}\n`);
|
|
1552
|
+
}
|
|
1553
|
+
return settled.exit;
|
|
1554
|
+
}
|
|
1555
|
+
const scaffold = scaffoldSelfTest(cov, margin);
|
|
1556
|
+
let written;
|
|
1557
|
+
let writeError;
|
|
1558
|
+
if (write) {
|
|
1559
|
+
const configFile = prep.value.configFile;
|
|
1560
|
+
if (!configFile || !existsSync(configFile)) {
|
|
1561
|
+
writeError = 'no local sharkcraft.config.ts to write to';
|
|
1562
|
+
}
|
|
1563
|
+
else {
|
|
1564
|
+
const res = insertSelfTest(readFileSync(configFile, 'utf8'), ruleId, scaffold.snippet);
|
|
1565
|
+
if (!res.ok)
|
|
1566
|
+
writeError = res.error;
|
|
1567
|
+
else {
|
|
1568
|
+
writeFileSync(configFile, res.text, 'utf8');
|
|
1569
|
+
written = configFile;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
if (json) {
|
|
1574
|
+
process.stdout.write(asJson({
|
|
1575
|
+
schema: SCHEMA,
|
|
1576
|
+
...scaffold,
|
|
1577
|
+
...(written ? { written } : {}),
|
|
1578
|
+
...(writeError ? { writeError } : {}),
|
|
1579
|
+
}) + '\n');
|
|
1580
|
+
return writeError ? ExitCode.Failure : ExitCode.VerifiedPass;
|
|
1581
|
+
}
|
|
1582
|
+
process.stdout.write(header(`Scaffolded selfTest — [${scaffold.plane}] ${ruleId}`));
|
|
1583
|
+
process.stdout.write(kv('matches now', `${scaffold.currentCount} ${cov.unitLabel}`) + '\n');
|
|
1584
|
+
process.stdout.write(kv('floor', `${scaffold.expectMatchesAtLeast} (${scaffold.marginPercent}% margin below the current count)`) + '\n');
|
|
1585
|
+
process.stdout.write('\n');
|
|
1586
|
+
for (const line of scaffold.snippet.split('\n'))
|
|
1587
|
+
process.stdout.write(` ${line}\n`);
|
|
1588
|
+
if (scaffold.note)
|
|
1589
|
+
process.stdout.write(`\n note: ${scaffold.note}\n`);
|
|
1590
|
+
if (written) {
|
|
1591
|
+
process.stdout.write(`\n wrote ${nodePath.relative(prep.value.cwd, written)} — review the diff before committing.\n`);
|
|
1592
|
+
}
|
|
1593
|
+
else if (writeError) {
|
|
1594
|
+
process.stdout.write(`\n ! --write refused: ${writeError}\n`);
|
|
1595
|
+
process.stdout.write(' Paste the block above into the rule by hand.\n');
|
|
1596
|
+
}
|
|
1597
|
+
else {
|
|
1598
|
+
process.stdout.write('\n Paste this into the rule, or re-run with --write to insert it.\n');
|
|
1599
|
+
}
|
|
1600
|
+
return writeError ? ExitCode.Failure : ExitCode.VerifiedPass;
|
|
1601
|
+
},
|
|
1602
|
+
};
|
|
365
1603
|
export const gatesCommand = {
|
|
366
1604
|
name: 'gates',
|
|
367
|
-
description: 'Rule-authoring trust layer: list every data-defined rule, show what each one MATCHED (the stale-selector detector),
|
|
368
|
-
usage: 'shrk gates
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
1605
|
+
description: 'Rule-authoring trust layer: run every plane\'s violation check in one pass (`check`), list every data-defined rule, show what each one MATCHED (`coverage` — the stale-selector detector), explain any one of them, dry-run a candidate rule before adding it (`try`), and scaffold its selfTest (`scaffold-selftest`). Only `scaffold-selftest --write` ever touches config. Not `shrk gate`, singular, which runs the quality-gate pipeline.',
|
|
1606
|
+
usage: 'shrk gates check | coverage | list | explain <id> | try --rule-file <f> | scaffold-selftest <id>',
|
|
1607
|
+
// Every subverb is a registered trie child; any other bare token is refused
|
|
1608
|
+
// by the dispatcher guard (closest match named) before this body runs — the
|
|
1609
|
+
// one unknown-subcommand authority. Only a bare `shrk gates` lands here.
|
|
1610
|
+
positionals: PositionalMode.None,
|
|
1611
|
+
booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-spawn']),
|
|
1612
|
+
async run() {
|
|
1613
|
+
process.stderr.write('Usage: shrk gates <subcommand>\n' +
|
|
1614
|
+
' check [--plane <p>] [--only <ids>] [--changed-only|--since <ref>] [--no-spawn]\n' +
|
|
1615
|
+
' run EVERY plane\'s violation check — one exit code (the CI / pre-commit primitive)\n' +
|
|
1616
|
+
' coverage [--plane <p>] [--changed-only|--since <ref>] [--fail-on-dead-units] [--strict]\n' +
|
|
1617
|
+
' what each rule MATCHED — the stale-selector detector (+ each selfTest, + dead globs)\n' +
|
|
1618
|
+
' list [--plane <p>] every declared rule, with severity + empty-match policy\n' +
|
|
1619
|
+
' explain <id> the concrete inputs one rule resolved\n' +
|
|
1620
|
+
" try --rule-file <f> | --wiring 'declared=<g>:<p> registered=<g>:<p>' [--flags <f>]\n" +
|
|
1621
|
+
' dry-run a candidate rule (and its selfTest) WITHOUT touching config\n' +
|
|
1622
|
+
' scaffold-selftest <id> [--margin N] [--write]\n' +
|
|
1623
|
+
" generate the rule's selfTest from what it matches today\n" +
|
|
374
1624
|
'(`shrk gate`, singular, runs the quality-gate pipeline — a different verb.)\n');
|
|
375
1625
|
return ExitCode.UsageError;
|
|
376
1626
|
},
|