@shrkcrft/cli 0.1.0-alpha.30 → 0.1.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asset-preview/apply-asset-preview.js +1 -1
- package/dist/authoring/authoring-kit.d.ts +9 -1
- package/dist/authoring/authoring-kit.d.ts.map +1 -1
- package/dist/authoring/authoring-kit.js +26 -13
- package/dist/bootstrap/unlinked-workspace-dependency.d.ts +11 -0
- package/dist/bootstrap/unlinked-workspace-dependency.d.ts.map +1 -0
- package/dist/bootstrap/unlinked-workspace-dependency.js +99 -0
- package/dist/command-registry.d.ts +100 -1
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +130 -3
- package/dist/commands/api-diff.command.d.ts.map +1 -1
- package/dist/commands/api-diff.command.js +12 -0
- package/dist/commands/apply.command.d.ts.map +1 -1
- package/dist/commands/apply.command.js +3 -0
- package/dist/commands/arch.command.d.ts.map +1 -1
- package/dist/commands/arch.command.js +26 -7
- package/dist/commands/architecture.command.d.ts.map +1 -1
- package/dist/commands/architecture.command.js +8 -2
- package/dist/commands/baseline.command.d.ts +54 -3
- package/dist/commands/baseline.command.d.ts.map +1 -1
- package/dist/commands/baseline.command.js +582 -74
- package/dist/commands/biome.command.d.ts.map +1 -1
- package/dist/commands/biome.command.js +20 -0
- package/dist/commands/boundaries.command.d.ts +9 -1
- package/dist/commands/boundaries.command.d.ts.map +1 -1
- package/dist/commands/boundaries.command.js +88 -20
- package/dist/commands/bundle.command.d.ts.map +1 -1
- package/dist/commands/bundle.command.js +57 -11
- package/dist/commands/cache-align.command.d.ts.map +1 -1
- package/dist/commands/cache-align.command.js +3 -1
- package/dist/commands/changelog-data.d.ts.map +1 -1
- package/dist/commands/changelog-data.js +138 -0
- package/dist/commands/changes.command.d.ts.map +1 -1
- package/dist/commands/changes.command.js +15 -0
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +1241 -322
- package/dist/commands/checks.command.d.ts.map +1 -1
- package/dist/commands/checks.command.js +188 -40
- package/dist/commands/ci.command.d.ts.map +1 -1
- package/dist/commands/ci.command.js +35 -2
- package/dist/commands/code-intel.command.d.ts.map +1 -1
- package/dist/commands/code-intel.command.js +3 -0
- package/dist/commands/command-catalog.d.ts +23 -1
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +152 -35
- package/dist/commands/commands.command.d.ts +21 -3
- package/dist/commands/commands.command.d.ts.map +1 -1
- package/dist/commands/commands.command.js +172 -27
- package/dist/commands/compress.command.d.ts.map +1 -1
- package/dist/commands/compress.command.js +4 -0
- package/dist/commands/constructs.command.d.ts.map +1 -1
- package/dist/commands/constructs.command.js +50 -12
- package/dist/commands/context.command.d.ts.map +1 -1
- package/dist/commands/context.command.js +40 -5
- package/dist/commands/contract-templates.command.d.ts.map +1 -1
- package/dist/commands/contract-templates.command.js +9 -1
- package/dist/commands/contract.command.d.ts.map +1 -1
- package/dist/commands/contract.command.js +35 -0
- package/dist/commands/conventions.command.d.ts.map +1 -1
- package/dist/commands/conventions.command.js +189 -29
- package/dist/commands/coverage.command.d.ts.map +1 -1
- package/dist/commands/coverage.command.js +9 -0
- package/dist/commands/daily.commands.d.ts +0 -1
- package/dist/commands/daily.commands.d.ts.map +1 -1
- package/dist/commands/daily.commands.js +12 -76
- package/dist/commands/dashboard.command.d.ts.map +1 -1
- package/dist/commands/dashboard.command.js +10 -0
- package/dist/commands/dev.command.d.ts.map +1 -1
- package/dist/commands/dev.command.js +45 -2
- package/dist/commands/diagnostics.command.d.ts +0 -2
- package/dist/commands/diagnostics.command.d.ts.map +1 -1
- package/dist/commands/diagnostics.command.js +5 -78
- package/dist/commands/diff-check.command.d.ts +6 -4
- package/dist/commands/diff-check.command.d.ts.map +1 -1
- package/dist/commands/diff-check.command.js +190 -103
- package/dist/commands/docs-references.command.d.ts +13 -0
- package/dist/commands/docs-references.command.d.ts.map +1 -1
- package/dist/commands/docs-references.command.js +269 -72
- package/dist/commands/doctor.command.d.ts.map +1 -1
- package/dist/commands/doctor.command.js +58 -4
- package/dist/commands/drift.command.d.ts.map +1 -1
- package/dist/commands/drift.command.js +17 -4
- package/dist/commands/eslint.command.d.ts.map +1 -1
- package/dist/commands/eslint.command.js +20 -0
- package/dist/commands/export.command.d.ts.map +1 -1
- package/dist/commands/export.command.js +23 -1
- package/dist/commands/feedback-dispatch.command.d.ts.map +1 -1
- package/dist/commands/feedback-dispatch.command.js +29 -0
- package/dist/commands/feedback.command.d.ts.map +1 -1
- package/dist/commands/feedback.command.js +6 -1
- package/dist/commands/finish.command.d.ts.map +1 -1
- package/dist/commands/finish.command.js +32 -8
- package/dist/commands/fix.command.d.ts.map +1 -1
- package/dist/commands/fix.command.js +19 -0
- package/dist/commands/gate.command.d.ts +6 -1
- package/dist/commands/gate.command.d.ts.map +1 -1
- package/dist/commands/gate.command.js +50 -124
- package/dist/commands/gates.command.d.ts +52 -0
- package/dist/commands/gates.command.d.ts.map +1 -1
- package/dist/commands/gates.command.js +811 -185
- package/dist/commands/gen.command.d.ts.map +1 -1
- package/dist/commands/gen.command.js +4 -0
- package/dist/commands/generated.command.d.ts +33 -3
- package/dist/commands/generated.command.d.ts.map +1 -1
- package/dist/commands/generated.command.js +499 -147
- package/dist/commands/graph-code-subverbs.d.ts +15 -0
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +198 -24
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +79 -37
- package/dist/commands/help.command.d.ts +30 -6
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +323 -199
- package/dist/commands/helper.command.d.ts +1 -0
- package/dist/commands/helper.command.d.ts.map +1 -1
- package/dist/commands/helper.command.js +195 -56
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +115 -12
- package/dist/commands/infer.command.d.ts.map +1 -1
- package/dist/commands/infer.command.js +20 -45
- package/dist/commands/ingest.command.d.ts +0 -1
- package/dist/commands/ingest.command.d.ts.map +1 -1
- package/dist/commands/ingest.command.js +25 -41
- package/dist/commands/knowledge-author.command.d.ts +0 -3
- package/dist/commands/knowledge-author.command.d.ts.map +1 -1
- package/dist/commands/knowledge-author.command.js +10 -23
- package/dist/commands/knowledge.command.d.ts.map +1 -1
- package/dist/commands/knowledge.command.js +469 -129
- package/dist/commands/languages.command.d.ts.map +1 -1
- package/dist/commands/languages.command.js +27 -0
- package/dist/commands/lint.command.d.ts.map +1 -1
- package/dist/commands/lint.command.js +3 -0
- package/dist/commands/mcp.command.d.ts +18 -0
- package/dist/commands/mcp.command.d.ts.map +1 -1
- package/dist/commands/mcp.command.js +33 -8
- package/dist/commands/onboard.command.d.ts.map +1 -1
- package/dist/commands/onboard.command.js +20 -0
- package/dist/commands/owners.command.d.ts.map +1 -1
- package/dist/commands/owners.command.js +14 -2
- package/dist/commands/packs-new.d.ts +13 -1
- package/dist/commands/packs-new.d.ts.map +1 -1
- package/dist/commands/packs-new.js +500 -335
- package/dist/commands/packs.command.d.ts.map +1 -1
- package/dist/commands/packs.command.js +378 -36
- package/dist/commands/paths.command.d.ts.map +1 -1
- package/dist/commands/paths.command.js +9 -1
- package/dist/commands/pipelines.command.d.ts.map +1 -1
- package/dist/commands/pipelines.command.js +7 -1
- package/dist/commands/plan-check.command.d.ts.map +1 -1
- package/dist/commands/plan-check.command.js +3 -0
- package/dist/commands/plan.command.d.ts.map +1 -1
- package/dist/commands/plan.command.js +3 -0
- package/dist/commands/playbooks.command.d.ts.map +1 -1
- package/dist/commands/playbooks.command.js +7 -2
- package/dist/commands/policy-lint.command.d.ts +19 -2
- package/dist/commands/policy-lint.command.d.ts.map +1 -1
- package/dist/commands/policy-lint.command.js +224 -65
- package/dist/commands/policy.command.d.ts.map +1 -1
- package/dist/commands/policy.command.js +6 -1
- package/dist/commands/pr.command.d.ts.map +1 -1
- package/dist/commands/pr.command.js +14 -0
- package/dist/commands/presets.command.d.ts.map +1 -1
- package/dist/commands/presets.command.js +12 -27
- package/dist/commands/profiles.command.d.ts.map +1 -1
- package/dist/commands/profiles.command.js +94 -26
- package/dist/commands/provenance.command.js +1 -1
- package/dist/commands/quality.command.d.ts.map +1 -1
- package/dist/commands/quality.command.js +129 -33
- package/dist/commands/recommend.command.d.ts +20 -4
- package/dist/commands/recommend.command.d.ts.map +1 -1
- package/dist/commands/recommend.command.js +172 -278
- package/dist/commands/registrations.command.d.ts.map +1 -1
- package/dist/commands/registrations.command.js +138 -13
- package/dist/commands/registry-lifecycle-run.d.ts +14 -0
- package/dist/commands/registry-lifecycle-run.d.ts.map +1 -0
- package/dist/commands/registry-lifecycle-run.js +336 -0
- package/dist/commands/registry.command.d.ts.map +1 -1
- package/dist/commands/registry.command.js +235 -62
- package/dist/commands/release.command.d.ts.map +1 -1
- package/dist/commands/release.command.js +99 -20
- package/dist/commands/report.command.d.ts.map +1 -1
- package/dist/commands/report.command.js +38 -2
- package/dist/commands/reuse-coverage.command.d.ts +23 -0
- package/dist/commands/reuse-coverage.command.d.ts.map +1 -0
- package/dist/commands/reuse-coverage.command.js +536 -0
- package/dist/commands/reuse.command.d.ts +3 -18
- package/dist/commands/reuse.command.d.ts.map +1 -1
- package/dist/commands/reuse.command.js +395 -244
- package/dist/commands/review.command.d.ts.map +1 -1
- package/dist/commands/review.command.js +33 -1
- package/dist/commands/rounds.command.d.ts.map +1 -1
- package/dist/commands/rounds.command.js +5 -0
- package/dist/commands/safety.command.d.ts.map +1 -1
- package/dist/commands/safety.command.js +9 -0
- package/dist/commands/scaffolds.command.d.ts.map +1 -1
- package/dist/commands/scaffolds.command.js +97 -25
- package/dist/commands/search.command.d.ts +0 -8
- package/dist/commands/search.command.d.ts.map +1 -1
- package/dist/commands/search.command.js +136 -28
- package/dist/commands/self-config-xrefs.command.d.ts +24 -0
- package/dist/commands/self-config-xrefs.command.d.ts.map +1 -0
- package/dist/commands/self-config-xrefs.command.js +170 -0
- package/dist/commands/self-config.command.d.ts.map +1 -1
- package/dist/commands/self-config.command.js +261 -65
- package/dist/commands/smart-context.command.d.ts.map +1 -1
- package/dist/commands/smart-context.command.js +39 -3
- package/dist/commands/spec.command.d.ts.map +1 -1
- package/dist/commands/spec.command.js +3 -0
- package/dist/commands/stats.command.d.ts.map +1 -1
- package/dist/commands/stats.command.js +3 -0
- package/dist/commands/surface.command.d.ts +6 -1
- package/dist/commands/surface.command.d.ts.map +1 -1
- package/dist/commands/surface.command.js +256 -54
- package/dist/commands/task-context.command.js +1 -1
- package/dist/commands/task.command.d.ts.map +1 -1
- package/dist/commands/task.command.js +22 -1
- package/dist/commands/templates.command.d.ts +4 -0
- package/dist/commands/templates.command.d.ts.map +1 -1
- package/dist/commands/templates.command.js +109 -22
- package/dist/commands/test.command.d.ts.map +1 -1
- package/dist/commands/test.command.js +161 -33
- package/dist/commands/tests.command.d.ts.map +1 -1
- package/dist/commands/tests.command.js +29 -3
- package/dist/commands/trace.command.d.ts.map +1 -1
- package/dist/commands/trace.command.js +29 -4
- package/dist/commands/watch.command.d.ts.map +1 -1
- package/dist/commands/watch.command.js +3 -0
- package/dist/commands/why.command.d.ts.map +1 -1
- package/dist/commands/why.command.js +3 -0
- package/dist/commands/wiring.command.d.ts +18 -4
- package/dist/commands/wiring.command.d.ts.map +1 -1
- package/dist/commands/wiring.command.js +360 -73
- package/dist/dashboard/code-intelligence-data.js +4 -4
- package/dist/diff/deleted-orphan-coverage.d.ts +32 -0
- package/dist/diff/deleted-orphan-coverage.d.ts.map +1 -0
- package/dist/diff/deleted-orphan-coverage.js +107 -0
- package/dist/diff/deleted-orphan-scope-notes.d.ts +23 -0
- package/dist/diff/deleted-orphan-scope-notes.d.ts.map +1 -0
- package/dist/diff/deleted-orphan-scope-notes.js +49 -0
- package/dist/diff/deleted-orphans.d.ts +32 -0
- package/dist/diff/deleted-orphans.d.ts.map +1 -1
- package/dist/diff/deleted-orphans.js +42 -2
- package/dist/dispatch/closest-match.d.ts +15 -0
- package/dist/dispatch/closest-match.d.ts.map +1 -0
- package/dist/dispatch/closest-match.js +47 -0
- package/dist/dispatch/global-flags.d.ts +94 -0
- package/dist/dispatch/global-flags.d.ts.map +1 -0
- package/dist/dispatch/global-flags.js +123 -0
- package/dist/dispatch/guard-invocation-input.d.ts +24 -0
- package/dist/dispatch/guard-invocation-input.d.ts.map +1 -0
- package/dist/dispatch/guard-invocation-input.js +1 -0
- package/dist/dispatch/guard-invocation.d.ts +33 -0
- package/dist/dispatch/guard-invocation.d.ts.map +1 -0
- package/dist/dispatch/guard-invocation.js +183 -0
- package/dist/dispatch/help-intercept.d.ts +22 -0
- package/dist/dispatch/help-intercept.d.ts.map +1 -0
- package/dist/dispatch/help-intercept.js +42 -0
- package/dist/dispatch/i-declared-invocation.d.ts +17 -0
- package/dist/dispatch/i-declared-invocation.d.ts.map +1 -0
- package/dist/dispatch/i-declared-invocation.js +1 -0
- package/dist/dispatch/i-unknown-flag-refusal-input.d.ts +28 -0
- package/dist/dispatch/i-unknown-flag-refusal-input.d.ts.map +1 -0
- package/dist/dispatch/i-unknown-flag-refusal-input.js +1 -0
- package/dist/dispatch/invocation-rejection-kind.d.ts +15 -0
- package/dist/dispatch/invocation-rejection-kind.d.ts.map +1 -0
- package/dist/dispatch/invocation-rejection-kind.js +15 -0
- package/dist/dispatch/invocation-rejection.d.ts +18 -0
- package/dist/dispatch/invocation-rejection.d.ts.map +1 -0
- package/dist/dispatch/invocation-rejection.js +1 -0
- package/dist/dispatch/judge-invocation.d.ts +24 -0
- package/dist/dispatch/judge-invocation.d.ts.map +1 -0
- package/dist/dispatch/judge-invocation.js +50 -0
- package/dist/dispatch/positional-mode.d.ts +18 -0
- package/dist/dispatch/positional-mode.d.ts.map +1 -0
- package/dist/dispatch/positional-mode.js +18 -0
- package/dist/dispatch/read-tracking-map.d.ts +28 -0
- package/dist/dispatch/read-tracking-map.d.ts.map +1 -0
- package/dist/dispatch/read-tracking-map.js +58 -0
- package/dist/dispatch/refuse-sibling-valve-flags.d.ts +30 -0
- package/dist/dispatch/refuse-sibling-valve-flags.d.ts.map +1 -0
- package/dist/dispatch/refuse-sibling-valve-flags.js +65 -0
- package/dist/dispatch/subverb-spec.d.ts +26 -0
- package/dist/dispatch/subverb-spec.d.ts.map +1 -0
- package/dist/dispatch/subverb-spec.js +1 -0
- package/dist/dispatch/undocumented-flag-reads.d.ts +22 -0
- package/dist/dispatch/undocumented-flag-reads.d.ts.map +1 -0
- package/dist/dispatch/undocumented-flag-reads.js +283 -0
- package/dist/dispatch/unknown-flag-refusal-mode.d.ts +13 -0
- package/dist/dispatch/unknown-flag-refusal-mode.d.ts.map +1 -0
- package/dist/dispatch/unknown-flag-refusal-mode.js +13 -0
- package/dist/dispatch/unknown-flag-refusal.d.ts +22 -0
- package/dist/dispatch/unknown-flag-refusal.d.ts.map +1 -0
- package/dist/dispatch/unknown-flag-refusal.js +57 -0
- package/dist/dispatch/unread-flags.d.ts +152 -0
- package/dist/dispatch/unread-flags.d.ts.map +1 -0
- package/dist/dispatch/unread-flags.js +295 -0
- package/dist/dispatch/verdict-valve-flag.d.ts +21 -0
- package/dist/dispatch/verdict-valve-flag.d.ts.map +1 -0
- package/dist/dispatch/verdict-valve-flag.js +21 -0
- package/dist/dispatch/walk-declared-subverbs.d.ts +14 -0
- package/dist/dispatch/walk-declared-subverbs.d.ts.map +1 -0
- package/dist/dispatch/walk-declared-subverbs.js +28 -0
- package/dist/exit-codes.d.ts +41 -3
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +153 -14
- package/dist/finish/run-finish.d.ts +72 -5
- package/dist/finish/run-finish.d.ts.map +1 -1
- package/dist/finish/run-finish.js +453 -104
- package/dist/gates/accepted-empty-note.d.ts +10 -0
- package/dist/gates/accepted-empty-note.d.ts.map +1 -0
- package/dist/gates/accepted-empty-note.js +11 -0
- package/dist/gates/allow-empty.d.ts +25 -0
- package/dist/gates/allow-empty.d.ts.map +1 -0
- package/dist/gates/allow-empty.js +27 -0
- package/dist/gates/asset-doctor-failing-units.d.ts +16 -0
- package/dist/gates/asset-doctor-failing-units.d.ts.map +1 -0
- package/dist/gates/asset-doctor-failing-units.js +14 -0
- package/dist/gates/asset-doctor-failure-line.d.ts +16 -0
- package/dist/gates/asset-doctor-failure-line.d.ts.map +1 -0
- package/dist/gates/asset-doctor-failure-line.js +28 -0
- package/dist/gates/empty-rule-advice-lines.d.ts +15 -0
- package/dist/gates/empty-rule-advice-lines.d.ts.map +1 -0
- package/dist/gates/empty-rule-advice-lines.js +20 -0
- package/dist/gates/gate-envelope.d.ts +85 -8
- package/dist/gates/gate-envelope.d.ts.map +1 -1
- package/dist/gates/gate-envelope.js +56 -7
- package/dist/gates/gate-rule-globs.d.ts +37 -3
- package/dist/gates/gate-rule-globs.d.ts.map +1 -1
- package/dist/gates/gate-rule-globs.js +78 -40
- package/dist/gates/gate-rule-view.d.ts +17 -4
- package/dist/gates/gate-rule-view.d.ts.map +1 -1
- package/dist/gates/gate-rule-view.js +105 -90
- package/dist/gates/i-coverage-dead-glob.d.ts +19 -0
- package/dist/gates/i-coverage-dead-glob.d.ts.map +1 -0
- package/dist/gates/i-coverage-dead-glob.js +1 -0
- package/dist/gates/i-coverage-negation.d.ts +13 -0
- package/dist/gates/i-coverage-negation.d.ts.map +1 -0
- package/dist/gates/i-coverage-negation.js +1 -0
- package/dist/gates/i-registration-query-settle-input.d.ts +18 -0
- package/dist/gates/i-registration-query-settle-input.d.ts.map +1 -0
- package/dist/gates/i-registration-query-settle-input.js +1 -0
- package/dist/gates/i-unit-state-note-row.d.ts +16 -0
- package/dist/gates/i-unit-state-note-row.d.ts.map +1 -0
- package/dist/gates/i-unit-state-note-row.js +1 -0
- package/dist/gates/i-unit-state-notes.d.ts +32 -0
- package/dist/gates/i-unit-state-notes.d.ts.map +1 -0
- package/dist/gates/i-unit-state-notes.js +1 -0
- package/dist/gates/incomplete-registry-inventory.d.ts +19 -0
- package/dist/gates/incomplete-registry-inventory.d.ts.map +1 -0
- package/dist/gates/incomplete-registry-inventory.js +63 -0
- package/dist/gates/measure-registration-roles.d.ts +10 -0
- package/dist/gates/measure-registration-roles.d.ts.map +1 -0
- package/dist/gates/measure-registration-roles.js +9 -0
- package/dist/gates/plane-verdict.d.ts +14 -0
- package/dist/gates/plane-verdict.d.ts.map +1 -0
- package/dist/gates/plane-verdict.js +22 -0
- package/dist/gates/qualify-clean-for-units.d.ts +12 -0
- package/dist/gates/qualify-clean-for-units.d.ts.map +1 -0
- package/dist/gates/qualify-clean-for-units.js +18 -0
- package/dist/gates/registration-graph-verdict.d.ts +34 -0
- package/dist/gates/registration-graph-verdict.d.ts.map +1 -0
- package/dist/gates/registration-graph-verdict.js +70 -0
- package/dist/gates/registration-roles.d.ts +9 -0
- package/dist/gates/registration-roles.d.ts.map +1 -0
- package/dist/gates/registration-roles.js +1 -0
- package/dist/gates/registry-liveness.d.ts +11 -0
- package/dist/gates/registry-liveness.d.ts.map +1 -0
- package/dist/gates/registry-liveness.js +13 -0
- package/dist/gates/rule-coverage.d.ts +187 -4
- package/dist/gates/rule-coverage.d.ts.map +1 -1
- package/dist/gates/rule-coverage.js +501 -49
- package/dist/gates/run-gate-planes.d.ts +14 -0
- package/dist/gates/run-gate-planes.d.ts.map +1 -1
- package/dist/gates/run-gate-planes.js +172 -30
- package/dist/gates/scaffold-selftest.d.ts +56 -0
- package/dist/gates/scaffold-selftest.d.ts.map +1 -0
- package/dist/gates/scaffold-selftest.js +119 -0
- package/dist/gates/seam-rejected-rules.d.ts +28 -0
- package/dist/gates/seam-rejected-rules.d.ts.map +1 -0
- package/dist/gates/seam-rejected-rules.js +95 -0
- package/dist/gates/self-test-check.d.ts +29 -0
- package/dist/gates/self-test-check.d.ts.map +1 -0
- package/dist/gates/self-test-check.js +1 -0
- package/dist/gates/self-test-subject.d.ts +28 -0
- package/dist/gates/self-test-subject.d.ts.map +1 -0
- package/dist/gates/self-test-subject.js +1 -0
- package/dist/gates/self-test.d.ts +20 -0
- package/dist/gates/self-test.d.ts.map +1 -0
- package/dist/gates/self-test.js +82 -0
- package/dist/gates/settle-verdict.d.ts +14 -0
- package/dist/gates/settle-verdict.d.ts.map +1 -0
- package/dist/gates/settle-verdict.js +13 -0
- package/dist/gates/settled-verdict.d.ts +8 -0
- package/dist/gates/settled-verdict.d.ts.map +1 -0
- package/dist/gates/settled-verdict.js +1 -0
- package/dist/gates/template-registry-coverage.d.ts +18 -0
- package/dist/gates/template-registry-coverage.d.ts.map +1 -0
- package/dist/gates/template-registry-coverage.js +24 -0
- package/dist/gates/unit-state-notes.d.ts +40 -0
- package/dist/gates/unit-state-notes.d.ts.map +1 -0
- package/dist/gates/unit-state-notes.js +71 -0
- package/dist/gates/verdict-line.d.ts +18 -0
- package/dist/gates/verdict-line.d.ts.map +1 -0
- package/dist/gates/verdict-line.js +40 -0
- package/dist/graph/graph-reuse-lookup.d.ts +10 -0
- package/dist/graph/graph-reuse-lookup.d.ts.map +1 -0
- package/dist/graph/graph-reuse-lookup.js +41 -0
- package/dist/graph/index-behind-hint.d.ts +11 -0
- package/dist/graph/index-behind-hint.d.ts.map +1 -0
- package/dist/graph/index-behind-hint.js +27 -0
- package/dist/knowledge/knowledge-stale-gate-flags.d.ts +3 -0
- package/dist/knowledge/knowledge-stale-gate-flags.d.ts.map +1 -0
- package/dist/knowledge/knowledge-stale-gate-flags.js +1 -0
- package/dist/knowledge/knowledge-stale-gate-input.d.ts +3 -0
- package/dist/knowledge/knowledge-stale-gate-input.d.ts.map +1 -0
- package/dist/knowledge/knowledge-stale-gate-input.js +1 -0
- package/dist/knowledge/knowledge-stale-gate-result.d.ts +3 -0
- package/dist/knowledge/knowledge-stale-gate-result.d.ts.map +1 -0
- package/dist/knowledge/knowledge-stale-gate-result.js +1 -0
- package/dist/knowledge/knowledge-stale-gate.d.ts +10 -0
- package/dist/knowledge/knowledge-stale-gate.d.ts.map +1 -0
- package/dist/knowledge/knowledge-stale-gate.js +9 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +200 -83
- package/dist/output/failure-hints.d.ts.map +1 -1
- package/dist/output/failure-hints.js +11 -9
- package/dist/output/rejected-entries-note.d.ts +57 -0
- package/dist/output/rejected-entries-note.d.ts.map +1 -0
- package/dist/output/rejected-entries-note.js +103 -0
- package/dist/output/stdout-is-pipe.d.ts +13 -0
- package/dist/output/stdout-is-pipe.d.ts.map +1 -0
- package/dist/output/stdout-is-pipe.js +21 -0
- package/dist/packs/stale-build-warning.d.ts +11 -0
- package/dist/packs/stale-build-warning.d.ts.map +1 -0
- package/dist/packs/stale-build-warning.js +23 -0
- package/dist/quality/run-quality.d.ts +123 -0
- package/dist/quality/run-quality.d.ts.map +1 -0
- package/dist/quality/run-quality.js +442 -0
- package/dist/schemas/json-schemas.d.ts +184 -3
- package/dist/schemas/json-schemas.d.ts.map +1 -1
- package/dist/schemas/json-schemas.js +113 -2
- package/dist/shrk.d.ts +3 -0
- package/dist/shrk.d.ts.map +1 -0
- package/dist/shrk.js +30 -0
- package/dist/surface/audience-applicability.d.ts +26 -0
- package/dist/surface/audience-applicability.d.ts.map +1 -0
- package/dist/surface/audience-applicability.js +60 -0
- package/dist/surface/catalog-command-safety.d.ts +18 -0
- package/dist/surface/catalog-command-safety.d.ts.map +1 -0
- package/dist/surface/catalog-command-safety.js +62 -0
- package/dist/surface/cli-command-resolver.d.ts +21 -0
- package/dist/surface/cli-command-resolver.d.ts.map +1 -0
- package/dist/surface/cli-command-resolver.js +87 -0
- package/dist/surface/command-dispatch-kind.d.ts +20 -0
- package/dist/surface/command-dispatch-kind.d.ts.map +1 -0
- package/dist/surface/command-dispatch-kind.js +20 -0
- package/dist/surface/command-index-entry.d.ts +40 -0
- package/dist/surface/command-index-entry.d.ts.map +1 -0
- package/dist/surface/command-index-entry.js +1 -0
- package/dist/surface/command-index.d.ts +77 -0
- package/dist/surface/command-index.d.ts.map +1 -0
- package/dist/surface/command-index.js +323 -0
- package/dist/surface/i-command-index.d.ts +22 -0
- package/dist/surface/i-command-index.d.ts.map +1 -0
- package/dist/surface/i-command-index.js +1 -0
- package/dist/surface/i-command-string-context.d.ts +20 -0
- package/dist/surface/i-command-string-context.d.ts.map +1 -0
- package/dist/surface/i-command-string-context.js +1 -0
- package/dist/surface/i-surface-denial.d.ts +13 -0
- package/dist/surface/i-surface-denial.d.ts.map +1 -0
- package/dist/surface/i-surface-denial.js +1 -0
- package/dist/surface/load-surface-context.d.ts +17 -1
- package/dist/surface/load-surface-context.d.ts.map +1 -1
- package/dist/surface/load-surface-context.js +48 -10
- package/dist/surface/not-enabled-error.d.ts +38 -4
- package/dist/surface/not-enabled-error.d.ts.map +1 -1
- package/dist/surface/not-enabled-error.js +96 -6
- package/dist/surface/profiles.d.ts +7 -0
- package/dist/surface/profiles.d.ts.map +1 -1
- package/dist/surface/resolve-command-string.d.ts +10 -0
- package/dist/surface/resolve-command-string.d.ts.map +1 -0
- package/dist/surface/resolve-command-string.js +572 -0
- package/dist/surface/surface-config-writer.d.ts +7 -2
- package/dist/surface/surface-config-writer.d.ts.map +1 -1
- package/dist/surface/surface-config-writer.js +25 -6
- package/dist/surface/surface-layer.d.ts +11 -0
- package/dist/surface/surface-layer.d.ts.map +1 -0
- package/dist/surface/surface-layer.js +11 -0
- package/dist/surface/surface-refusal-reason.d.ts +16 -0
- package/dist/surface/surface-refusal-reason.d.ts.map +1 -0
- package/dist/surface/surface-refusal-reason.js +16 -0
- package/dist/surface/surface-selector.d.ts +26 -0
- package/dist/surface/surface-selector.d.ts.map +1 -0
- package/dist/surface/surface-selector.js +42 -0
- package/dist/surface/surface-summary.d.ts +61 -18
- package/dist/surface/surface-summary.d.ts.map +1 -1
- package/dist/surface/surface-summary.js +147 -60
- package/dist/surface/tier.d.ts +85 -22
- package/dist/surface/tier.d.ts.map +1 -1
- package/dist/surface/tier.js +132 -30
- package/dist/usage/usage-log.d.ts +6 -1
- package/dist/usage/usage-log.d.ts.map +1 -1
- package/dist/usage/usage-log.js +7 -2
- package/dist/validation/run-validation-loop.d.ts +9 -0
- package/dist/validation/run-validation-loop.d.ts.map +1 -1
- package/dist/validation/run-validation-loop.js +14 -11
- package/dist/validation/typecheck-emitted.d.ts +8 -9
- package/dist/validation/typecheck-emitted.d.ts.map +1 -1
- package/dist/validation/typecheck-emitted.js +17 -97
- package/package.json +34 -34
|
@@ -14,20 +14,32 @@
|
|
|
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 } from 'node:fs';
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
18
|
import * as nodePath from 'node:path';
|
|
19
|
-
import { resolvePlaneExtractors } from '@shrkcrft/core';
|
|
20
|
-
import { explainWiring, inspectSource, scanRegistry } from '@shrkcrft/boundaries';
|
|
21
|
-
import { BaselineRuleSchema, DocReferenceRuleSchema, GeneratedArtifactRuleSchema, PolicyRuleSchema, RegistrationIdiomSchema, RegistryDeclarationSchema, WiringRuleSchema, } from '@shrkcrft/config';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
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';
|
|
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";
|
|
25
28
|
import { ExitCode } from "../exit-codes.js";
|
|
26
29
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
27
30
|
import { collectGateRules, GATE_PLANES, } from "../gates/gate-rule-view.js";
|
|
28
|
-
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";
|
|
29
35
|
import { buildGateEnvelope } from "../gates/gate-envelope.js";
|
|
30
|
-
import {
|
|
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";
|
|
31
43
|
import { runGatePlanes } from "../gates/run-gate-planes.js";
|
|
32
44
|
import { baselineExplainCommand } from "./baseline.command.js";
|
|
33
45
|
import { generatedExplainCommand } from "./generated.command.js";
|
|
@@ -35,7 +47,7 @@ import { docsReferencesExplainCommand } from "./docs-references.command.js";
|
|
|
35
47
|
import { renderPolicyExplain, runPolicyExplain } from "./policy-lint.command.js";
|
|
36
48
|
import { renderWiringExplain } from "./wiring.command.js";
|
|
37
49
|
const SCHEMA = 'sharkcraft.gates/v1';
|
|
38
|
-
async function prepare(args) {
|
|
50
|
+
export async function prepare(args) {
|
|
39
51
|
const cwd = resolveCwd(args);
|
|
40
52
|
// Coverage memoizes its tree reads inside one call (see `withFileReadCache`).
|
|
41
53
|
// `--no-cache` drops anything already memoized so a suspected caching bug can
|
|
@@ -52,15 +64,17 @@ async function prepare(args) {
|
|
|
52
64
|
process.stderr.write(`Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
|
|
53
65
|
return { ok: false, code: ExitCode.UsageError };
|
|
54
66
|
}
|
|
55
|
-
const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
|
|
56
67
|
return {
|
|
57
68
|
ok: true,
|
|
58
69
|
value: {
|
|
59
70
|
cwd,
|
|
60
71
|
rules: collectGateRules(loaded.value.config),
|
|
61
|
-
|
|
72
|
+
// THE plane scan scope — the one authority every plane verb reads too.
|
|
73
|
+
excludeDirs: planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir),
|
|
62
74
|
planeDiagnostics: loaded.value.planeDiagnostics,
|
|
75
|
+
rejectedRules: seamRejectedRules(loaded.value),
|
|
63
76
|
extractors: loaded.value.config.extractors ?? {},
|
|
77
|
+
configFile: loaded.value.configFile,
|
|
64
78
|
},
|
|
65
79
|
};
|
|
66
80
|
}
|
|
@@ -100,7 +114,7 @@ function writeNoRules(json) {
|
|
|
100
114
|
* silently degrade to an empty diff, which would narrow every rule out of
|
|
101
115
|
* scope and produce a green run that checked nothing.
|
|
102
116
|
*/
|
|
103
|
-
function resolveScope(args, cwd) {
|
|
117
|
+
export function resolveScope(args, cwd) {
|
|
104
118
|
const changedOnly = flagBool(args, 'changed-only');
|
|
105
119
|
const since = flagString(args, 'since') ?? flagString(args, 'base');
|
|
106
120
|
if (!changedOnly && !since)
|
|
@@ -125,7 +139,7 @@ function resolveScope(args, cwd) {
|
|
|
125
139
|
* must never print the same headline as a full one — "0 violations across 2 of
|
|
126
140
|
* 9 rules" and "0 violations across 9 of 9" are different facts.
|
|
127
141
|
*/
|
|
128
|
-
function narrowToScope(rules, files) {
|
|
142
|
+
export function narrowToScope(rules, files) {
|
|
129
143
|
if (files === undefined)
|
|
130
144
|
return { selected: rules, skippedByScope: 0 };
|
|
131
145
|
const selected = rules.filter((r) => ruleTouchedBy(r, files));
|
|
@@ -134,6 +148,8 @@ function narrowToScope(rules, files) {
|
|
|
134
148
|
/** Flags every `gates` verb accepts; anything else is a typo, not an opt-in. */
|
|
135
149
|
const GATES_FLAGS = new Set([
|
|
136
150
|
'json',
|
|
151
|
+
'margin',
|
|
152
|
+
'write',
|
|
137
153
|
'strict',
|
|
138
154
|
'plane',
|
|
139
155
|
'only',
|
|
@@ -144,28 +160,32 @@ const GATES_FLAGS = new Set([
|
|
|
144
160
|
'no-cache',
|
|
145
161
|
'full',
|
|
146
162
|
'limit',
|
|
147
|
-
'cwd',
|
|
148
163
|
'id',
|
|
149
164
|
'rule-file',
|
|
150
165
|
'wiring',
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
// Every dispatcher global (THE list) — a direct handler call may carry them.
|
|
167
|
+
...GLOBAL_FLAGS,
|
|
153
168
|
]);
|
|
154
169
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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.
|
|
160
175
|
*/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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).
|
|
169
189
|
/**
|
|
170
190
|
* Apply `--plane` and `--only` narrowing.
|
|
171
191
|
*
|
|
@@ -173,19 +193,28 @@ function rejectUnknownFlags(args) {
|
|
|
173
193
|
* typo'd rule id would otherwise narrow the run to zero rules and report a
|
|
174
194
|
* confident pass over an empty set.
|
|
175
195
|
*/
|
|
176
|
-
function filterRules(all, planes, only) {
|
|
196
|
+
function filterRules(all, planes, only, rejectedAll = []) {
|
|
177
197
|
let rules = planes ? all.filter((r) => planes.has(r.plane)) : all;
|
|
198
|
+
let rejected = planes ? rejectedAll.filter((r) => planes.has(r.type)) : rejectedAll;
|
|
178
199
|
if (!only)
|
|
179
|
-
return { ok: true, rules };
|
|
200
|
+
return { ok: true, rules, rejected };
|
|
180
201
|
const wanted = only.split(',').map((x) => x.trim()).filter(Boolean);
|
|
181
|
-
|
|
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)]);
|
|
182
205
|
const unknown = wanted.filter((w) => !known.has(w));
|
|
183
206
|
if (unknown.length > 0) {
|
|
184
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`);
|
|
185
208
|
return { ok: false };
|
|
186
209
|
}
|
|
187
210
|
rules = rules.filter((r) => wanted.includes(r.id));
|
|
188
|
-
|
|
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`);
|
|
189
218
|
}
|
|
190
219
|
/**
|
|
191
220
|
* Build the registries the doc-reference plane resolves against — only when a
|
|
@@ -202,7 +231,9 @@ async function inspectionIfNeeded(cwd, rules) {
|
|
|
202
231
|
// resolver is sync. Warming here is what makes a correct pack playbook cited
|
|
203
232
|
// in prose actually resolve.
|
|
204
233
|
const inspection = await inspectSharkcraft({ cwd });
|
|
205
|
-
|
|
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);
|
|
206
237
|
return inspection;
|
|
207
238
|
}
|
|
208
239
|
export const gatesListCommand = {
|
|
@@ -221,7 +252,10 @@ export const gatesListCommand = {
|
|
|
221
252
|
const rules = planes.planes
|
|
222
253
|
? prep.value.rules.filter((r) => planes.planes.has(r.plane))
|
|
223
254
|
: prep.value.rules;
|
|
224
|
-
|
|
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)
|
|
225
259
|
return writeNoRules(json);
|
|
226
260
|
if (json) {
|
|
227
261
|
process.stdout.write(asJson({
|
|
@@ -235,6 +269,8 @@ export const gatesListCommand = {
|
|
|
235
269
|
failOnEmpty: r.failOnEmpty,
|
|
236
270
|
selfTest: r.selfTest ?? null,
|
|
237
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 })),
|
|
238
274
|
diagnostics: prep.value.planeDiagnostics,
|
|
239
275
|
}) + '\n');
|
|
240
276
|
return ExitCode.VerifiedPass;
|
|
@@ -256,6 +292,10 @@ export const gatesListCommand = {
|
|
|
256
292
|
process.stdout.write(` ${r.description}\n`);
|
|
257
293
|
}
|
|
258
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
|
+
}
|
|
259
299
|
for (const d of prep.value.planeDiagnostics)
|
|
260
300
|
process.stdout.write(` ! ${d}\n`);
|
|
261
301
|
process.stdout.write('\nRun `shrk gates coverage` to see what each one actually matches.\n');
|
|
@@ -265,12 +305,10 @@ export const gatesListCommand = {
|
|
|
265
305
|
export const gatesCoverageCommand = {
|
|
266
306
|
name: 'coverage',
|
|
267
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.',
|
|
268
|
-
usage: 'shrk gates coverage [--plane <p>] [--changed-only | --since <ref>] [--only <ids>] [--strict] [--json]',
|
|
269
|
-
booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-cache']),
|
|
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]),
|
|
270
311
|
async run(args) {
|
|
271
|
-
const flagReject = rejectUnknownFlags(args);
|
|
272
|
-
if (flagReject !== undefined)
|
|
273
|
-
return flagReject;
|
|
274
312
|
const prep = await prepare(args);
|
|
275
313
|
if (!prep.ok)
|
|
276
314
|
return prep.code;
|
|
@@ -278,11 +316,15 @@ export const gatesCoverageCommand = {
|
|
|
278
316
|
if (!planes.ok)
|
|
279
317
|
return ExitCode.UsageError;
|
|
280
318
|
const json = flagBool(args, 'json');
|
|
281
|
-
const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'));
|
|
319
|
+
const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'), prep.value.rejectedRules);
|
|
282
320
|
if (!filtered.ok)
|
|
283
321
|
return ExitCode.UsageError;
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
}
|
|
286
328
|
// Scoping the STALE-SELECTOR check to the diff is what moves it from a
|
|
287
329
|
// CI-only report to something you can afford on every save — a stale glob
|
|
288
330
|
// is then caught the moment you cause it, not the next morning.
|
|
@@ -292,8 +334,14 @@ export const gatesCoverageCommand = {
|
|
|
292
334
|
return ExitCode.UsageError;
|
|
293
335
|
}
|
|
294
336
|
const { selected, skippedByScope } = narrowToScope(filtered.rules, scope.files);
|
|
295
|
-
if (selected.length === 0) {
|
|
337
|
+
if (selected.length === 0 && rejected.length === 0) {
|
|
296
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
|
+
});
|
|
297
345
|
if (json) {
|
|
298
346
|
process.stdout.write(asJson({
|
|
299
347
|
schema: 'sharkcraft.gate-coverage/v1',
|
|
@@ -301,7 +349,8 @@ export const gatesCoverageCommand = {
|
|
|
301
349
|
total: 0,
|
|
302
350
|
scoped: true,
|
|
303
351
|
skippedByScope,
|
|
304
|
-
exitCode:
|
|
352
|
+
exitCode: emptyEnv.exit,
|
|
353
|
+
gate: emptyEnv,
|
|
305
354
|
}) + '\n');
|
|
306
355
|
}
|
|
307
356
|
else {
|
|
@@ -309,44 +358,31 @@ export const gatesCoverageCommand = {
|
|
|
309
358
|
process.stdout.write(` No rule's footprint intersects the changeset (${skippedByScope} skipped by scope).\n` +
|
|
310
359
|
' Nothing was checked — this is NOT a pass.\n');
|
|
311
360
|
}
|
|
312
|
-
return
|
|
361
|
+
return emptyEnv.exit;
|
|
313
362
|
}
|
|
314
363
|
const rules = selected;
|
|
315
|
-
const report = buildGateCoverage(prep.value.cwd, rules, prep.value.excludeDirs, prep.value.extractors, false, await inspectionIfNeeded(prep.value.cwd, rules));
|
|
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);
|
|
316
366
|
// A rule that matched nothing is NOT-VERIFIED (2) by default — it neither
|
|
317
367
|
// passed nor failed, it never ran. `failOnEmpty` on the rule (or the global
|
|
318
|
-
// --strict promotion) turns that into a hard failure
|
|
319
|
-
|
|
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;
|
|
320
375
|
const softEmpty = report.rules.filter((r) => r.status === 'empty' && !r.failOnEmpty);
|
|
321
|
-
const exit = hardFailures.length > 0
|
|
322
|
-
? ExitCode.Failure
|
|
323
|
-
: softEmpty.length > 0
|
|
324
|
-
? ExitCode.NotVerified
|
|
325
|
-
: ExitCode.VerifiedPass;
|
|
326
376
|
if (json) {
|
|
327
377
|
process.stdout.write(asJson({
|
|
328
378
|
...report,
|
|
329
|
-
|
|
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,
|
|
330
383
|
...(scope.files ? { scoped: true, skippedByScope } : {}),
|
|
331
384
|
exitCode: exit,
|
|
332
|
-
gate:
|
|
333
|
-
id: r.id,
|
|
334
|
-
type: r.plane,
|
|
335
|
-
status: r.status === 'ok'
|
|
336
|
-
? 'passed'
|
|
337
|
-
: r.status === 'empty'
|
|
338
|
-
? r.failOnEmpty
|
|
339
|
-
? 'failed'
|
|
340
|
-
: 'skipped'
|
|
341
|
-
: r.status === 'error'
|
|
342
|
-
? 'error'
|
|
343
|
-
: 'failed',
|
|
344
|
-
severity: r.failOnEmpty ? 'error' : 'warning',
|
|
345
|
-
counts: { files: r.filesMatched, units: r.unitsMatched },
|
|
346
|
-
violations: r.expectationFailures.map((f) => ({ id: r.id, message: f })),
|
|
347
|
-
...(r.status === 'empty' ? { skipReason: `matched 0 ${r.unitLabel}` } : {}),
|
|
348
|
-
...(r.error ? { error: r.error } : {}),
|
|
349
|
-
}))),
|
|
385
|
+
gate: env,
|
|
350
386
|
}) + '\n');
|
|
351
387
|
return exit;
|
|
352
388
|
}
|
|
@@ -355,12 +391,27 @@ export const gatesCoverageCommand = {
|
|
|
355
391
|
if (scope.files) {
|
|
356
392
|
process.stdout.write(kv('scope', `changed-only (${scope.files.length} file(s))`) + '\n');
|
|
357
393
|
}
|
|
358
|
-
|
|
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');
|
|
359
406
|
if (report.errored > 0)
|
|
360
407
|
process.stdout.write(kv('misconfigured', String(report.errored)) + '\n');
|
|
361
408
|
if (report.expectationFailures > 0) {
|
|
362
409
|
process.stdout.write(kv('broken selfTest', String(report.expectationFailures)) + '\n');
|
|
363
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
|
+
}
|
|
364
415
|
// The shared extractors, ONCE. Their whole value is that N consumers cannot
|
|
365
416
|
// disagree about which set they check — so one line proving the shared set
|
|
366
417
|
// is live and non-empty covers all N.
|
|
@@ -380,32 +431,122 @@ export const gatesCoverageCommand = {
|
|
|
380
431
|
}
|
|
381
432
|
}
|
|
382
433
|
process.stdout.write('\n');
|
|
383
|
-
for (const r of report.rules) {
|
|
384
|
-
|
|
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
|
+
: '✗';
|
|
385
450
|
process.stdout.write(` ${mark} [${r.plane}] ${r.id}${r.viaExtractor ? ` (via $use:${r.viaExtractor})` : ''}` +
|
|
386
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`);
|
|
387
481
|
if (r.sampleIds.length > 0) {
|
|
388
482
|
process.stdout.write(` e.g. ${r.sampleIds.join(', ')}\n`);
|
|
389
483
|
}
|
|
390
484
|
if (r.status === 'empty') {
|
|
391
|
-
|
|
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`);
|
|
392
495
|
// When the engine knows WHY a correct zero-match is probably not what
|
|
393
496
|
// the author meant, say so here rather than leaving them to rediscover
|
|
394
497
|
// it — this is the one dead end `import-edges` reliably produces.
|
|
395
498
|
if (r.hint)
|
|
396
499
|
process.stdout.write(` → ${r.hint}\n`);
|
|
397
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`);
|
|
398
505
|
if (r.error)
|
|
399
506
|
process.stdout.write(` ! ${r.error}\n`);
|
|
400
507
|
for (const f of r.expectationFailures)
|
|
401
508
|
process.stdout.write(` ! selfTest: ${f}\n`);
|
|
402
509
|
}
|
|
403
|
-
|
|
404
|
-
|
|
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`);
|
|
405
524
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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`);
|
|
409
550
|
}
|
|
410
551
|
return exit;
|
|
411
552
|
},
|
|
@@ -413,12 +554,10 @@ export const gatesCoverageCommand = {
|
|
|
413
554
|
export const gatesCheckCommand = {
|
|
414
555
|
name: 'check',
|
|
415
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).',
|
|
416
|
-
usage: 'shrk gates check [--plane <p>] [--only <ids>] [--changed-only | --since <ref>] [--no-spawn] [--strict] [--json]',
|
|
417
|
-
booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-spawn', 'no-cache']),
|
|
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]),
|
|
418
560
|
async run(args) {
|
|
419
|
-
const flagReject = rejectUnknownFlags(args);
|
|
420
|
-
if (flagReject !== undefined)
|
|
421
|
-
return flagReject;
|
|
422
561
|
const prep = await prepare(args);
|
|
423
562
|
if (!prep.ok)
|
|
424
563
|
return prep.code;
|
|
@@ -426,11 +565,18 @@ export const gatesCheckCommand = {
|
|
|
426
565
|
if (!planes.ok)
|
|
427
566
|
return ExitCode.UsageError;
|
|
428
567
|
const json = flagBool(args, 'json');
|
|
429
|
-
const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'));
|
|
568
|
+
const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'), prep.value.rejectedRules);
|
|
430
569
|
if (!filtered.ok)
|
|
431
570
|
return ExitCode.UsageError;
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
}
|
|
434
580
|
const scope = resolveScope(args, prep.value.cwd);
|
|
435
581
|
if (scope.error) {
|
|
436
582
|
process.stderr.write(`Cannot scope to the changeset: ${scope.error}\n`);
|
|
@@ -438,7 +584,7 @@ export const gatesCheckCommand = {
|
|
|
438
584
|
}
|
|
439
585
|
const { selected, skippedByScope } = narrowToScope(filtered.rules, scope.files);
|
|
440
586
|
const noSpawn = flagBool(args, 'no-spawn');
|
|
441
|
-
const
|
|
587
|
+
const planeRun = selected.length === 0
|
|
442
588
|
? { results: [], diagnostics: [] }
|
|
443
589
|
: runGatePlanes(selected, {
|
|
444
590
|
cwd: prep.value.cwd,
|
|
@@ -447,6 +593,8 @@ export const gatesCheckCommand = {
|
|
|
447
593
|
...(noSpawn ? { noSpawn: true } : {}),
|
|
448
594
|
...(await inspectionIfNeeded(prep.value.cwd, selected).then((i) => i ? { inspection: i } : {})),
|
|
449
595
|
});
|
|
596
|
+
const run = { ...planeRun, results: [...planeRun.results, ...rejected] };
|
|
597
|
+
const inScope = selected.length + rejected.length;
|
|
450
598
|
// Only an ERROR-severity failure blocks. A warning-severity rule reports
|
|
451
599
|
// without failing, exactly as its own plane's verb does.
|
|
452
600
|
//
|
|
@@ -474,16 +622,42 @@ export const gatesCheckCommand = {
|
|
|
474
622
|
// dropped the drift half. Nobody asked for that, and it is exactly the
|
|
475
623
|
// silent-green this engine exists to prevent, so it is `2`.
|
|
476
624
|
// Matches `check wiring --changed-only`, which draws the same line.
|
|
477
|
-
|
|
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
|
|
478
631
|
? ExitCode.Failure
|
|
479
|
-
: evaluated === 0 || skipped > 0
|
|
632
|
+
: (evaluated === 0 && selected.length > 0) || skipped > 0
|
|
480
633
|
? ExitCode.NotVerified
|
|
481
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;
|
|
482
654
|
const diagnostics = [...run.diagnostics, ...prep.value.planeDiagnostics];
|
|
483
655
|
if (json) {
|
|
484
656
|
process.stdout.write(asJson({
|
|
485
657
|
schema: SCHEMA,
|
|
486
|
-
configured: filtered.rules.length,
|
|
658
|
+
configured: filtered.rules.length + rejected.length,
|
|
659
|
+
// Pack rules the merge seam refused — errored rows in `gate.rules`.
|
|
660
|
+
rejected: rejected.length,
|
|
487
661
|
selected: selected.length,
|
|
488
662
|
evaluated,
|
|
489
663
|
skipped,
|
|
@@ -493,16 +667,22 @@ export const gatesCheckCommand = {
|
|
|
493
667
|
strict,
|
|
494
668
|
failed: blocking.length,
|
|
495
669
|
failedWarnings: failedWarnings.length,
|
|
496
|
-
verdict:
|
|
670
|
+
verdict: exit === ExitCode.Failure ? 'errors' : exit === ExitCode.VerifiedPass ? 'pass' : 'not-verified',
|
|
497
671
|
diagnostics,
|
|
498
672
|
exitCode: exit,
|
|
499
|
-
gate:
|
|
673
|
+
gate: env,
|
|
500
674
|
}) + '\n');
|
|
501
675
|
return exit;
|
|
502
676
|
}
|
|
503
677
|
process.stdout.write(header('Gate check — every rule plane'));
|
|
504
|
-
|
|
505
|
-
|
|
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');
|
|
506
686
|
if (scope.files) {
|
|
507
687
|
process.stdout.write(kv('scope', `changed-only (${scope.files.length} file(s))`) + '\n');
|
|
508
688
|
}
|
|
@@ -510,17 +690,42 @@ export const gatesCheckCommand = {
|
|
|
510
690
|
process.stdout.write(kv('mode', '--no-spawn — shell-executing checks skipped') + '\n');
|
|
511
691
|
process.stdout.write(kv('violations', `${blocking.length} blocking rule(s), ${failedWarnings.length} warning rule(s)` +
|
|
512
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`.
|
|
513
700
|
for (const plane of GATE_PLANES) {
|
|
514
|
-
const inPlane =
|
|
701
|
+
const inPlane = env.rules.filter((r) => r.type === plane);
|
|
515
702
|
if (inPlane.length === 0)
|
|
516
703
|
continue;
|
|
517
704
|
for (const r of inPlane) {
|
|
518
|
-
const mark = r.status === 'passed'
|
|
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
|
+
: '!';
|
|
519
716
|
const counts = Object.entries(r.counts).map(([k, v]) => `${k} ${v}`).join(', ');
|
|
520
717
|
process.stdout.write(` ${mark} [${plane}] ${r.id}${counts ? ` (${counts})` : ''}\n`);
|
|
521
718
|
if (r.status === 'skipped' && r.skipReason) {
|
|
522
719
|
process.stdout.write(` SKIPPED — ${r.skipReason}\n`);
|
|
523
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
|
+
}
|
|
524
729
|
if (r.error)
|
|
525
730
|
process.stdout.write(` ! ${r.error}\n`);
|
|
526
731
|
for (const v of r.violations.slice(0, 10)) {
|
|
@@ -537,31 +742,84 @@ export const gatesCheckCommand = {
|
|
|
537
742
|
}
|
|
538
743
|
for (const d of diagnostics)
|
|
539
744
|
process.stdout.write(` ! ${d}\n`);
|
|
745
|
+
process.stdout.write(unitNotes.text);
|
|
540
746
|
if (strict && failedWarnings.length > 0 && exit === ExitCode.Failure) {
|
|
541
747
|
process.stdout.write(`\n${failedWarnings.length} warning rule(s) reported findings and --strict promoted them to failures.\n`);
|
|
542
748
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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');
|
|
549
756
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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`);
|
|
559
782
|
}
|
|
560
783
|
return exit;
|
|
561
784
|
},
|
|
562
785
|
};
|
|
563
|
-
/**
|
|
564
|
-
|
|
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) {
|
|
565
823
|
const sides = new Map();
|
|
566
824
|
// Split on whitespace that precedes a `<side>=`, so a glob may contain none.
|
|
567
825
|
for (const part of spec.trim().split(/\s+(?=(?:declared|registered)=)/)) {
|
|
@@ -573,7 +831,7 @@ function parseInlineWiring(spec) {
|
|
|
573
831
|
return { error: `unknown side "${side}" — use declared= or registered=` };
|
|
574
832
|
}
|
|
575
833
|
const rest = part.slice(eq + 1);
|
|
576
|
-
const colon = rest.
|
|
834
|
+
const colon = rest.indexOf(':');
|
|
577
835
|
if (colon <= 0)
|
|
578
836
|
return { error: `"${side}=" needs <glob>:<pattern>` };
|
|
579
837
|
sides.set(side, { files: [rest.slice(0, colon)], match: rest.slice(colon + 1) });
|
|
@@ -587,28 +845,175 @@ function parseInlineWiring(spec) {
|
|
|
587
845
|
files: side.files,
|
|
588
846
|
extract: 'regex-capture',
|
|
589
847
|
pattern: side.match,
|
|
848
|
+
...(flags !== undefined && flags.length > 0 ? { flags } : {}),
|
|
590
849
|
});
|
|
591
850
|
return {
|
|
592
851
|
rule: { id: '(try)', declared: toSource(declared), registered: toSource(registered) },
|
|
593
852
|
};
|
|
594
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
|
+
}
|
|
595
987
|
export const gatesTryCommand = {
|
|
596
988
|
name: 'try',
|
|
597
|
-
description:
|
|
598
|
-
usage: "shrk gates try --rule-file <rule.json> [--plane <p>] [--full | --limit N] | --wiring 'declared=<glob>:<pat> registered=<glob>:<pat>' [--json]",
|
|
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]",
|
|
599
991
|
booleanFlags: new Set(['json', 'full']),
|
|
992
|
+
flags: new Set([...GATES_FLAGS, ...TRY_ONLY_FLAGS]),
|
|
600
993
|
async run(args) {
|
|
601
|
-
|
|
602
|
-
if
|
|
603
|
-
|
|
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
|
+
}
|
|
604
1001
|
const ruleFile = flagString(args, 'rule-file') ?? args.positional[0];
|
|
605
1002
|
const inline = flagString(args, 'wiring');
|
|
1003
|
+
const inlineFlags = flagString(args, 'flags');
|
|
606
1004
|
if (!ruleFile && !inline) {
|
|
607
1005
|
process.stderr.write('Usage: shrk gates try --rule-file <rule.json> [--plane <p>] [--full | --limit N]\n' +
|
|
608
|
-
" shrk gates try --wiring 'declared=<glob>:<pat> registered=<glob>:<pat>'\n" +
|
|
609
|
-
' Runs the extraction and prints the resolved sets — config is never touched.\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' +
|
|
610
1008
|
' --full dump the WHOLE extracted set (default: first 5)\n' +
|
|
611
|
-
' --limit N dump the first N\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");
|
|
612
1017
|
return ExitCode.UsageError;
|
|
613
1018
|
}
|
|
614
1019
|
const cwd = resolveCwd(args);
|
|
@@ -618,19 +1023,11 @@ export const gatesTryCommand = {
|
|
|
618
1023
|
// declared rule is read, and nothing is written).
|
|
619
1024
|
const loaded = await resolveProjectConfig(cwd);
|
|
620
1025
|
const extractors = loaded.ok ? (loaded.value.config.extractors ?? {}) : {};
|
|
621
|
-
const excludeDirs = loaded.ok
|
|
622
|
-
? (() => {
|
|
623
|
-
const rel = nodePath
|
|
624
|
-
.relative(cwd, loaded.value.sharkcraftDir)
|
|
625
|
-
.split(nodePath.sep)
|
|
626
|
-
.join('/');
|
|
627
|
-
return rel && !rel.startsWith('..') ? [rel] : [];
|
|
628
|
-
})()
|
|
629
|
-
: [];
|
|
1026
|
+
const excludeDirs = loaded.ok ? planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir) : [];
|
|
630
1027
|
let raw;
|
|
631
1028
|
let plane = 'wiring';
|
|
632
1029
|
if (inline) {
|
|
633
|
-
const parsed = parseInlineWiring(inline);
|
|
1030
|
+
const parsed = parseInlineWiring(inline, inlineFlags);
|
|
634
1031
|
if (parsed.error) {
|
|
635
1032
|
process.stderr.write(`Invalid --wiring spec: ${parsed.error}\n`);
|
|
636
1033
|
return ExitCode.UsageError;
|
|
@@ -675,10 +1072,24 @@ export const gatesTryCommand = {
|
|
|
675
1072
|
process.stderr.write(`Invalid ${plane} rule: ${summary}\n`);
|
|
676
1073
|
return ExitCode.UsageError;
|
|
677
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;
|
|
678
1089
|
// Resolve `$use` exactly as the loader would, so a candidate referencing a
|
|
679
1090
|
// shared extractor is tried against the real shared definition.
|
|
680
1091
|
const planeKey = PLANE_CONFIG_KEY[plane];
|
|
681
|
-
const resolved = resolvePlaneExtractors({ [planeKey]: [
|
|
1092
|
+
const resolved = resolvePlaneExtractors({ [planeKey]: [candidate] }, extractors);
|
|
682
1093
|
if (resolved.errors.length > 0) {
|
|
683
1094
|
const summary = resolved.errors.map((e) => e.message).join('; ');
|
|
684
1095
|
if (json)
|
|
@@ -687,33 +1098,75 @@ export const gatesTryCommand = {
|
|
|
687
1098
|
process.stderr.write(`Unresolvable extractor reference: ${summary}\n`);
|
|
688
1099
|
return ExitCode.UsageError;
|
|
689
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
|
+
}
|
|
690
1113
|
const resolvedByKey = resolved;
|
|
691
|
-
const rule = resolvedByKey[planeKey]?.[0] ??
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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';
|
|
699
1133
|
// The wiring plane already has a BOTH-SIDES explainer (resolved declared
|
|
700
1134
|
// set, resolved registered set, and the set-difference between them) — the
|
|
701
1135
|
// exact view an author tightening a selector needs. Reuse it rather than
|
|
702
1136
|
// printing a second, thinner one that could disagree with `gates explain`.
|
|
703
1137
|
if (plane === 'wiring') {
|
|
704
1138
|
const explain = explainWiring(cwd, rule, { excludeDirs });
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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;
|
|
708
1162
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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;
|
|
713
1169
|
}
|
|
714
|
-
// The candidate's COVERAGE (what it matched) is the answer the author
|
|
715
|
-
// needs, and the violation run tells them whether the rule would be green.
|
|
716
|
-
const coverage = buildGateCoverage(cwd, [view], excludeDirs, extractors, true, await inspectionIfNeeded(cwd, [view]));
|
|
717
1170
|
// `noSpawn` is not a performance choice here: a `--rule-file` is arbitrary
|
|
718
1171
|
// JSON, and honouring a `regen` / `compute.run` from it would turn a
|
|
719
1172
|
// read-only preview into shell execution from an untrusted file.
|
|
@@ -721,11 +1174,35 @@ export const gatesTryCommand = {
|
|
|
721
1174
|
cwd,
|
|
722
1175
|
excludeDirs,
|
|
723
1176
|
noSpawn: true,
|
|
724
|
-
...(
|
|
1177
|
+
...(inspection ? { inspection } : {}),
|
|
725
1178
|
});
|
|
726
1179
|
const result = run.results[0];
|
|
727
|
-
const cov = coverage.rules[0];
|
|
728
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
|
+
}));
|
|
729
1206
|
if (json) {
|
|
730
1207
|
process.stdout.write(asJson({
|
|
731
1208
|
schema: 'sharkcraft.gates-try/v1',
|
|
@@ -734,13 +1211,20 @@ export const gatesTryCommand = {
|
|
|
734
1211
|
coverage: cov,
|
|
735
1212
|
...(cov.hint ? { hint: cov.hint } : {}),
|
|
736
1213
|
result: result ?? null,
|
|
1214
|
+
selfTest: selfTestPayload(cov, declared),
|
|
1215
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
737
1216
|
note: 'nothing was written — paste the rule into sharkcraft.config.ts to keep it',
|
|
1217
|
+
exitCode: settled.exit,
|
|
1218
|
+
settled,
|
|
738
1219
|
}) + '\n');
|
|
739
|
-
return
|
|
1220
|
+
return settled.exit;
|
|
740
1221
|
}
|
|
741
1222
|
process.stdout.write(header(`gates try — candidate ${plane} rule "${view.id}"`));
|
|
742
|
-
|
|
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');
|
|
743
1226
|
process.stdout.write(kv(cov.unitLabel, String(cov.unitsMatched)) + '\n');
|
|
1227
|
+
process.stdout.write(kv('coverage', formatCoverage(cov.coverage)) + '\n');
|
|
744
1228
|
if (cov.viaExtractor)
|
|
745
1229
|
process.stdout.write(kv('via extractor', `$use:${cov.viaExtractor}`) + '\n');
|
|
746
1230
|
// Tuning a selector against a large tree needs the WHOLE candidate set —
|
|
@@ -774,25 +1258,25 @@ export const gatesTryCommand = {
|
|
|
774
1258
|
process.stdout.write(` … (${result.violations.length - 20} more)\n`);
|
|
775
1259
|
}
|
|
776
1260
|
}
|
|
1261
|
+
writeSelfTestBlock(view.id, cov, declared);
|
|
1262
|
+
writeNotes(notes);
|
|
777
1263
|
if (cov.status === 'empty') {
|
|
778
1264
|
process.stdout.write('\n This selector matched NOTHING — tighten it before adding it to config.\n');
|
|
779
1265
|
if (cov.hint)
|
|
780
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`);
|
|
781
1269
|
}
|
|
782
1270
|
process.stdout.write('\n Nothing was written. Paste the rule into sharkcraft.config.ts to keep it.\n');
|
|
783
|
-
|
|
1271
|
+
writeTryVerdict(settled, cov, cov.status === 'empty' ? 'The candidate matched nothing — it proved nothing.' : undefined);
|
|
1272
|
+
return settled.exit;
|
|
784
1273
|
},
|
|
785
1274
|
};
|
|
786
|
-
/**
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
registration: 'registrationGraph',
|
|
792
|
-
baseline: 'baselines',
|
|
793
|
-
generated: 'generatedArtifacts',
|
|
794
|
-
'doc-reference': 'docReferences',
|
|
795
|
-
};
|
|
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;
|
|
796
1280
|
/** The loader's own schema for each plane — one validation, not a second one. */
|
|
797
1281
|
const PLANE_SCHEMAS = {
|
|
798
1282
|
wiring: WiringRuleSchema,
|
|
@@ -830,8 +1314,23 @@ function inferPlane(raw) {
|
|
|
830
1314
|
return 'registry';
|
|
831
1315
|
return undefined;
|
|
832
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
|
+
}
|
|
833
1331
|
/** Render a registry inventory as the trust-layer explain view. */
|
|
834
|
-
function explainRegistry(cwd,
|
|
1332
|
+
function explainRegistry(cwd, view, excludeDirs) {
|
|
1333
|
+
const decl = view.raw;
|
|
835
1334
|
const inventory = scanRegistry(cwd, decl, { excludeDirs });
|
|
836
1335
|
const insp = inspectSource(cwd, decl.source, excludeDirs);
|
|
837
1336
|
if (decl.source.$use)
|
|
@@ -846,9 +1345,11 @@ function explainRegistry(cwd, decl, excludeDirs) {
|
|
|
846
1345
|
}
|
|
847
1346
|
for (const d of inventory.diagnostics)
|
|
848
1347
|
process.stdout.write(` ! ${d}\n`);
|
|
1348
|
+
writeExplainUnitStates(cwd, view, excludeDirs);
|
|
849
1349
|
}
|
|
850
1350
|
/** Render the three sides of a registration idiom. */
|
|
851
|
-
function explainRegistration(cwd,
|
|
1351
|
+
function explainRegistration(cwd, view, excludeDirs) {
|
|
1352
|
+
const idiom = view.raw;
|
|
852
1353
|
for (const [label, source] of [
|
|
853
1354
|
['declared', idiom.declared],
|
|
854
1355
|
['provided', idiom.provided],
|
|
@@ -865,6 +1366,7 @@ function explainRegistration(cwd, idiom, excludeDirs) {
|
|
|
865
1366
|
if (insp.sites.length > 20)
|
|
866
1367
|
process.stdout.write(` … (${insp.sites.length - 20} more)\n`);
|
|
867
1368
|
}
|
|
1369
|
+
writeExplainUnitStates(cwd, view, excludeDirs);
|
|
868
1370
|
process.stdout.write(`\n Query one token's chain with \`shrk wiring chain <token>\`.\n`);
|
|
869
1371
|
}
|
|
870
1372
|
export const gatesExplainCommand = {
|
|
@@ -950,10 +1452,10 @@ export const gatesExplainCommand = {
|
|
|
950
1452
|
if (view.description)
|
|
951
1453
|
process.stdout.write(` ${view.description}\n`);
|
|
952
1454
|
if (view.plane === 'registry') {
|
|
953
|
-
explainRegistry(prep.value.cwd, view
|
|
1455
|
+
explainRegistry(prep.value.cwd, view, prep.value.excludeDirs);
|
|
954
1456
|
}
|
|
955
1457
|
else {
|
|
956
|
-
explainRegistration(prep.value.cwd, view
|
|
1458
|
+
explainRegistration(prep.value.cwd, view, prep.value.excludeDirs);
|
|
957
1459
|
}
|
|
958
1460
|
return ExitCode.VerifiedPass;
|
|
959
1461
|
},
|
|
@@ -978,23 +1480,147 @@ export async function tryExplainGateRule(args, id) {
|
|
|
978
1480
|
const forwarded = { ...args, positional: [id] };
|
|
979
1481
|
return gatesExplainCommand.run(forwarded);
|
|
980
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
|
+
};
|
|
981
1603
|
export const gatesCommand = {
|
|
982
1604
|
name: 'gates',
|
|
983
|
-
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,
|
|
984
|
-
usage: 'shrk gates check | coverage | list | explain <id> | try --rule-file <f>',
|
|
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,
|
|
985
1611
|
booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-spawn']),
|
|
986
|
-
async run(
|
|
987
|
-
|
|
988
|
-
process.stderr.write((sub ? `Unknown subcommand "${sub}". ` : '') +
|
|
989
|
-
'Usage: shrk gates <subcommand>\n' +
|
|
1612
|
+
async run() {
|
|
1613
|
+
process.stderr.write('Usage: shrk gates <subcommand>\n' +
|
|
990
1614
|
' check [--plane <p>] [--only <ids>] [--changed-only|--since <ref>] [--no-spawn]\n' +
|
|
991
1615
|
' run EVERY plane\'s violation check — one exit code (the CI / pre-commit primitive)\n' +
|
|
992
|
-
' coverage [--plane <p>] [--changed-only|--since <ref>] [--strict]\n' +
|
|
993
|
-
' what each rule MATCHED — the stale-selector detector\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' +
|
|
994
1618
|
' list [--plane <p>] every declared rule, with severity + empty-match policy\n' +
|
|
995
1619
|
' explain <id> the concrete inputs one rule resolved\n' +
|
|
996
|
-
" try --rule-file <f> | --wiring 'declared=<g>:<p> registered=<g>:<p>'\n" +
|
|
997
|
-
' dry-run a candidate rule WITHOUT touching config\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" +
|
|
998
1624
|
'(`shrk gate`, singular, runs the quality-gate pipeline — a different verb.)\n');
|
|
999
1625
|
return ExitCode.UsageError;
|
|
1000
1626
|
},
|