@shrkcrft/boundaries 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/baseline/compute-baseline.d.ts +8 -0
- package/dist/baseline/compute-baseline.d.ts.map +1 -1
- package/dist/baseline/compute-baseline.js +8 -7
- package/dist/baseline/diff-baseline.d.ts +28 -0
- package/dist/baseline/diff-baseline.d.ts.map +1 -1
- package/dist/baseline/diff-baseline.js +29 -0
- package/dist/evaluate/boundary-unit-finding.d.ts +10 -0
- package/dist/evaluate/boundary-unit-finding.d.ts.map +1 -0
- package/dist/evaluate/boundary-unit-finding.js +21 -0
- package/dist/evaluate/boundary-unit-kind.d.ts +7 -0
- package/dist/evaluate/boundary-unit-kind.d.ts.map +1 -0
- package/dist/evaluate/boundary-unit-kind.js +14 -0
- package/dist/evaluate/evaluate-boundaries.d.ts +230 -3
- package/dist/evaluate/evaluate-boundaries.d.ts.map +1 -1
- package/dist/evaluate/evaluate-boundaries.js +481 -53
- package/dist/evaluate/i-boundary-rule-settle-input.d.ts +27 -0
- package/dist/evaluate/i-boundary-rule-settle-input.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-rule-settle-input.js +1 -0
- package/dist/evaluate/i-boundary-rule-settlement.d.ts +23 -0
- package/dist/evaluate/i-boundary-rule-settlement.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-rule-settlement.js +1 -0
- package/dist/evaluate/i-boundary-unit-finding.d.ts +22 -0
- package/dist/evaluate/i-boundary-unit-finding.d.ts.map +1 -0
- package/dist/evaluate/i-boundary-unit-finding.js +1 -0
- package/dist/evaluate/settle-boundary-rule.d.ts +23 -0
- package/dist/evaluate/settle-boundary-rule.d.ts.map +1 -0
- package/dist/evaluate/settle-boundary-rule.js +73 -0
- package/dist/evaluate/with-boundary-rule-settlement.d.ts +11 -0
- package/dist/evaluate/with-boundary-rule-settlement.d.ts.map +1 -0
- package/dist/evaluate/with-boundary-rule-settlement.js +44 -0
- package/dist/extract/code-zones.d.ts +95 -3
- package/dist/extract/code-zones.d.ts.map +1 -1
- package/dist/extract/code-zones.js +313 -4
- package/dist/extract/extract-tokens.d.ts +30 -0
- package/dist/extract/extract-tokens.d.ts.map +1 -1
- package/dist/extract/extract-tokens.js +233 -27
- package/dist/extract/i-labeled-source.d.ts +12 -0
- package/dist/extract/i-labeled-source.d.ts.map +1 -0
- package/dist/extract/i-labeled-source.js +1 -0
- package/dist/extract/import-edges.d.ts +1 -0
- package/dist/extract/import-edges.d.ts.map +1 -1
- package/dist/extract/import-edges.js +20 -7
- package/dist/extract/inspect-source.d.ts +32 -3
- package/dist/extract/inspect-source.d.ts.map +1 -1
- package/dist/extract/inspect-source.js +50 -7
- package/dist/extract/parse-imports.d.ts +34 -12
- package/dist/extract/parse-imports.d.ts.map +1 -1
- package/dist/extract/parse-imports.js +119 -20
- package/dist/extract/scan-literals.d.ts +12 -1
- package/dist/extract/scan-literals.d.ts.map +1 -1
- package/dist/extract/scan-literals.js +15 -1
- package/dist/extract/source-liveness-request.d.ts +24 -0
- package/dist/extract/source-liveness-request.d.ts.map +1 -0
- package/dist/extract/source-liveness-request.js +45 -0
- package/dist/generated/check-provenance.d.ts.map +1 -1
- package/dist/generated/check-provenance.js +3 -1
- package/dist/generated/read-regen-tree.d.ts +16 -0
- package/dist/generated/read-regen-tree.d.ts.map +1 -0
- package/dist/generated/read-regen-tree.js +61 -0
- package/dist/generated/scan-generated.d.ts +8 -0
- package/dist/generated/scan-generated.d.ts.map +1 -1
- package/dist/generated/scan-generated.js +25 -10
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -0
- package/dist/model/boundary-intended-empty-line.d.ts +10 -0
- package/dist/model/boundary-intended-empty-line.d.ts.map +1 -0
- package/dist/model/boundary-intended-empty-line.js +11 -0
- package/dist/model/boundary-markable-list.d.ts +15 -0
- package/dist/model/boundary-markable-list.d.ts.map +1 -0
- package/dist/model/boundary-markable-list.js +15 -0
- package/dist/model/boundary-rule-input-keys.d.ts +11 -0
- package/dist/model/boundary-rule-input-keys.d.ts.map +1 -0
- package/dist/model/boundary-rule-input-keys.js +29 -0
- package/dist/model/boundary-rule-input.d.ts +23 -0
- package/dist/model/boundary-rule-input.d.ts.map +1 -0
- package/dist/model/boundary-rule-input.js +1 -0
- package/dist/model/boundary-rule-key-problems.d.ts +11 -0
- package/dist/model/boundary-rule-key-problems.d.ts.map +1 -0
- package/dist/model/boundary-rule-key-problems.js +36 -0
- package/dist/model/boundary-rule-marker-problems.d.ts +26 -0
- package/dist/model/boundary-rule-marker-problems.d.ts.map +1 -0
- package/dist/model/boundary-rule-marker-problems.js +94 -0
- package/dist/model/boundary-rule-scope.d.ts +88 -0
- package/dist/model/boundary-rule-scope.d.ts.map +1 -0
- package/dist/model/boundary-rule-scope.js +175 -0
- package/dist/model/boundary-rule.d.ts +120 -5
- package/dist/model/boundary-rule.d.ts.map +1 -1
- package/dist/model/boundary-rule.js +148 -1
- package/dist/model/boundary-unit-problem-issue.d.ts +9 -0
- package/dist/model/boundary-unit-problem-issue.d.ts.map +1 -0
- package/dist/model/boundary-unit-problem-issue.js +16 -0
- package/dist/model/normalize-boundary-rule.d.ts +21 -0
- package/dist/model/normalize-boundary-rule.d.ts.map +1 -0
- package/dist/model/normalize-boundary-rule.js +62 -0
- package/dist/policy/evaluate-policy.d.ts +80 -3
- package/dist/policy/evaluate-policy.d.ts.map +1 -1
- package/dist/policy/evaluate-policy.js +152 -19
- package/dist/policy/i-policy-rule-liveness.d.ts +15 -0
- package/dist/policy/i-policy-rule-liveness.d.ts.map +1 -0
- package/dist/policy/i-policy-rule-liveness.js +1 -0
- package/dist/policy/run-policy.d.ts +1 -1
- package/dist/policy/run-policy.d.ts.map +1 -1
- package/dist/policy/run-policy.js +171 -21
- package/dist/registry/load-boundary-rules.d.ts +29 -1
- package/dist/registry/load-boundary-rules.d.ts.map +1 -1
- package/dist/registry/load-boundary-rules.js +38 -8
- package/dist/scan/glob.d.ts +90 -0
- package/dist/scan/glob.d.ts.map +1 -1
- package/dist/scan/glob.js +205 -0
- package/dist/scan/i-glob-unit-measure.d.ts +22 -0
- package/dist/scan/i-glob-unit-measure.d.ts.map +1 -0
- package/dist/scan/i-glob-unit-measure.js +1 -0
- package/dist/scan/import-pattern.d.ts +109 -0
- package/dist/scan/import-pattern.d.ts.map +1 -0
- package/dist/scan/import-pattern.js +191 -0
- package/dist/scan/node-builtin-package-names.d.ts +13 -0
- package/dist/scan/node-builtin-package-names.d.ts.map +1 -0
- package/dist/scan/node-builtin-package-names.js +25 -0
- package/dist/scan/scan-imports.d.ts +36 -3
- package/dist/scan/scan-imports.d.ts.map +1 -1
- package/dist/scan/scan-imports.js +114 -45
- package/dist/util/blank-run-hazard-finding.d.ts +34 -0
- package/dist/util/blank-run-hazard-finding.d.ts.map +1 -0
- package/dist/util/blank-run-hazard-finding.js +1 -0
- package/dist/util/blank-run-hazard.d.ts +29 -0
- package/dist/util/blank-run-hazard.d.ts.map +1 -0
- package/dist/util/blank-run-hazard.js +450 -0
- package/dist/util/dead-glob-units.d.ts +46 -0
- package/dist/util/dead-glob-units.d.ts.map +1 -0
- package/dist/util/dead-glob-units.js +116 -0
- package/dist/util/glob-list-liveness-input.d.ts +31 -0
- package/dist/util/glob-list-liveness-input.d.ts.map +1 -0
- package/dist/util/glob-list-liveness-input.js +65 -0
- package/dist/util/i-dead-glob-unit.d.ts +24 -0
- package/dist/util/i-dead-glob-unit.d.ts.map +1 -0
- package/dist/util/i-dead-glob-unit.js +1 -0
- package/dist/util/i-glob-list-units.d.ts +25 -0
- package/dist/util/i-glob-list-units.d.ts.map +1 -0
- package/dist/util/i-glob-list-units.js +1 -0
- package/dist/util/i-glob-liveness-list.d.ts +23 -0
- package/dist/util/i-glob-liveness-list.d.ts.map +1 -0
- package/dist/util/i-glob-liveness-list.js +1 -0
- package/dist/util/i-glob-liveness-request.d.ts +11 -0
- package/dist/util/i-glob-liveness-request.d.ts.map +1 -0
- package/dist/util/i-glob-liveness-request.js +1 -0
- package/dist/util/i-glob-negation.d.ts +13 -0
- package/dist/util/i-glob-negation.d.ts.map +1 -0
- package/dist/util/i-glob-negation.js +1 -0
- package/dist/util/matched-files.d.ts +22 -0
- package/dist/util/matched-files.d.ts.map +1 -0
- package/dist/util/matched-files.js +1 -0
- package/dist/util/negation-cause.d.ts +15 -0
- package/dist/util/negation-cause.d.ts.map +1 -0
- package/dist/util/negation-cause.js +21 -0
- package/dist/util/plane-scan-exclude-dirs.d.ts +17 -0
- package/dist/util/plane-scan-exclude-dirs.d.ts.map +1 -0
- package/dist/util/plane-scan-exclude-dirs.js +22 -0
- package/dist/util/read-glob-list-liveness.d.ts +20 -0
- package/dist/util/read-glob-list-liveness.d.ts.map +1 -0
- package/dist/util/read-glob-list-liveness.js +20 -0
- package/dist/util/read-scope-coverage.d.ts +90 -0
- package/dist/util/read-scope-coverage.d.ts.map +1 -0
- package/dist/util/read-scope-coverage.js +173 -0
- package/dist/util/read-scope.d.ts +14 -0
- package/dist/util/read-scope.d.ts.map +1 -0
- package/dist/util/read-scope.js +1 -0
- package/dist/util/read-selected-files.d.ts +16 -0
- package/dist/util/read-selected-files.d.ts.map +1 -0
- package/dist/util/read-selected-files.js +25 -0
- package/dist/util/settle-glob-lists.d.ts +12 -0
- package/dist/util/settle-glob-lists.d.ts.map +1 -0
- package/dist/util/settle-glob-lists.js +13 -0
- package/dist/util/unread-file-reason.d.ts +26 -0
- package/dist/util/unread-file-reason.d.ts.map +1 -0
- package/dist/util/unread-file-reason.js +26 -0
- package/dist/util/unread-file.d.ts +10 -0
- package/dist/util/unread-file.d.ts.map +1 -0
- package/dist/util/unread-file.js +1 -0
- package/dist/util/walk-files.d.ts +74 -10
- package/dist/util/walk-files.d.ts.map +1 -1
- package/dist/util/walk-files.js +137 -31
- package/dist/wiring/evaluate-wiring.d.ts +79 -5
- package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
- package/dist/wiring/evaluate-wiring.js +267 -12
- package/dist/wiring/explain-wiring.d.ts +49 -5
- package/dist/wiring/explain-wiring.d.ts.map +1 -1
- package/dist/wiring/explain-wiring.js +118 -10
- package/dist/wiring/i-idiom-role-coverage.d.ts +24 -0
- package/dist/wiring/i-idiom-role-coverage.d.ts.map +1 -0
- package/dist/wiring/i-idiom-role-coverage.js +1 -0
- package/dist/wiring/i-registration-query-verdict.d.ts +38 -0
- package/dist/wiring/i-registration-query-verdict.d.ts.map +1 -0
- package/dist/wiring/i-registration-query-verdict.js +1 -0
- package/dist/wiring/i-registration-roles.d.ts +71 -0
- package/dist/wiring/i-registration-roles.d.ts.map +1 -0
- package/dist/wiring/i-registration-roles.js +1 -0
- package/dist/wiring/measure-idiom-role-coverage.d.ts +12 -0
- package/dist/wiring/measure-idiom-role-coverage.d.ts.map +1 -0
- package/dist/wiring/measure-idiom-role-coverage.js +20 -0
- package/dist/wiring/measure-registration-roles.d.ts +25 -0
- package/dist/wiring/measure-registration-roles.d.ts.map +1 -0
- package/dist/wiring/measure-registration-roles.js +124 -0
- package/dist/wiring/plan-wiring-fix.js +2 -2
- package/dist/wiring/registration-graph.d.ts +12 -0
- package/dist/wiring/registration-graph.d.ts.map +1 -1
- package/dist/wiring/registration-graph.js +16 -5
- package/dist/wiring/registration-query-verdict.d.ts +23 -0
- package/dist/wiring/registration-query-verdict.d.ts.map +1 -0
- package/dist/wiring/registration-query-verdict.js +118 -0
- package/dist/wiring/registry-query.d.ts +8 -0
- package/dist/wiring/registry-query.d.ts.map +1 -1
- package/dist/wiring/registry-query.js +12 -5
- package/dist/wiring/scan-wiring-files.d.ts +4 -2
- package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
- package/dist/wiring/scan-wiring-files.js +52 -15
- package/dist/wiring/sink-imports.d.ts +11 -0
- package/dist/wiring/sink-imports.d.ts.map +1 -1
- package/dist/wiring/sink-imports.js +11 -2
- package/dist/wiring/trace-literal.d.ts +6 -0
- package/dist/wiring/trace-literal.d.ts.map +1 -1
- package/dist/wiring/trace-literal.js +5 -2
- package/dist/wiring/wiring-labeled-sources.d.ts +12 -0
- package/dist/wiring/wiring-labeled-sources.d.ts.map +1 -0
- package/dist/wiring/wiring-labeled-sources.js +20 -0
- package/package.json +2 -2
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { type IPolicyRule, type PolicySurface } from '@shrkcrft/core';
|
|
1
|
+
import { type IPolicyRule, type IUnitLiveness, type IUnitStateLists, type IVerdictCoverage, type PolicySurface } from '@shrkcrft/core';
|
|
2
|
+
import type { IPolicyRuleLiveness } from './i-policy-rule-liveness.js';
|
|
3
|
+
import type { IReadScope } from '../util/read-scope.js';
|
|
4
|
+
import type { IUnreadFile } from '../util/unread-file.js';
|
|
5
|
+
import type { IDeadGlobUnit } from '../util/i-dead-glob-unit.js';
|
|
6
|
+
import type { IGlobNegation } from '../util/i-glob-negation.js';
|
|
2
7
|
export declare const POLICY_LINT_SCHEMA: "sharkcraft.policy-lint/v1";
|
|
3
8
|
/**
|
|
4
9
|
* A chunk of content to scan for one rule. For whole files `baseLine` is 1; for
|
|
@@ -52,6 +57,49 @@ export interface IPolicyRuleResult {
|
|
|
52
57
|
/** Content units actually scanned — the stale-glob signal. */
|
|
53
58
|
readonly unitsScanned: number;
|
|
54
59
|
readonly error?: string;
|
|
60
|
+
/**
|
|
61
|
+
* What the rule examined against what it was asked to (`IVerdictCoverage`,
|
|
62
|
+
* unit `content units`) — the ONE per-rule record `policy-lint`, `gates
|
|
63
|
+
* check`, `quality`, `shrk gate` and `finish` all settle on. A rule that
|
|
64
|
+
* scanned nothing, or is misconfigured, examined nothing (`expected: 0` — a
|
|
65
|
+
* shortfall). The engine fills it in; it never decides a verdict from it.
|
|
66
|
+
*/
|
|
67
|
+
readonly coverage: IVerdictCoverage;
|
|
68
|
+
/**
|
|
69
|
+
* Glob-matched files in the rule's scope that the one reader did not read
|
|
70
|
+
* (over the read cap, or unreadable). Set only when non-empty. `coverage`
|
|
71
|
+
* names them too: such a rule is never a pass.
|
|
72
|
+
*/
|
|
73
|
+
readonly unread?: readonly IUnreadFile[];
|
|
74
|
+
/**
|
|
75
|
+
* Globs of the rule's own `files[]` that do nothing — an inclusion glob
|
|
76
|
+
* that selects no file in the walk, or a negation that excludes none — a
|
|
77
|
+
* dead unit inside a rule whose other globs keep it scanning. Set only when
|
|
78
|
+
* non-empty, and only for author-written `files` (a surface's default globs
|
|
79
|
+
* are not the author's selector). Advisory; `gates coverage` reports it.
|
|
80
|
+
*/
|
|
81
|
+
readonly deadGlobs?: readonly string[];
|
|
82
|
+
/** The units behind {@link deadGlobs}, same order, each with its reason (`globListUnits`). */
|
|
83
|
+
readonly deadGlobUnits?: readonly IDeadGlobUnit[];
|
|
84
|
+
/** Live negations of the rule's own `files[]`, each with the files it excludes. Set only when non-empty. */
|
|
85
|
+
readonly negations?: readonly IGlobNegation[];
|
|
86
|
+
/**
|
|
87
|
+
* Set only when the rule scanned nothing BECAUSE of its own `files[]`
|
|
88
|
+
* negations: its inclusion globs matched files and these negations excluded
|
|
89
|
+
* every one (`globListUnits`' `allExcluded`). That is not a stale selector,
|
|
90
|
+
* so its skip reason and coverage reason say what happened instead.
|
|
91
|
+
*/
|
|
92
|
+
readonly excludedByNegations?: readonly IGlobNegation[];
|
|
93
|
+
/**
|
|
94
|
+
* The acceptance of the rule's `expectEmpty` units — settle record B of its
|
|
95
|
+
* `files` (round 13). Folded into the envelope's ONE settle beside
|
|
96
|
+
* {@link coverage}; when the rule is intended-empty it IS {@link coverage}.
|
|
97
|
+
*/
|
|
98
|
+
readonly unitAcceptance?: IVerdictCoverage;
|
|
99
|
+
/** The rule's dead / intended-empty / went-live `files` units as printed lines (`unitStateLists`). */
|
|
100
|
+
readonly units?: IUnitStateLists;
|
|
101
|
+
/** The rule's non-live `files` units (dead, intended-empty, went-live, unproven), for `--fail-on-dead-units` (`selectorUnitFails`). */
|
|
102
|
+
readonly unitLiveness?: readonly IUnitLiveness[];
|
|
55
103
|
}
|
|
56
104
|
/** A rule that scanned nothing, reported loudly instead of as a green pass. */
|
|
57
105
|
export interface IPolicySkip {
|
|
@@ -74,9 +122,21 @@ export interface IPolicyReport {
|
|
|
74
122
|
* Count of rules that actually scanned ≥1 unit. A rule whose globs matched 0
|
|
75
123
|
* files (e.g. a `style` rule in a project with no stylesheets) is NOT
|
|
76
124
|
* evaluated — a loud skip rather than a green pass. Misconfigured rules count
|
|
77
|
-
* as evaluated so their error is not swallowed
|
|
125
|
+
* as evaluated so their error is not swallowed, and so does a rule whose
|
|
126
|
+
* globs matched a file the reader could not read (it ran, and is PARTIAL:
|
|
127
|
+
* its coverage names the unread file). A rule ACCEPTED as intended-empty
|
|
128
|
+
* (round 13 — every inclusion glob of its `files` marked `expectEmpty`, 0
|
|
129
|
+
* files matched) is counted too, so the verdict path's `evaluated === 0`
|
|
130
|
+
* NOT-VERIFIED guard never reads an accepted plan as "nothing ran"; the
|
|
131
|
+
* printed "N evaluated" excludes it: N = `evaluated − acceptedEmpty`.
|
|
78
132
|
*/
|
|
79
133
|
readonly evaluated: number;
|
|
134
|
+
/**
|
|
135
|
+
* Rules accepted as intended-empty (round 13, K6): they examined 0 files, so
|
|
136
|
+
* a renderer prints them apart — `N evaluated, M accepted as
|
|
137
|
+
* intended-empty` — never inside the evaluated count.
|
|
138
|
+
*/
|
|
139
|
+
readonly acceptedEmpty: number;
|
|
80
140
|
readonly verdict: 'pass' | 'errors' | 'warnings';
|
|
81
141
|
}
|
|
82
142
|
/** Resolves the content units to scan for a given rule (injected for purity/testability). */
|
|
@@ -89,6 +149,23 @@ export type PolicyUnitResolver = (rule: IPolicyRule) => readonly IPolicyUnit[];
|
|
|
89
149
|
* them, so `explain` can show what was matched AND what was let through. A
|
|
90
150
|
* misconfigured rule (uncompilable regex) degrades to a diagnostic — never
|
|
91
151
|
* throws, so one bad rule cannot crash the check.
|
|
152
|
+
*
|
|
153
|
+
* `scopeOf` reports each rule's read scope: the glob-matched files the one
|
|
154
|
+
* reader read and the ones it did not. A rule with an unread file in scope
|
|
155
|
+
* examined less than its globs matched, so its coverage is the file record
|
|
156
|
+
* (`readScopeCoverage`) and it is never a pass. When the unread file is why it
|
|
157
|
+
* scanned no unit at all, it is PARTIAL, not a `failOnEmpty` failure: it
|
|
158
|
+
* matched something it could not read, which is not "matched nothing".
|
|
159
|
+
*/
|
|
160
|
+
export declare function evaluatePolicy(rules: readonly IPolicyRule[], resolve: PolicyUnitResolver, scopeOf?: (rule: IPolicyRule) => IReadScope,
|
|
161
|
+
/**
|
|
162
|
+
* The rule's own `files` judged per unit and settled with its `expectEmpty`
|
|
163
|
+
* markers (round 13) — `runPolicyLint` supplies it from the walk it already
|
|
164
|
+
* did. A rule that scanned nothing is decided from it by THE rule-emptiness
|
|
165
|
+
* settle (`settleRuleEmptiness`): every inclusion glob intended-empty and no
|
|
166
|
+
* file matched → accepted (`passed`, the acceptance as its coverage), never a
|
|
167
|
+
* failOnEmpty failure. Absent (hand-driven, or no own `files`): no unit is
|
|
168
|
+
* marked, so an empty rule is a loud skip exactly as before.
|
|
92
169
|
*/
|
|
93
|
-
|
|
170
|
+
livenessOf?: (rule: IPolicyRule) => IPolicyRuleLiveness | undefined): IPolicyReport;
|
|
94
171
|
//# sourceMappingURL=evaluate-policy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EAErB,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AAIxB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAIhE,eAAO,MAAM,kBAAkB,EAAG,2BAAoC,CAAC;AAEvE;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC;AAED,oFAAoF;AACpF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+BAA+B;IAC/B,QAAQ,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,GAAG,UAAU,CAAC;CAC1D;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kEAAkE;IAClE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACzC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,8FAA8F;IAC9F,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAClD,4GAA4G;IAC5G,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IACxD;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAC3C,sGAAsG;IACtG,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC,uIAAuI;IACvI,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CAClD;AAED,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;CACxC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,kBAAkB,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,qEAAqE;IACrE,QAAQ,CAAC,UAAU,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACnD,kCAAkC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IACzC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;CAClD;AAED,6FAA6F;AAC7F,MAAM,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,WAAW,EAAE,CAAC;AAiE/E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,EAAE,kBAAkB,EAC3B,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,UAAU;AAC3C;;;;;;;;GAQG;AACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,mBAAmB,GAAG,SAAS,GAClE,aAAa,CA6Pf"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { failsWhenEmpty } from '@shrkcrft/core';
|
|
2
|
-
import { lexCodeZones,
|
|
1
|
+
import { failsWhenEmpty, normalizeRuleList, RuleEmptiness, settleRuleEmptiness, } from '@shrkcrft/core';
|
|
2
|
+
import { lexCodeZones, zoneKeepsAt } from "../extract/code-zones.js";
|
|
3
|
+
import { allExcludedCause } from "../util/negation-cause.js";
|
|
4
|
+
import { settleGlobLists } from "../util/settle-glob-lists.js";
|
|
5
|
+
import { readScopeCoverage, readScopeHasUnread } from "../util/read-scope-coverage.js";
|
|
3
6
|
import { safeCompile } from "../util/safe-regex.js";
|
|
4
7
|
export const POLICY_LINT_SCHEMA = 'sharkcraft.policy-lint/v1';
|
|
5
8
|
function lineWithin(content, index) {
|
|
@@ -24,19 +27,34 @@ function markerNear(lines, line, marker) {
|
|
|
24
27
|
return above !== undefined && above.includes(marker);
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
27
|
-
* Whether the zone rule keeps a match at `index`.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
+
* Whether the zone rule keeps a match at `index`.
|
|
31
|
+
*
|
|
32
|
+
* Delegates the zone semantics to {@link zoneKeepsAt} — the one authority the
|
|
33
|
+
* extraction DSL also reads — and adds only the policy-specific exemption:
|
|
34
|
+
* inline-template units are NOT zoned, because their content is already a
|
|
35
|
+
* string body lifted out of a source file.
|
|
30
36
|
*/
|
|
31
37
|
function zoneKeeps(zone, unit, zones, index) {
|
|
32
38
|
if (zone === 'all' || unit.inlineTemplate || zones === undefined)
|
|
33
39
|
return true;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
return zoneKeepsAt(zone, zones, index);
|
|
41
|
+
}
|
|
42
|
+
/** Why a rule that scanned no content unit examined nothing. */
|
|
43
|
+
const NO_UNITS_REASON = '0 content units matched the rule globs';
|
|
44
|
+
/**
|
|
45
|
+
* What one policy rule examined: every content unit its globs matched (whole
|
|
46
|
+
* files, or inline-template bodies). A rule that scanned nothing, or is
|
|
47
|
+
* misconfigured, examined nothing — `expected: 0`, which core's
|
|
48
|
+
* `coverageShortfall` reports as a gap. The one derivation; every surface that
|
|
49
|
+
* settles a policy verdict reads the record this puts on the rule result.
|
|
50
|
+
*/
|
|
51
|
+
function policyCoverage(outcome) {
|
|
52
|
+
const unit = 'content units';
|
|
53
|
+
if (outcome.kind === 'error')
|
|
54
|
+
return { unit, expected: 0, examined: 0, reason: 'the rule is misconfigured' };
|
|
55
|
+
if (outcome.kind === 'skipped')
|
|
56
|
+
return { unit, expected: 0, examined: 0, reason: outcome.reason };
|
|
57
|
+
return { unit, expected: outcome.units, examined: outcome.units };
|
|
40
58
|
}
|
|
41
59
|
/**
|
|
42
60
|
* Pure policy evaluation. Each rule's regex is run over the units the resolver
|
|
@@ -46,18 +64,66 @@ function zoneKeeps(zone, unit, zones, index) {
|
|
|
46
64
|
* them, so `explain` can show what was matched AND what was let through. A
|
|
47
65
|
* misconfigured rule (uncompilable regex) degrades to a diagnostic — never
|
|
48
66
|
* throws, so one bad rule cannot crash the check.
|
|
67
|
+
*
|
|
68
|
+
* `scopeOf` reports each rule's read scope: the glob-matched files the one
|
|
69
|
+
* reader read and the ones it did not. A rule with an unread file in scope
|
|
70
|
+
* examined less than its globs matched, so its coverage is the file record
|
|
71
|
+
* (`readScopeCoverage`) and it is never a pass. When the unread file is why it
|
|
72
|
+
* scanned no unit at all, it is PARTIAL, not a `failOnEmpty` failure: it
|
|
73
|
+
* matched something it could not read, which is not "matched nothing".
|
|
74
|
+
*/
|
|
75
|
+
export function evaluatePolicy(rules, resolve, scopeOf,
|
|
76
|
+
/**
|
|
77
|
+
* The rule's own `files` judged per unit and settled with its `expectEmpty`
|
|
78
|
+
* markers (round 13) — `runPolicyLint` supplies it from the walk it already
|
|
79
|
+
* did. A rule that scanned nothing is decided from it by THE rule-emptiness
|
|
80
|
+
* settle (`settleRuleEmptiness`): every inclusion glob intended-empty and no
|
|
81
|
+
* file matched → accepted (`passed`, the acceptance as its coverage), never a
|
|
82
|
+
* failOnEmpty failure. Absent (hand-driven, or no own `files`): no unit is
|
|
83
|
+
* marked, so an empty rule is a loud skip exactly as before.
|
|
49
84
|
*/
|
|
50
|
-
|
|
85
|
+
livenessOf) {
|
|
51
86
|
const ruleResults = [];
|
|
52
87
|
const findings = [];
|
|
53
88
|
const suppressed = [];
|
|
54
89
|
const diagnostics = [];
|
|
55
90
|
const skipped = [];
|
|
56
91
|
let evaluated = 0;
|
|
92
|
+
let acceptedEmpty = 0;
|
|
57
93
|
let misconfigError = false;
|
|
58
94
|
let misconfigWarn = false;
|
|
59
|
-
for (const
|
|
60
|
-
const severity =
|
|
95
|
+
for (const authored of rules) {
|
|
96
|
+
const severity = authored.severity ?? 'error';
|
|
97
|
+
// The engine entry normalises idempotently (round 13): a loaded rule comes
|
|
98
|
+
// back as the SAME object; a hand-built `{ pattern, expectEmpty }` entry is
|
|
99
|
+
// a unit plus a marker; a malformed entry is a misconfigured rule — never
|
|
100
|
+
// an object handed to a glob reader, never a crash.
|
|
101
|
+
const normalized = normalizeRuleList(authored, 'files');
|
|
102
|
+
if (!normalized.ok) {
|
|
103
|
+
const msg = `rule "${authored.id}": ${normalized.error.message}`;
|
|
104
|
+
diagnostics.push(msg);
|
|
105
|
+
if (severity === 'error')
|
|
106
|
+
misconfigError = true;
|
|
107
|
+
else
|
|
108
|
+
misconfigWarn = true;
|
|
109
|
+
ruleResults.push({
|
|
110
|
+
ruleId: authored.id,
|
|
111
|
+
surface: authored.surface,
|
|
112
|
+
severity,
|
|
113
|
+
status: 'error',
|
|
114
|
+
findingCount: 0,
|
|
115
|
+
suppressedCount: 0,
|
|
116
|
+
unitsScanned: 0,
|
|
117
|
+
error: msg,
|
|
118
|
+
coverage: policyCoverage({ kind: 'error' }),
|
|
119
|
+
});
|
|
120
|
+
evaluated += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const rule = normalized.value;
|
|
124
|
+
const lv = livenessOf?.(rule);
|
|
125
|
+
const acceptance = lv?.liveness.acceptance;
|
|
126
|
+
const acceptanceField = acceptance !== undefined ? { unitAcceptance: acceptance } : {};
|
|
61
127
|
const { re, error } = safeCompile(rule.pattern, rule.flags);
|
|
62
128
|
if (error || !re) {
|
|
63
129
|
const msg = `rule "${rule.id}": ${error}`;
|
|
@@ -75,6 +141,7 @@ export function evaluatePolicy(rules, resolve) {
|
|
|
75
141
|
suppressedCount: 0,
|
|
76
142
|
unitsScanned: 0,
|
|
77
143
|
error: msg,
|
|
144
|
+
coverage: policyCoverage({ kind: 'error' }),
|
|
78
145
|
});
|
|
79
146
|
// A misconfigured rule attempted to run — count it as evaluated so its
|
|
80
147
|
// error isn't swallowed by the gate's `evaluated === 0` skip path.
|
|
@@ -148,18 +215,77 @@ export function evaluatePolicy(rules, resolve) {
|
|
|
148
215
|
suppressedCount,
|
|
149
216
|
unitsScanned: units.length,
|
|
150
217
|
error: msg,
|
|
218
|
+
coverage: policyCoverage({ kind: 'error' }),
|
|
151
219
|
});
|
|
152
220
|
evaluated += 1;
|
|
153
221
|
continue;
|
|
154
222
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
223
|
+
const scope = scopeOf?.(rule);
|
|
224
|
+
const unreadField = scope !== undefined && scope.unread.length > 0 ? { unread: scope.unread } : {};
|
|
225
|
+
if (units.length === 0 && readScopeHasUnread(scope)) {
|
|
226
|
+
// Its globs matched files, but the reader could not read them. That is
|
|
227
|
+
// not a stale glob: it ran over everything readable (nothing) and is
|
|
228
|
+
// PARTIAL, naming the unread files. Never failOnEmpty's 1, never a pass.
|
|
229
|
+
evaluated += 1;
|
|
230
|
+
ruleResults.push({
|
|
158
231
|
ruleId: rule.id,
|
|
159
|
-
|
|
160
|
-
failed,
|
|
232
|
+
surface: rule.surface,
|
|
161
233
|
severity,
|
|
234
|
+
status: 'passed',
|
|
235
|
+
findingCount: 0,
|
|
236
|
+
suppressedCount: 0,
|
|
237
|
+
unitsScanned: 0,
|
|
238
|
+
...unreadField,
|
|
239
|
+
...acceptanceField,
|
|
240
|
+
coverage: readScopeCoverage(policyCoverage({ kind: 'skipped', reason: NO_UNITS_REASON }), scope),
|
|
162
241
|
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (units.length === 0) {
|
|
245
|
+
// THE rule-emptiness settle (round 13, `settleRuleEmptiness`): every
|
|
246
|
+
// inclusion glob of the rule's own `files` marked `expectEmpty` and no
|
|
247
|
+
// file matched → the empty result is the INTENDED one, accepted and
|
|
248
|
+
// printed; anything else — a dead or unmarked glob, files that yielded no
|
|
249
|
+
// content unit — is the loud skip it always was (failOnEmpty's 1, else 2).
|
|
250
|
+
const emptiedBy = lv?.units.allExcluded === true ? lv.units.negations : undefined;
|
|
251
|
+
const emptiness = settleRuleEmptiness({
|
|
252
|
+
subject: rule.id,
|
|
253
|
+
unitLabel: 'content units',
|
|
254
|
+
filesMatched: scope?.read ?? 0,
|
|
255
|
+
unitsMatched: 0,
|
|
256
|
+
unread: false,
|
|
257
|
+
emptiedByNegations: emptiedBy !== undefined,
|
|
258
|
+
...(emptiedBy !== undefined ? { emptiedReason: `0 content units: ${allExcludedCause(emptiedBy)}` } : {}),
|
|
259
|
+
liveness: lv?.liveness ?? settleGlobLists({ subject: rule.id, lists: [], marks: [] }),
|
|
260
|
+
primaryLists: ['files'],
|
|
261
|
+
failOnEmpty: failsWhenEmpty(rule),
|
|
262
|
+
noFilesReason: NO_UNITS_REASON,
|
|
263
|
+
noUnitsReason: NO_UNITS_REASON,
|
|
264
|
+
});
|
|
265
|
+
if (emptiness.state === RuleEmptiness.IntendedEmpty && emptiness.coverage !== undefined) {
|
|
266
|
+
// Counted in `evaluated` (the verdict path's "nothing ran" guard must
|
|
267
|
+
// never read an accepted plan as a skip) AND in `acceptedEmpty`, which
|
|
268
|
+
// a renderer subtracts: `N evaluated, M accepted as intended-empty`.
|
|
269
|
+
evaluated += 1;
|
|
270
|
+
acceptedEmpty += 1;
|
|
271
|
+
ruleResults.push({
|
|
272
|
+
ruleId: rule.id,
|
|
273
|
+
surface: rule.surface,
|
|
274
|
+
severity,
|
|
275
|
+
status: 'passed',
|
|
276
|
+
findingCount: 0,
|
|
277
|
+
suppressedCount: 0,
|
|
278
|
+
unitsScanned: 0,
|
|
279
|
+
...unreadField,
|
|
280
|
+
// The acceptance IS the rule's coverage (the same record, folded once).
|
|
281
|
+
coverage: emptiness.coverage,
|
|
282
|
+
unitAcceptance: emptiness.coverage,
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const failed = emptiness.fails;
|
|
287
|
+
const reason = emptiness.skipReason ?? NO_UNITS_REASON;
|
|
288
|
+
skipped.push({ ruleId: rule.id, reason, failed, severity });
|
|
163
289
|
if (failed) {
|
|
164
290
|
if (severity === 'error')
|
|
165
291
|
misconfigError = true;
|
|
@@ -174,6 +300,9 @@ export function evaluatePolicy(rules, resolve) {
|
|
|
174
300
|
findingCount: 0,
|
|
175
301
|
suppressedCount: 0,
|
|
176
302
|
unitsScanned: 0,
|
|
303
|
+
...unreadField,
|
|
304
|
+
...acceptanceField,
|
|
305
|
+
coverage: readScopeCoverage(policyCoverage({ kind: 'skipped', reason: emptiedBy !== undefined ? allExcludedCause(emptiedBy) : reason }), scope),
|
|
177
306
|
});
|
|
178
307
|
continue;
|
|
179
308
|
}
|
|
@@ -186,6 +315,9 @@ export function evaluatePolicy(rules, resolve) {
|
|
|
186
315
|
findingCount: count,
|
|
187
316
|
suppressedCount,
|
|
188
317
|
unitsScanned: units.length,
|
|
318
|
+
...unreadField,
|
|
319
|
+
...acceptanceField,
|
|
320
|
+
coverage: readScopeCoverage(policyCoverage({ kind: 'ran', units: units.length }), scope),
|
|
189
321
|
});
|
|
190
322
|
}
|
|
191
323
|
const hasError = misconfigError || findings.some((f) => f.severity === 'error');
|
|
@@ -198,6 +330,7 @@ export function evaluatePolicy(rules, resolve) {
|
|
|
198
330
|
suppressed,
|
|
199
331
|
skipped,
|
|
200
332
|
evaluated,
|
|
333
|
+
acceptedEmpty,
|
|
201
334
|
verdict: hasError ? 'errors' : hasWarn ? 'warnings' : 'pass',
|
|
202
335
|
};
|
|
203
336
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ISettledUnitLiveness } from '@shrkcrft/core';
|
|
2
|
+
import type { IGlobListUnits } from '../util/i-glob-list-units.js';
|
|
3
|
+
/**
|
|
4
|
+
* One policy rule's own `files` judged per unit and settled with its
|
|
5
|
+
* `expectEmpty` markers (round 13) — what `evaluatePolicy` decides a rule that
|
|
6
|
+
* scanned nothing from (`settleRuleEmptiness`), and what `runPolicyLint`
|
|
7
|
+
* reports as its dead / intended-empty / went-live units.
|
|
8
|
+
*/
|
|
9
|
+
export interface IPolicyRuleLiveness {
|
|
10
|
+
/** The list's units, by the one dead-unit decision (`globListUnits`). */
|
|
11
|
+
readonly units: IGlobListUnits;
|
|
12
|
+
/** The settle (`settleUnitLiveness` over `globListLivenessInput`). */
|
|
13
|
+
readonly liveness: ISettledUnitLiveness;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=i-policy-rule-liveness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i-policy-rule-liveness.d.ts","sourceRoot":"","sources":["../../src/policy/i-policy-rule-liveness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;CACzC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IPolicyRule, type PolicySurface } from '@shrkcrft/core';
|
|
2
2
|
import { type IPolicyReport } from './evaluate-policy.js';
|
|
3
3
|
export interface IRunPolicyOptions {
|
|
4
4
|
/** Restrict to rules on these surfaces. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-policy.d.ts","sourceRoot":"","sources":["../../src/policy/run-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"run-policy.d.ts","sourceRoot":"","sources":["../../src/policy/run-policy.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AASxB,OAAO,EAAkB,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAa5F,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC7C,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,kEAAkE;IAClE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,2FAA2F;IAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AA2BD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CAgMf"}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as nodePath from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeRuleList, settleUnitLiveness, unitStateLists, UnitLivenessState, } from '@shrkcrft/core';
|
|
3
|
+
import { globListSelects } from "../scan/glob.js";
|
|
4
|
+
import { readScopeHasUnread, readScopeOf, unreadEntryWhollyMatches } from "../util/read-scope-coverage.js";
|
|
3
5
|
import { readMatchingFiles } from "../util/walk-files.js";
|
|
6
|
+
import { globListUnits } from "../util/dead-glob-units.js";
|
|
7
|
+
import { globListLivenessInput } from "../util/glob-list-liveness-input.js";
|
|
8
|
+
import { allExcludedCause } from "../util/negation-cause.js";
|
|
4
9
|
import { extractInlineTemplates } from "./extract-templates.js";
|
|
5
10
|
import { evaluatePolicy } from "./evaluate-policy.js";
|
|
6
11
|
/** Per-surface default globs when a rule omits `files`. */
|
|
@@ -14,6 +19,25 @@ const SOURCE_EXT = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs'
|
|
|
14
19
|
function globsFor(rule) {
|
|
15
20
|
return rule.files && rule.files.length > 0 ? rule.files : SURFACE_DEFAULT_GLOBS[rule.surface];
|
|
16
21
|
}
|
|
22
|
+
// Why a rule scanned nothing when its OWN negations emptied its `files[]` is
|
|
23
|
+
// THE shared cause (`allExcludedCause`, util/negation-cause.ts — the wiring
|
|
24
|
+
// engine words its source side from the same module). The coverage reason
|
|
25
|
+
// carries it bare (the shortfall line already says "0 content units"); the
|
|
26
|
+
// skip reason leads with the count. `evaluatePolicy` words both.
|
|
27
|
+
/** The report of a run with no rule in scope. */
|
|
28
|
+
function emptyReport() {
|
|
29
|
+
return {
|
|
30
|
+
schema: 'sharkcraft.policy-lint/v1',
|
|
31
|
+
rules: [],
|
|
32
|
+
findings: [],
|
|
33
|
+
diagnostics: [],
|
|
34
|
+
suppressed: [],
|
|
35
|
+
skipped: [],
|
|
36
|
+
evaluated: 0,
|
|
37
|
+
acceptedEmpty: 0,
|
|
38
|
+
verdict: 'pass',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
17
41
|
/**
|
|
18
42
|
* Filesystem-backed policy-lint. Walks the project once; on the `template`
|
|
19
43
|
* surface, source files contribute their inline `template:` bodies (with real
|
|
@@ -21,7 +45,19 @@ function globsFor(rule) {
|
|
|
21
45
|
* output; the only IO is the read-only walk + reads.
|
|
22
46
|
*/
|
|
23
47
|
export function runPolicyLint(projectRoot, rules, options = {}) {
|
|
24
|
-
|
|
48
|
+
// The engine entry normalises idempotently (round 13): a loaded rule comes
|
|
49
|
+
// back as the SAME object (so every per-rule memo below hits), a hand-built
|
|
50
|
+
// `{ pattern, expectEmpty }` entry becomes a unit plus a marker. A MALFORMED
|
|
51
|
+
// entry is left for `evaluatePolicy` to report as a misconfigured rule, and
|
|
52
|
+
// no glob reader below is ever handed it.
|
|
53
|
+
const malformed = new Set();
|
|
54
|
+
let selected = rules.map((r) => {
|
|
55
|
+
const n = normalizeRuleList(r, 'files');
|
|
56
|
+
if (n.ok)
|
|
57
|
+
return n.value;
|
|
58
|
+
malformed.add(r);
|
|
59
|
+
return r;
|
|
60
|
+
});
|
|
25
61
|
if (options.surfaces && options.surfaces.length > 0) {
|
|
26
62
|
const s = new Set(options.surfaces);
|
|
27
63
|
selected = selected.filter((r) => s.has(r.surface));
|
|
@@ -32,29 +68,142 @@ export function runPolicyLint(projectRoot, rules, options = {}) {
|
|
|
32
68
|
}
|
|
33
69
|
if (options.changedOnly) {
|
|
34
70
|
const changed = options.changedFiles ?? [];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
schema: 'sharkcraft.policy-lint/v1',
|
|
40
|
-
rules: [],
|
|
41
|
-
findings: [],
|
|
42
|
-
diagnostics: [],
|
|
43
|
-
suppressed: [],
|
|
44
|
-
skipped: [],
|
|
45
|
-
evaluated: 0,
|
|
46
|
-
verdict: 'pass',
|
|
47
|
-
};
|
|
71
|
+
// A changed file the rule's own `files` EXCLUDE (a `!` entry) cannot put a
|
|
72
|
+
// finding in its scope, so it does not select the rule. A malformed rule
|
|
73
|
+
// has no footprint to prove it out of scope, so it stays in (and errors).
|
|
74
|
+
selected = selected.filter((r) => malformed.has(r) || changed.some((c) => globListSelects(c, globsFor(r))));
|
|
48
75
|
}
|
|
76
|
+
if (selected.length === 0)
|
|
77
|
+
return emptyReport();
|
|
49
78
|
// Under --changed-only, restrict the SCANNED files to the changed set too (not
|
|
50
79
|
// just rule selection). Per-file regex findings have no cross-file dependency,
|
|
51
80
|
// so a pre-existing violation in a file the diff never touched is out of scope
|
|
52
81
|
// — this mirrors how `check boundaries`/wiring restrict to the changeset and
|
|
53
82
|
// stops the gate failing RED on untouched legacy debt.
|
|
54
83
|
const changedSet = options.changedOnly ? new Set(options.changedFiles ?? []) : undefined;
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
84
|
+
const excludeDirs = new Set(options.excludeDirs ?? []);
|
|
85
|
+
// One POSITIVE walk over the union; each rule selects its own units from it
|
|
86
|
+
// (`globListSelects`), so rule A's `!x` never removes x from rule B.
|
|
87
|
+
const allGlobs = [...new Set(selected.filter((r) => !malformed.has(r)).flatMap((r) => [...globsFor(r)]))];
|
|
88
|
+
const matched = readMatchingFiles(projectRoot, allGlobs, excludeDirs);
|
|
89
|
+
const cache = matched.files;
|
|
90
|
+
// The walk holds every file any selected rule's inclusion globs matched —
|
|
91
|
+
// the positive set — so a glob absent from it matched nothing.
|
|
92
|
+
const walked = [...cache.keys()];
|
|
93
|
+
const unitsByRule = new Map();
|
|
94
|
+
const unitsFor = (rule) => {
|
|
95
|
+
const memo = unitsByRule.get(rule);
|
|
96
|
+
if (memo !== undefined)
|
|
97
|
+
return memo;
|
|
98
|
+
const units = resolveUnits(rule);
|
|
99
|
+
unitsByRule.set(rule, units);
|
|
100
|
+
return units;
|
|
101
|
+
};
|
|
102
|
+
// What each rule's walk covered: the matched files in its scope (the
|
|
103
|
+
// changeset, under --changed-only) that were read, and the ones the reader
|
|
104
|
+
// reported UNREAD (over the read cap, or unreadable). The reader's own list
|
|
105
|
+
// is the one answer to "was this matched path left unexamined?". Nothing
|
|
106
|
+
// here re-derives the walk's skip rules or re-stats a path: a path the walk
|
|
107
|
+
// never reaches is neither read nor unread, which is exactly narrowing.
|
|
108
|
+
const scopeByRule = new Map();
|
|
109
|
+
const scopeFor = (rule) => {
|
|
110
|
+
const memo = scopeByRule.get(rule);
|
|
111
|
+
if (memo !== undefined)
|
|
112
|
+
return memo;
|
|
113
|
+
const raw = readScopeOf(matched, globsFor(rule), changedSet);
|
|
114
|
+
// An `exemptFiles` file's hits can only ever be suppressions, so leaving
|
|
115
|
+
// it unread cannot change the verdict: an explicit exemption is narrowing,
|
|
116
|
+
// not a gap (the keystone contract). Without this, a large exempted
|
|
117
|
+
// vendored or generated file made the rule NOT VERIFIED (2) for good, with
|
|
118
|
+
// no valve (`--allow-empty` never accepts a partial scope).
|
|
119
|
+
const exempt = rule.exemptFiles && rule.exemptFiles.length > 0 ? rule.exemptFiles : undefined;
|
|
120
|
+
const scope = exempt === undefined
|
|
121
|
+
? raw
|
|
122
|
+
: { read: raw.read, unread: raw.unread.filter((u) => !unreadEntryWhollyMatches(u, exempt)) };
|
|
123
|
+
scopeByRule.set(rule, scope);
|
|
124
|
+
return scope;
|
|
125
|
+
};
|
|
126
|
+
// Per-glob units of the author's OWN `files[]`, off the walk this run already
|
|
127
|
+
// did, settled with the rule's `expectEmpty` markers (round 13): core's one
|
|
128
|
+
// authority, `settleUnitLiveness`, over THE gate-plane glob predicate
|
|
129
|
+
// (`globListLivenessInput`) — the same input `gates coverage` builds, so the
|
|
130
|
+
// two cannot disagree about one glob. The one dead-unit decision
|
|
131
|
+
// (`globListUnits`) judges a negation by what it EXCLUDES, and a glob that
|
|
132
|
+
// reaches an UNREAD file (or beneath an unlistable directory) the list keeps
|
|
133
|
+
// in scope is never dead. A surface's default globs are not a selector
|
|
134
|
+
// anyone wrote, so they are not judged.
|
|
135
|
+
const livenessByRule = new Map();
|
|
136
|
+
const livenessFor = (rule) => {
|
|
137
|
+
if (livenessByRule.has(rule))
|
|
138
|
+
return livenessByRule.get(rule);
|
|
139
|
+
const own = rule.files;
|
|
140
|
+
let value;
|
|
141
|
+
if (own !== undefined && own.length > 0 && !malformed.has(rule)) {
|
|
142
|
+
const units = globListUnits(walked, matched.unread, own);
|
|
143
|
+
const liveness = settleUnitLiveness(globListLivenessInput({
|
|
144
|
+
subject: rule.id,
|
|
145
|
+
lists: [{ list: 'files', globs: own, units }],
|
|
146
|
+
marks: rule.expectEmptyUnits ?? [],
|
|
147
|
+
}));
|
|
148
|
+
value = { units, liveness };
|
|
149
|
+
}
|
|
150
|
+
livenessByRule.set(rule, value);
|
|
151
|
+
return value;
|
|
152
|
+
};
|
|
153
|
+
// --changed-only SELECTS a rule because a changed PATH matches its globs,
|
|
154
|
+
// but the change may still put nothing in the rule's scope: the path was
|
|
155
|
+
// deleted, or it was read and holds nothing on the rule's surface (a `.ts`
|
|
156
|
+
// with no inline `template:` under a template rule), or it lies where no
|
|
157
|
+
// policy walk goes. That is narrowing — the change cannot have introduced a
|
|
158
|
+
// finding there — so the rule is narrowed out exactly like an unselected
|
|
159
|
+
// one, never reported as a rule that "matched nothing": `failOnEmpty` turns
|
|
160
|
+
// that into a FAILURE, so a pure deletion failed `policy-lint --changed-only`
|
|
161
|
+
// and, once `finish` / `shrk gate` stopped masking a selected rule that
|
|
162
|
+
// examined nothing, would have failed them too. A matched path left UNREAD
|
|
163
|
+
// keeps the rule in scope: PARTIAL, naming the file, never narrowed away.
|
|
164
|
+
const inScope = changedSet
|
|
165
|
+
? selected.filter((rule) => malformed.has(rule) || unitsFor(rule).length > 0 || readScopeHasUnread(scopeFor(rule)))
|
|
166
|
+
: selected;
|
|
167
|
+
if (inScope.length === 0)
|
|
168
|
+
return emptyReport();
|
|
169
|
+
const report = evaluatePolicy(inScope, unitsFor, scopeFor, livenessFor);
|
|
170
|
+
// Each rule's units, reported: `evaluatePolicy` emits one result per rule, in
|
|
171
|
+
// order, so the index is the rule. Dead units are the UNMARKED dead ones
|
|
172
|
+
// only — a marked glob is intended-empty (accepted, on `unitAcceptance`) or
|
|
173
|
+
// went-live — and every non-live unit rides along for `--fail-on-dead-units`.
|
|
174
|
+
const skippedIds = new Set(report.skipped.map((s) => s.ruleId));
|
|
175
|
+
const ruleResults = report.rules.map((r, i) => {
|
|
176
|
+
const rule = inScope[i];
|
|
177
|
+
const lv = rule !== undefined ? livenessFor(rule) : undefined;
|
|
178
|
+
if (lv === undefined)
|
|
179
|
+
return r;
|
|
180
|
+
const settled = lv.liveness;
|
|
181
|
+
const deadSet = new Set(settled.dead.map((u) => u.unit));
|
|
182
|
+
const nonLive = settled.units.filter((u) => u.state !== UnitLivenessState.Live);
|
|
183
|
+
const listed = settled.dead.length + settled.intendedEmpty.length + settled.wentLive.length > 0;
|
|
184
|
+
// A rule its OWN negations emptied is not a stale selector: every file its
|
|
185
|
+
// inclusion globs select was excluded as written.
|
|
186
|
+
const emptied = lv.units.allExcluded && skippedIds.has(r.ruleId);
|
|
187
|
+
return {
|
|
188
|
+
...r,
|
|
189
|
+
...(deadSet.size > 0
|
|
190
|
+
? {
|
|
191
|
+
deadGlobs: lv.units.dead.filter((d) => deadSet.has(d.glob)).map((d) => d.glob),
|
|
192
|
+
deadGlobUnits: lv.units.dead.filter((d) => deadSet.has(d.glob)),
|
|
193
|
+
}
|
|
194
|
+
: {}),
|
|
195
|
+
...(lv.units.negations.length > 0 ? { negations: lv.units.negations } : {}),
|
|
196
|
+
...(listed ? { units: unitStateLists(settled) } : {}),
|
|
197
|
+
...(nonLive.length > 0 ? { unitLiveness: nonLive } : {}),
|
|
198
|
+
...(emptied
|
|
199
|
+
? { excludedByNegations: lv.units.negations, coverage: { ...r.coverage, reason: allExcludedCause(lv.units.negations) } }
|
|
200
|
+
: {}),
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
return { ...report, rules: ruleResults };
|
|
204
|
+
function resolveUnits(rule) {
|
|
205
|
+
if (malformed.has(rule))
|
|
206
|
+
return [];
|
|
58
207
|
const globs = globsFor(rule);
|
|
59
208
|
// `exemptFiles` never removes the file from the scan — it MARKS it, so the
|
|
60
209
|
// hits it would have produced are reported as suppressed rather than
|
|
@@ -65,9 +214,10 @@ export function runPolicyLint(projectRoot, rules, options = {}) {
|
|
|
65
214
|
for (const [path, content] of cache) {
|
|
66
215
|
if (changedSet && !changedSet.has(path))
|
|
67
216
|
continue;
|
|
68
|
-
|
|
217
|
+
// `files` `!` EXCLUDES (the file is out of scope); `exemptFiles` MARKS.
|
|
218
|
+
if (!globListSelects(path, globs))
|
|
69
219
|
continue;
|
|
70
|
-
const exemptFile = exempt !== undefined &&
|
|
220
|
+
const exemptFile = exempt !== undefined && globListSelects(path, exempt);
|
|
71
221
|
const ext = nodePath.extname(path).toLowerCase();
|
|
72
222
|
if (rule.surface === 'template' && SOURCE_EXT.has(ext)) {
|
|
73
223
|
for (const tpl of extractInlineTemplates(content)) {
|
|
@@ -86,5 +236,5 @@ export function runPolicyLint(projectRoot, rules, options = {}) {
|
|
|
86
236
|
}
|
|
87
237
|
}
|
|
88
238
|
return units;
|
|
89
|
-
}
|
|
239
|
+
}
|
|
90
240
|
}
|