@shrkcrft/boundaries 0.1.0-alpha.28 → 0.1.0-alpha.30
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.map +1 -1
- package/dist/baseline/compute-baseline.js +2 -1
- package/dist/extract/extract-tokens.d.ts +19 -1
- package/dist/extract/extract-tokens.d.ts.map +1 -1
- package/dist/extract/extract-tokens.js +53 -1
- package/dist/extract/import-edges.d.ts +49 -0
- package/dist/extract/import-edges.d.ts.map +1 -0
- package/dist/extract/import-edges.js +157 -0
- package/dist/extract/inspect-source.d.ts +6 -0
- package/dist/extract/inspect-source.d.ts.map +1 -1
- package/dist/extract/inspect-source.js +6 -1
- package/dist/extract/parse-imports.d.ts +58 -0
- package/dist/extract/parse-imports.d.ts.map +1 -0
- package/dist/extract/parse-imports.js +155 -0
- package/dist/generated/check-provenance.d.ts +20 -2
- package/dist/generated/check-provenance.d.ts.map +1 -1
- package/dist/generated/check-provenance.js +24 -5
- package/dist/generated/scan-generated.d.ts +49 -3
- package/dist/generated/scan-generated.d.ts.map +1 -1
- package/dist/generated/scan-generated.js +90 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/policy/evaluate-policy.d.ts +1 -1
- package/dist/policy/evaluate-policy.d.ts.map +1 -1
- package/dist/policy/evaluate-policy.js +2 -1
- package/dist/util/walk-files.d.ts +21 -2
- package/dist/util/walk-files.d.ts.map +1 -1
- package/dist/util/walk-files.js +85 -4
- package/dist/wiring/evaluate-wiring.d.ts +10 -3
- package/dist/wiring/evaluate-wiring.d.ts.map +1 -1
- package/dist/wiring/evaluate-wiring.js +12 -7
- package/dist/wiring/explain-wiring.d.ts +7 -9
- package/dist/wiring/explain-wiring.d.ts.map +1 -1
- package/dist/wiring/explain-wiring.js +18 -0
- package/dist/wiring/plan-wiring-fix.d.ts +75 -0
- package/dist/wiring/plan-wiring-fix.d.ts.map +1 -0
- package/dist/wiring/plan-wiring-fix.js +302 -0
- package/dist/wiring/registration-graph.d.ts +1 -1
- package/dist/wiring/registration-graph.d.ts.map +1 -1
- package/dist/wiring/registration-graph.js +8 -3
- package/dist/wiring/registry-query.d.ts +1 -1
- package/dist/wiring/registry-query.d.ts.map +1 -1
- package/dist/wiring/registry-query.js +7 -3
- package/dist/wiring/scan-wiring-files.d.ts +1 -1
- package/dist/wiring/scan-wiring-files.d.ts.map +1 -1
- package/dist/wiring/scan-wiring-files.js +5 -1
- package/dist/wiring/sink-imports.d.ts +84 -0
- package/dist/wiring/sink-imports.d.ts.map +1 -0
- package/dist/wiring/sink-imports.js +115 -0
- package/package.json +2 -2
|
@@ -6,7 +6,11 @@ export type ProvenanceFindingKind =
|
|
|
6
6
|
/** A file OUTSIDE the glob carries the header — a hand-written file mislabeled. */
|
|
7
7
|
| 'mislabeled'
|
|
8
8
|
/** Advisory: the header does not name how to regenerate the file. */
|
|
9
|
-
| 'no-regen-pointer'
|
|
9
|
+
| 'no-regen-pointer'
|
|
10
|
+
/** In the tree, owned by no writer and not blessed as hand-maintained. */
|
|
11
|
+
| 'unclassified'
|
|
12
|
+
/** A `handMaintained` bless whose file no longer exists. */
|
|
13
|
+
| 'stale-hand-maintained';
|
|
10
14
|
export interface IProvenanceFinding {
|
|
11
15
|
readonly ruleId: string;
|
|
12
16
|
readonly file: string;
|
|
@@ -20,6 +24,20 @@ export interface IProvenanceFinding {
|
|
|
20
24
|
* deterministic — never a whole-tree read of every file type.
|
|
21
25
|
*/
|
|
22
26
|
export declare function deriveOutsideGlobs(generatedGlob: readonly string[]): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Tree-classification results a mixed-tree rule produces alongside its files.
|
|
29
|
+
*
|
|
30
|
+
* Passed in rather than recomputed so the classification a caller already did
|
|
31
|
+
* (see `scanGeneratedFiles`) is the same one reported — two implementations of
|
|
32
|
+
* "which files does this rule own?" is precisely the drift this plane exists
|
|
33
|
+
* to catch.
|
|
34
|
+
*/
|
|
35
|
+
export interface IClassificationFindings {
|
|
36
|
+
/** Files under the tree owned by no writer and not blessed. */
|
|
37
|
+
readonly unclassified?: readonly string[];
|
|
38
|
+
/** `handMaintained` patterns currently matching no file. */
|
|
39
|
+
readonly staleHandMaintained?: readonly string[];
|
|
40
|
+
}
|
|
23
41
|
/**
|
|
24
42
|
* Check the "this file is generated" header contract.
|
|
25
43
|
*
|
|
@@ -30,7 +48,7 @@ export declare function deriveOutsideGlobs(generatedGlob: readonly string[]): st
|
|
|
30
48
|
* `outside` should contain candidate files NOT in `generatedGlob`; pass an empty
|
|
31
49
|
* map when `forbidOutside` is off.
|
|
32
50
|
*/
|
|
33
|
-
export declare function checkProvenanceHeaders(rule: IGeneratedArtifactRule, generated: ReadonlyMap<string, string>, outside: ReadonlyMap<string, string
|
|
51
|
+
export declare function checkProvenanceHeaders(rule: IGeneratedArtifactRule, generated: ReadonlyMap<string, string>, outside: ReadonlyMap<string, string>, classification?: IClassificationFindings): {
|
|
34
52
|
findings: readonly IProvenanceFinding[];
|
|
35
53
|
error?: string;
|
|
36
54
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-provenance.d.ts","sourceRoot":"","sources":["../../src/generated/check-provenance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAG7D,0CAA0C;AAC1C,MAAM,MAAM,qBAAqB;AAC/B,qEAAqE;AACnE,gBAAgB;AAClB,mFAAmF;GACjF,YAAY;AACd,qEAAqE;GACnE,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"check-provenance.d.ts","sourceRoot":"","sources":["../../src/generated/check-provenance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAG7D,0CAA0C;AAC1C,MAAM,MAAM,qBAAqB;AAC/B,qEAAqE;AACnE,gBAAgB;AAClB,mFAAmF;GACjF,YAAY;AACd,qEAAqE;GACnE,kBAAkB;AACpB,0EAA0E;GACxE,cAAc;AAChB,4DAA4D;GAC1D,uBAAuB,CAAC;AAE5B,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAQD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAO7E;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACtC,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,4DAA4D;IAC5D,QAAQ,CAAC,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClD;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,sBAAsB,EAC5B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,cAAc,GAAE,uBAA4B,GAC3C;IAAE,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAgF7D"}
|
|
@@ -28,16 +28,35 @@ export function deriveOutsideGlobs(generatedGlob) {
|
|
|
28
28
|
* `outside` should contain candidate files NOT in `generatedGlob`; pass an empty
|
|
29
29
|
* map when `forbidOutside` is off.
|
|
30
30
|
*/
|
|
31
|
-
export function checkProvenanceHeaders(rule, generated, outside) {
|
|
31
|
+
export function checkProvenanceHeaders(rule, generated, outside, classification = {}) {
|
|
32
|
+
const severity = rule.severity ?? 'error';
|
|
33
|
+
// Classification findings do not depend on the header contract — a mixed tree
|
|
34
|
+
// must be fully accounted for whether or not the rule asserts headers, so
|
|
35
|
+
// these are produced before the early return below.
|
|
36
|
+
const classFindings = [
|
|
37
|
+
...(classification.unclassified ?? []).map((file) => ({
|
|
38
|
+
ruleId: rule.id,
|
|
39
|
+
file,
|
|
40
|
+
kind: 'unclassified',
|
|
41
|
+
severity,
|
|
42
|
+
message: 'in the generated tree but owned by no `sources[]` glob and not listed in `handMaintained` — classify it',
|
|
43
|
+
})),
|
|
44
|
+
...(classification.staleHandMaintained ?? []).map((pattern) => ({
|
|
45
|
+
ruleId: rule.id,
|
|
46
|
+
file: pattern,
|
|
47
|
+
kind: 'stale-hand-maintained',
|
|
48
|
+
severity: 'warning',
|
|
49
|
+
message: '`handMaintained` entry matches no file — the blessed file was renamed or deleted',
|
|
50
|
+
})),
|
|
51
|
+
];
|
|
32
52
|
const header = rule.provenanceHeader;
|
|
33
53
|
if (!header)
|
|
34
|
-
return { findings:
|
|
54
|
+
return { findings: classFindings };
|
|
35
55
|
const { re, error } = safeCompile(header.mustMatch, header.flags);
|
|
36
56
|
if (error || !re)
|
|
37
|
-
return { findings:
|
|
38
|
-
const severity = rule.severity ?? 'error';
|
|
57
|
+
return { findings: classFindings, error: `provenanceHeader ${error}` };
|
|
39
58
|
const within = header.withinLines ?? 10;
|
|
40
|
-
const findings = [];
|
|
59
|
+
const findings = [...classFindings];
|
|
41
60
|
const test = (content) => {
|
|
42
61
|
re.lastIndex = 0;
|
|
43
62
|
return re.test(headOf(content, within));
|
|
@@ -1,17 +1,63 @@
|
|
|
1
1
|
import type { IGeneratedArtifactRule } from '@shrkcrft/core';
|
|
2
|
+
/** One writer's slice of a mixed generated tree. */
|
|
3
|
+
export interface IGeneratedSourceSlice {
|
|
4
|
+
/** Label for output — the writer's `id`, or `source[<index>]`. */
|
|
5
|
+
readonly label: string;
|
|
6
|
+
/** The regen command that owns this slice. */
|
|
7
|
+
readonly regen: string;
|
|
8
|
+
/** Globs selecting the committed files this writer owns. */
|
|
9
|
+
readonly glob: readonly string[];
|
|
10
|
+
/** Committed files matching this writer's glob (path → content). */
|
|
11
|
+
readonly files: ReadonlyMap<string, string>;
|
|
12
|
+
readonly timeoutMs?: number;
|
|
13
|
+
}
|
|
2
14
|
/** The committed generated files plus the mislabel-candidate set. */
|
|
3
15
|
export interface IGeneratedScan {
|
|
4
16
|
/** Files matching `generatedGlob` (project-relative path → content). */
|
|
5
17
|
readonly generated: ReadonlyMap<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* The subset of {@link generated} that is checkable — i.e. everything except
|
|
20
|
+
* the hand-maintained blesses. Header and byte checks run over this.
|
|
21
|
+
*/
|
|
22
|
+
readonly checkable: ReadonlyMap<string, string>;
|
|
23
|
+
/** Files excluded by `handMaintained` or by the in-file marker, sorted. */
|
|
24
|
+
readonly handMaintained: readonly string[];
|
|
25
|
+
/** The subset of {@link handMaintained} that declared itself via the marker. */
|
|
26
|
+
readonly markedHandMaintained: readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* `handMaintained` patterns that currently match NO file. A bless whose file
|
|
29
|
+
* was renamed or deleted is a stale exemption: it silently widens nothing
|
|
30
|
+
* today, but it also documents a file that no longer exists, and the next
|
|
31
|
+
* file to take that name inherits an exemption nobody reviewed.
|
|
32
|
+
*/
|
|
33
|
+
readonly staleHandMaintained: readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* Files under `generatedGlob` owned by NO writer and not blessed. Only
|
|
36
|
+
* populated for multi-writer rules, where the writers partition the tree —
|
|
37
|
+
* a single-writer rule owns everything its glob matches by definition.
|
|
38
|
+
*/
|
|
39
|
+
readonly unclassified: readonly string[];
|
|
40
|
+
/** Per-writer slices for a multi-writer rule; empty for a single-writer one. */
|
|
41
|
+
readonly slices: readonly IGeneratedSourceSlice[];
|
|
6
42
|
/** Candidates for the `forbidOutside` mislabel check (empty when it is off). */
|
|
7
43
|
readonly outside: ReadonlyMap<string, string>;
|
|
8
44
|
/** Globs actually used for the outside scan (derived or configured). */
|
|
9
45
|
readonly outsideGlobs: readonly string[];
|
|
10
46
|
}
|
|
11
47
|
/**
|
|
12
|
-
* Read the committed side of a generated-artifact rule.
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
48
|
+
* Read + CLASSIFY the committed side of a generated-artifact rule.
|
|
49
|
+
*
|
|
50
|
+
* Classification is the part that makes this usable on a real tree. A blanket
|
|
51
|
+
* "everything under this glob is generated by that one command" holds only in a
|
|
52
|
+
* demo; a real `generated/` dir mixes several writers' output with files that
|
|
53
|
+
* are still hand-maintained. Partitioning the tree first means each writer is
|
|
54
|
+
* compared against its OWN slice (no cross-writer `only-committed` noise), the
|
|
55
|
+
* hand-written files are exempted from the header contract instead of failing
|
|
56
|
+
* it, and anything belonging to neither is surfaced as `unclassified` rather
|
|
57
|
+
* than silently assumed.
|
|
58
|
+
*
|
|
59
|
+
* Pure filesystem reads — the regen commands (if any) are orchestrated by the
|
|
60
|
+
* caller, which is also where the local-config-only trust decision is enforced.
|
|
15
61
|
*/
|
|
16
62
|
export declare function scanGeneratedFiles(projectRoot: string, rule: IGeneratedArtifactRule, excludeDirs?: readonly string[]): IGeneratedScan;
|
|
17
63
|
//# sourceMappingURL=scan-generated.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan-generated.d.ts","sourceRoot":"","sources":["../../src/generated/scan-generated.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"scan-generated.d.ts","sourceRoot":"","sources":["../../src/generated/scan-generated.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAM7D,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,qEAAqE;AACrE,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,gFAAgF;IAChF,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAClD,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,sBAAsB,EAC5B,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,cAAc,CAiGhB"}
|
|
@@ -1,17 +1,92 @@
|
|
|
1
1
|
import { matchesAny } from "../scan/glob.js";
|
|
2
|
+
import { safeCompile } from "../util/safe-regex.js";
|
|
2
3
|
import { readMatchingFiles } from "../util/walk-files.js";
|
|
3
4
|
import { deriveOutsideGlobs } from "./check-provenance.js";
|
|
4
5
|
/**
|
|
5
|
-
* Read the committed side of a generated-artifact rule.
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
6
|
+
* Read + CLASSIFY the committed side of a generated-artifact rule.
|
|
7
|
+
*
|
|
8
|
+
* Classification is the part that makes this usable on a real tree. A blanket
|
|
9
|
+
* "everything under this glob is generated by that one command" holds only in a
|
|
10
|
+
* demo; a real `generated/` dir mixes several writers' output with files that
|
|
11
|
+
* are still hand-maintained. Partitioning the tree first means each writer is
|
|
12
|
+
* compared against its OWN slice (no cross-writer `only-committed` noise), the
|
|
13
|
+
* hand-written files are exempted from the header contract instead of failing
|
|
14
|
+
* it, and anything belonging to neither is surfaced as `unclassified` rather
|
|
15
|
+
* than silently assumed.
|
|
16
|
+
*
|
|
17
|
+
* Pure filesystem reads — the regen commands (if any) are orchestrated by the
|
|
18
|
+
* caller, which is also where the local-config-only trust decision is enforced.
|
|
8
19
|
*/
|
|
9
20
|
export function scanGeneratedFiles(projectRoot, rule, excludeDirs = []) {
|
|
10
21
|
const exclude = new Set(excludeDirs);
|
|
11
22
|
const generated = readMatchingFiles(projectRoot, rule.generatedGlob, exclude);
|
|
23
|
+
const blessPatterns = rule.handMaintained ?? [];
|
|
24
|
+
// The in-file marker is read from the same head window the header contract
|
|
25
|
+
// uses, so "the top of the file" means one thing for both classifications.
|
|
26
|
+
const markerWindow = rule.provenanceHeader?.withinLines ?? 10;
|
|
27
|
+
const marker = rule.handMaintainedMarker
|
|
28
|
+
? safeCompile(rule.handMaintainedMarker, rule.handMaintainedMarkerFlags).re
|
|
29
|
+
: undefined;
|
|
30
|
+
const carriesMarker = (content) => {
|
|
31
|
+
if (!marker)
|
|
32
|
+
return false;
|
|
33
|
+
marker.lastIndex = 0;
|
|
34
|
+
return marker.test(content.split('\n').slice(0, Math.max(1, markerWindow)).join('\n'));
|
|
35
|
+
};
|
|
36
|
+
const handMaintained = [];
|
|
37
|
+
const markedHandMaintained = [];
|
|
38
|
+
const checkable = new Map();
|
|
39
|
+
for (const [path, content] of generated) {
|
|
40
|
+
if (blessPatterns.length > 0 && matchesAny(path, blessPatterns)) {
|
|
41
|
+
handMaintained.push(path);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (carriesMarker(content)) {
|
|
45
|
+
handMaintained.push(path);
|
|
46
|
+
markedHandMaintained.push(path);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
checkable.set(path, content);
|
|
50
|
+
}
|
|
51
|
+
handMaintained.sort();
|
|
52
|
+
markedHandMaintained.sort();
|
|
53
|
+
// A bless is pinned to a NAMED file (the schema forbids a wildcard basename),
|
|
54
|
+
// so one that matches nothing points at a file that has been renamed away.
|
|
55
|
+
const staleHandMaintained = blessPatterns
|
|
56
|
+
.filter((pattern) => ![...generated.keys()].some((p) => matchesAny(p, [pattern])))
|
|
57
|
+
.sort();
|
|
58
|
+
const slices = (rule.sources ?? []).map((src, i) => {
|
|
59
|
+
const files = new Map();
|
|
60
|
+
for (const [path, content] of checkable) {
|
|
61
|
+
if (matchesAny(path, src.glob))
|
|
62
|
+
files.set(path, content);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
label: src.id ?? `source[${i}]`,
|
|
66
|
+
regen: src.regen,
|
|
67
|
+
glob: src.glob,
|
|
68
|
+
files,
|
|
69
|
+
...(src.timeoutMs !== undefined ? { timeoutMs: src.timeoutMs } : {}),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
// Only a multi-writer rule PARTITIONS the tree; a single-writer rule owns
|
|
73
|
+
// everything its glob matched, so nothing there can be unclassified.
|
|
74
|
+
const unclassified = slices.length === 0
|
|
75
|
+
? []
|
|
76
|
+
: [...checkable.keys()].filter((p) => !slices.some((s) => matchesAny(p, s.glob))).sort();
|
|
12
77
|
const wantOutside = rule.provenanceHeader?.forbidOutside === true;
|
|
13
78
|
if (!wantOutside) {
|
|
14
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
generated,
|
|
81
|
+
checkable,
|
|
82
|
+
handMaintained,
|
|
83
|
+
markedHandMaintained,
|
|
84
|
+
staleHandMaintained,
|
|
85
|
+
unclassified,
|
|
86
|
+
slices,
|
|
87
|
+
outside: new Map(),
|
|
88
|
+
outsideGlobs: [],
|
|
89
|
+
};
|
|
15
90
|
}
|
|
16
91
|
const outsideGlobs = rule.provenanceHeader?.outsideGlob && rule.provenanceHeader.outsideGlob.length > 0
|
|
17
92
|
? [...rule.provenanceHeader.outsideGlob]
|
|
@@ -23,5 +98,15 @@ export function scanGeneratedFiles(projectRoot, rule, excludeDirs = []) {
|
|
|
23
98
|
continue;
|
|
24
99
|
outside.set(path, content);
|
|
25
100
|
}
|
|
26
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
generated,
|
|
103
|
+
checkable,
|
|
104
|
+
handMaintained,
|
|
105
|
+
markedHandMaintained,
|
|
106
|
+
staleHandMaintained,
|
|
107
|
+
unclassified,
|
|
108
|
+
slices,
|
|
109
|
+
outside,
|
|
110
|
+
outsideGlobs,
|
|
111
|
+
};
|
|
27
112
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,11 +11,15 @@ export * from './wiring/explain-wiring.js';
|
|
|
11
11
|
export * from './wiring/registry-query.js';
|
|
12
12
|
export * from './wiring/registration-graph.js';
|
|
13
13
|
export * from './wiring/trace-literal.js';
|
|
14
|
+
export * from './wiring/plan-wiring-fix.js';
|
|
15
|
+
export * from './wiring/sink-imports.js';
|
|
14
16
|
export * from './policy/extract-templates.js';
|
|
15
17
|
export * from './policy/evaluate-policy.js';
|
|
16
18
|
export * from './policy/run-policy.js';
|
|
17
19
|
export * from './extract/scan-literals.js';
|
|
18
20
|
export * from './extract/extract-tokens.js';
|
|
21
|
+
export * from './extract/parse-imports.js';
|
|
22
|
+
export * from './extract/import-edges.js';
|
|
19
23
|
export * from './extract/code-zones.js';
|
|
20
24
|
export * from './extract/inspect-source.js';
|
|
21
25
|
export * from './baseline/canonicalize.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mCAAmC,CAAC;AAClD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mCAAmC,CAAC;AAClD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iCAAiC,CAAC;AAChD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -11,11 +11,15 @@ export * from "./wiring/explain-wiring.js";
|
|
|
11
11
|
export * from "./wiring/registry-query.js";
|
|
12
12
|
export * from "./wiring/registration-graph.js";
|
|
13
13
|
export * from "./wiring/trace-literal.js";
|
|
14
|
+
export * from "./wiring/plan-wiring-fix.js";
|
|
15
|
+
export * from "./wiring/sink-imports.js";
|
|
14
16
|
export * from "./policy/extract-templates.js";
|
|
15
17
|
export * from "./policy/evaluate-policy.js";
|
|
16
18
|
export * from "./policy/run-policy.js";
|
|
17
19
|
export * from "./extract/scan-literals.js";
|
|
18
20
|
export * from "./extract/extract-tokens.js";
|
|
21
|
+
export * from "./extract/parse-imports.js";
|
|
22
|
+
export * from "./extract/import-edges.js";
|
|
19
23
|
export * from "./extract/code-zones.js";
|
|
20
24
|
export * from "./extract/inspect-source.js";
|
|
21
25
|
export * from "./baseline/canonicalize.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IPolicyRule, type PolicySurface } from '@shrkcrft/core';
|
|
2
2
|
export declare const POLICY_LINT_SCHEMA: "sharkcraft.policy-lint/v1";
|
|
3
3
|
/**
|
|
4
4
|
* A chunk of content to scan for one rule. For whole files `baseLine` is 1; for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { failsWhenEmpty } from '@shrkcrft/core';
|
|
1
2
|
import { lexCodeZones, zoneAt } from "../extract/code-zones.js";
|
|
2
3
|
import { safeCompile } from "../util/safe-regex.js";
|
|
3
4
|
export const POLICY_LINT_SCHEMA = 'sharkcraft.policy-lint/v1';
|
|
@@ -152,7 +153,7 @@ export function evaluatePolicy(rules, resolve) {
|
|
|
152
153
|
continue;
|
|
153
154
|
}
|
|
154
155
|
if (units.length === 0) {
|
|
155
|
-
const failed = rule
|
|
156
|
+
const failed = failsWhenEmpty(rule);
|
|
156
157
|
skipped.push({
|
|
157
158
|
ruleId: rule.id,
|
|
158
159
|
reason: '0 content units matched the rule globs',
|
|
@@ -7,7 +7,26 @@ export declare const MAX_SCAN_FILE_BYTES = 1000000;
|
|
|
7
7
|
* `excludeDirs` is a set of project-relative POSIX directory paths to prune
|
|
8
8
|
* entirely (e.g. the SharkCraft asset/config dir).
|
|
9
9
|
*/
|
|
10
|
-
export declare function walkMatching(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>): string[];
|
|
10
|
+
export declare function walkMatching(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Run `fn` with the read memo enabled, then clear it.
|
|
13
|
+
*
|
|
14
|
+
* ONLY wrap a scan that performs no writes and spawns no commands for its
|
|
15
|
+
* duration — within such a window the tree cannot change beneath the memo, so
|
|
16
|
+
* reuse is exact rather than merely probable.
|
|
17
|
+
*/
|
|
18
|
+
export declare function withFileReadCache<T>(fn: () => T): T;
|
|
19
|
+
/** Drop every memoized read. For tests, and for a caller that has just written. */
|
|
20
|
+
export declare function clearFileReadCache(): void;
|
|
21
|
+
/**
|
|
22
|
+
* The dot-directory segments a glob set explicitly names.
|
|
23
|
+
*
|
|
24
|
+
* `.claude/skills/**` asks for `.claude`; `docs/**` asks for nothing. Deriving
|
|
25
|
+
* the allowlist from the globs themselves means a rule gets exactly the
|
|
26
|
+
* directories it named and no others — a blanket "scan dot-dirs" switch would
|
|
27
|
+
* wander into `.venv` and `.yarn` the moment someone wrote a recursive glob.
|
|
28
|
+
*/
|
|
29
|
+
export declare function dotDirsNamedBy(globs: readonly string[]): Set<string>;
|
|
11
30
|
/** Walk + read every file matching `globs`, skipping oversized/unreadable files. */
|
|
12
|
-
export declare function readMatchingFiles(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>): Map<string, string>;
|
|
31
|
+
export declare function readMatchingFiles(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>, allowDotDirs?: ReadonlySet<string>): Map<string, string>;
|
|
13
32
|
//# sourceMappingURL=walk-files.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walk-files.d.ts","sourceRoot":"","sources":["../../src/util/walk-files.ts"],"names":[],"mappings":"AAIA,+CAA+C;AAC/C,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAUxC,CAAC;AAEH,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,UAAY,CAAC;AAE7C;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,
|
|
1
|
+
{"version":3,"file":"walk-files.d.ts","sourceRoot":"","sources":["../../src/util/walk-files.ts"],"names":[],"mappings":"AAIA,+CAA+C;AAC/C,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAUxC,CAAC;AAEH,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,UAAY,CAAC;AAE7C;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,EAC5C,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,MAAM,EAAE,CAgCV;AAuBD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CASnD;AAED,mFAAmF;AACnF,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAGD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAUpE;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,EAC5C,YAAY,GAAE,WAAW,CAAC,MAAM,CAAa,GAC5C,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAgCrB"}
|
package/dist/util/walk-files.js
CHANGED
|
@@ -20,7 +20,7 @@ export const MAX_SCAN_FILE_BYTES = 1_000_000;
|
|
|
20
20
|
* `excludeDirs` is a set of project-relative POSIX directory paths to prune
|
|
21
21
|
* entirely (e.g. the SharkCraft asset/config dir).
|
|
22
22
|
*/
|
|
23
|
-
export function walkMatching(root, globs, excludeDirs = new Set()) {
|
|
23
|
+
export function walkMatching(root, globs, excludeDirs = new Set(), allowDotDirs = new Set()) {
|
|
24
24
|
const out = [];
|
|
25
25
|
const visit = (abs) => {
|
|
26
26
|
let entries;
|
|
@@ -39,7 +39,13 @@ export function walkMatching(root, globs, excludeDirs = new Set()) {
|
|
|
39
39
|
// so neither policy-lint nor wiring scans tooling/vendored sources.
|
|
40
40
|
// (Dirent.isDirectory() is false for symlinks, so symlinked dirs are
|
|
41
41
|
// never descended — no loop risk.)
|
|
42
|
-
|
|
42
|
+
// Dot-directories are vendored tooling by default (`.venv`, `.yarn`,
|
|
43
|
+
// `.gradle`). But a rule may legitimately target one — an agent skill
|
|
44
|
+
// file lives in `.claude/skills` — so a caller can name the dot-dirs
|
|
45
|
+
// its OWN globs ask for. `SKIP_DIRS` stays absolute either way.
|
|
46
|
+
if (SKIP_DIRS.has(e.name) || excludeDirs.has(rel))
|
|
47
|
+
continue;
|
|
48
|
+
if (e.name.startsWith('.') && !allowDotDirs.has(e.name))
|
|
43
49
|
continue;
|
|
44
50
|
visit(childAbs);
|
|
45
51
|
}
|
|
@@ -52,10 +58,83 @@ export function walkMatching(root, globs, excludeDirs = new Set()) {
|
|
|
52
58
|
visit(root);
|
|
53
59
|
return out;
|
|
54
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Memo of `readMatchingFiles`, keyed by (root, glob set, excludes).
|
|
63
|
+
*
|
|
64
|
+
* One coverage run resolves the SAME globs many times over: every rule on every
|
|
65
|
+
* plane walks the tree for its own sources, and a shared `$use` extractor is by
|
|
66
|
+
* construction read once per consumer. The walk + read is roughly half the cost
|
|
67
|
+
* of a wide extraction, so memoizing turns N walks into one.
|
|
68
|
+
*
|
|
69
|
+
* It is OFF by default and enabled only by {@link withFileReadCache}, around a
|
|
70
|
+
* scan that provably neither spawns nor writes. That restriction is not
|
|
71
|
+
* caution for its own sake — a global memo really does hand back a stale
|
|
72
|
+
* snapshot when the same process writes a file between two scans, and a trust
|
|
73
|
+
* tool answering "nothing drifted" from a stale read is worse than a slow one.
|
|
74
|
+
* Making the safe window explicit means the condition cannot be forgotten at a
|
|
75
|
+
* call site.
|
|
76
|
+
*/
|
|
77
|
+
const READ_MEMO = new Map();
|
|
78
|
+
/** True only inside {@link withFileReadCache}. */
|
|
79
|
+
let memoEnabled = false;
|
|
80
|
+
/**
|
|
81
|
+
* Run `fn` with the read memo enabled, then clear it.
|
|
82
|
+
*
|
|
83
|
+
* ONLY wrap a scan that performs no writes and spawns no commands for its
|
|
84
|
+
* duration — within such a window the tree cannot change beneath the memo, so
|
|
85
|
+
* reuse is exact rather than merely probable.
|
|
86
|
+
*/
|
|
87
|
+
export function withFileReadCache(fn) {
|
|
88
|
+
const previous = memoEnabled;
|
|
89
|
+
memoEnabled = true;
|
|
90
|
+
try {
|
|
91
|
+
return fn();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
memoEnabled = previous;
|
|
95
|
+
if (!previous)
|
|
96
|
+
READ_MEMO.clear();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Drop every memoized read. For tests, and for a caller that has just written. */
|
|
100
|
+
export function clearFileReadCache() {
|
|
101
|
+
READ_MEMO.clear();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The dot-directory segments a glob set explicitly names.
|
|
105
|
+
*
|
|
106
|
+
* `.claude/skills/**` asks for `.claude`; `docs/**` asks for nothing. Deriving
|
|
107
|
+
* the allowlist from the globs themselves means a rule gets exactly the
|
|
108
|
+
* directories it named and no others — a blanket "scan dot-dirs" switch would
|
|
109
|
+
* wander into `.venv` and `.yarn` the moment someone wrote a recursive glob.
|
|
110
|
+
*/
|
|
111
|
+
export function dotDirsNamedBy(globs) {
|
|
112
|
+
const out = new Set();
|
|
113
|
+
for (const glob of globs) {
|
|
114
|
+
for (const segment of glob.split('/')) {
|
|
115
|
+
if (segment.startsWith('.') && segment.length > 1 && !segment.includes('*') && !segment.includes('?')) {
|
|
116
|
+
out.add(segment);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
55
122
|
/** Walk + read every file matching `globs`, skipping oversized/unreadable files. */
|
|
56
|
-
export function readMatchingFiles(root, globs, excludeDirs = new Set()) {
|
|
123
|
+
export function readMatchingFiles(root, globs, excludeDirs = new Set(), allowDotDirs = new Set()) {
|
|
124
|
+
const key = memoEnabled
|
|
125
|
+
? `${root}\u0000${[...globs].sort().join('\u0001')}\u0000${[...excludeDirs].sort().join('\u0001')}` +
|
|
126
|
+
`\u0000${[...allowDotDirs].sort().join('\u0001')}`
|
|
127
|
+
: undefined;
|
|
128
|
+
if (key !== undefined) {
|
|
129
|
+
const hit = READ_MEMO.get(key);
|
|
130
|
+
// Hand back a COPY: callers routinely mutate the map they get (the wiring
|
|
131
|
+
// scan filters it, the generated scan partitions it), and a shared instance
|
|
132
|
+
// would let one rule's bookkeeping corrupt the next rule's inputs.
|
|
133
|
+
if (hit)
|
|
134
|
+
return new Map(hit);
|
|
135
|
+
}
|
|
57
136
|
const out = new Map();
|
|
58
|
-
for (const rel of walkMatching(root, globs, excludeDirs)) {
|
|
137
|
+
for (const rel of walkMatching(root, globs, excludeDirs, allowDotDirs)) {
|
|
59
138
|
const abs = nodePath.join(root, rel);
|
|
60
139
|
let size = -1;
|
|
61
140
|
try {
|
|
@@ -76,5 +155,7 @@ export function readMatchingFiles(root, globs, excludeDirs = new Set()) {
|
|
|
76
155
|
// unreadable — skip
|
|
77
156
|
}
|
|
78
157
|
}
|
|
158
|
+
if (key !== undefined)
|
|
159
|
+
READ_MEMO.set(key, new Map(out));
|
|
79
160
|
return out;
|
|
80
161
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type IWiringRule, type IWiringSource } from '@shrkcrft/core';
|
|
2
|
-
import { type IExtractFileEntry, type IExtractedSite } from '../extract/extract-tokens.js';
|
|
2
|
+
import { type IExtractContext, type IExtractFileEntry, type IExtractedSite } from '../extract/extract-tokens.js';
|
|
3
3
|
export declare const WIRING_SCHEMA: "sharkcraft.wiring/v1";
|
|
4
4
|
/** A file made available to the engine. */
|
|
5
5
|
export type IWiringFileEntry = IExtractFileEntry;
|
|
@@ -68,6 +68,13 @@ export interface IWiringRuleResult {
|
|
|
68
68
|
* silently reported as N unrelated violations.
|
|
69
69
|
*/
|
|
70
70
|
readonly emptySink?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* A diagnosis for a side that extracted nothing for a knowable reason — e.g.
|
|
73
|
+
* an `import-edges` sink targeting `to.files` against barrel imports. Carried
|
|
74
|
+
* so the empty-sink message can explain itself instead of sending the reader
|
|
75
|
+
* to check a glob that is fine.
|
|
76
|
+
*/
|
|
77
|
+
readonly sinkHint?: string;
|
|
71
78
|
/** Per-hop breakdown for a `chain` rule. */
|
|
72
79
|
readonly hops?: readonly IWiringHopResult[];
|
|
73
80
|
}
|
|
@@ -107,7 +114,7 @@ export declare function wiringGlobsOf(rule: IWiringRule): string[];
|
|
|
107
114
|
* extractor-backed baseline compute, so all three honour identical semantics. A
|
|
108
115
|
* misconfigured source returns an `error` and no sites (never throws).
|
|
109
116
|
*/
|
|
110
|
-
export declare function collectSourceSites(source: IWiringSource, files: readonly IWiringFileEntry[]): {
|
|
117
|
+
export declare function collectSourceSites(source: IWiringSource, files: readonly IWiringFileEntry[], context?: IExtractContext): {
|
|
111
118
|
sites: readonly IWiringTokenSite[];
|
|
112
119
|
error?: string;
|
|
113
120
|
};
|
|
@@ -128,5 +135,5 @@ export declare function validateWiringRule(rule: IWiringRule): string | undefine
|
|
|
128
135
|
* The `resolve` callback supplies the files for a given rule-side (injected so
|
|
129
136
|
* the engine stays pure / testable — see `runWiring` for the fs-backed wiring).
|
|
130
137
|
*/
|
|
131
|
-
export declare function evaluateWiring(rules: readonly IWiringRule[], resolve: WiringFileResolver): IWiringReport;
|
|
138
|
+
export declare function evaluateWiring(rules: readonly IWiringRule[], resolve: WiringFileResolver, context?: IExtractContext): IWiringReport;
|
|
132
139
|
//# sourceMappingURL=evaluate-wiring.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"evaluate-wiring.d.ts","sourceRoot":"","sources":["../../src/wiring/evaluate-wiring.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"evaluate-wiring.d.ts","sourceRoot":"","sources":["../../src/wiring/evaluate-wiring.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { validateWiringSource, } from '@shrkcrft/core';
|
|
1
|
+
import { failsWhenEmpty, validateWiringSource, } from '@shrkcrft/core';
|
|
2
2
|
import { extractTokens, } from "../extract/extract-tokens.js";
|
|
3
3
|
export const WIRING_SCHEMA = 'sharkcraft.wiring/v1';
|
|
4
4
|
/** Normalize the `registered` field (single source or union array) to an array. */
|
|
@@ -54,8 +54,8 @@ function hopPairs(rule) {
|
|
|
54
54
|
* extractor-backed baseline compute, so all three honour identical semantics. A
|
|
55
55
|
* misconfigured source returns an `error` and no sites (never throws).
|
|
56
56
|
*/
|
|
57
|
-
export function collectSourceSites(source, files) {
|
|
58
|
-
const res = extractTokens(source, files);
|
|
57
|
+
export function collectSourceSites(source, files, context = {}) {
|
|
58
|
+
const res = extractTokens(source, files, context);
|
|
59
59
|
return res.error ? { sites: [], error: res.error } : { sites: res.sites };
|
|
60
60
|
}
|
|
61
61
|
/** Group key for a token site under `groupBy` (dir = dirname; package = first two segments). */
|
|
@@ -133,7 +133,7 @@ export function validateWiringRule(rule) {
|
|
|
133
133
|
* The `resolve` callback supplies the files for a given rule-side (injected so
|
|
134
134
|
* the engine stays pure / testable — see `runWiring` for the fs-backed wiring).
|
|
135
135
|
*/
|
|
136
|
-
export function evaluateWiring(rules, resolve) {
|
|
136
|
+
export function evaluateWiring(rules, resolve, context = {}) {
|
|
137
137
|
const ruleResults = [];
|
|
138
138
|
const all = [];
|
|
139
139
|
const diagnostics = [];
|
|
@@ -179,9 +179,10 @@ export function evaluateWiring(rules, resolve) {
|
|
|
179
179
|
let sinkFiles = 0;
|
|
180
180
|
let sinkCount = 0;
|
|
181
181
|
let emptySink = false;
|
|
182
|
+
let sinkHint;
|
|
182
183
|
for (const [hopIndex, pair] of pairs.entries()) {
|
|
183
184
|
const fromFiles = resolve(pair.from);
|
|
184
|
-
const fromSites = extractTokens(pair.from, fromFiles).sites;
|
|
185
|
+
const fromSites = extractTokens(pair.from, fromFiles, context).sites;
|
|
185
186
|
const fromKeys = firstSites(fromSites, groupBy);
|
|
186
187
|
// Each sink kept separate so `intersection` can require membership in ALL.
|
|
187
188
|
const sinkKeySets = [];
|
|
@@ -189,7 +190,10 @@ export function evaluateWiring(rules, resolve) {
|
|
|
189
190
|
for (const sink of pair.to) {
|
|
190
191
|
const files = resolve(sink);
|
|
191
192
|
hopSinkFiles += files.length;
|
|
192
|
-
|
|
193
|
+
const sinkRes = extractTokens(sink, files, context);
|
|
194
|
+
if (sinkRes.hint && sinkHint === undefined)
|
|
195
|
+
sinkHint = sinkRes.hint;
|
|
196
|
+
sinkKeySets.push(firstSites(sinkRes.sites, groupBy));
|
|
193
197
|
}
|
|
194
198
|
const unionKeys = new Map();
|
|
195
199
|
for (const set of sinkKeySets) {
|
|
@@ -277,7 +281,7 @@ export function evaluateWiring(rules, resolve) {
|
|
|
277
281
|
? '0 ids extracted from the source side'
|
|
278
282
|
: undefined;
|
|
279
283
|
if (skipReason !== undefined) {
|
|
280
|
-
const failed = rule
|
|
284
|
+
const failed = failsWhenEmpty(rule);
|
|
281
285
|
skipped.push({ ruleId: rule.id, reason: skipReason, failed, severity });
|
|
282
286
|
if (failed) {
|
|
283
287
|
if (severity === 'error')
|
|
@@ -310,6 +314,7 @@ export function evaluateWiring(rules, resolve) {
|
|
|
310
314
|
registeredFiles: sinkFiles,
|
|
311
315
|
violations,
|
|
312
316
|
...(emptySink ? { emptySink: true } : {}),
|
|
317
|
+
...(emptySink && sinkHint !== undefined ? { sinkHint } : {}),
|
|
313
318
|
...(pairs.length > 1 ? { hops } : {}),
|
|
314
319
|
});
|
|
315
320
|
all.push(...violations);
|