@shrkcrft/boundaries 0.1.0-alpha.21 → 0.1.0-alpha.22
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/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/policy/evaluate-policy.d.ts +50 -0
- package/dist/policy/evaluate-policy.d.ts.map +1 -0
- package/dist/policy/evaluate-policy.js +92 -0
- package/dist/policy/extract-templates.d.ts +15 -0
- package/dist/policy/extract-templates.d.ts.map +1 -0
- package/dist/policy/extract-templates.js +42 -0
- package/dist/policy/run-policy.d.ts +21 -0
- package/dist/policy/run-policy.d.ts.map +1 -0
- package/dist/policy/run-policy.js +61 -0
- package/dist/util/safe-regex.d.ts +13 -0
- package/dist/util/safe-regex.d.ts.map +1 -0
- package/dist/util/safe-regex.js +24 -0
- package/dist/util/walk-files.d.ts +13 -0
- package/dist/util/walk-files.d.ts.map +1 -0
- package/dist/util/walk-files.js +80 -0
- package/dist/wiring/evaluate-wiring.d.ts +52 -0
- package/dist/wiring/evaluate-wiring.d.ts.map +1 -0
- package/dist/wiring/evaluate-wiring.js +131 -0
- package/dist/wiring/scan-wiring-files.d.ts +19 -0
- package/dist/wiring/scan-wiring-files.d.ts.map +1 -0
- package/dist/wiring/scan-wiring-files.js +32 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,11 @@ export * from './scan/glob.js';
|
|
|
5
5
|
export * from './scan/scan-imports.js';
|
|
6
6
|
export * from './evaluate/evaluate-boundaries.js';
|
|
7
7
|
export * from './scan/tsconfig-aliases.js';
|
|
8
|
+
export * from './wiring/evaluate-wiring.js';
|
|
9
|
+
export * from './wiring/scan-wiring-files.js';
|
|
10
|
+
export * from './policy/extract-templates.js';
|
|
11
|
+
export * from './policy/evaluate-policy.js';
|
|
12
|
+
export * from './policy/run-policy.js';
|
|
13
|
+
export * from './util/safe-regex.js';
|
|
14
|
+
export * from './util/walk-files.js';
|
|
8
15
|
//# sourceMappingURL=index.d.ts.map
|
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"}
|
|
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,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,10 @@ export * from "./scan/glob.js";
|
|
|
5
5
|
export * from "./scan/scan-imports.js";
|
|
6
6
|
export * from "./evaluate/evaluate-boundaries.js";
|
|
7
7
|
export * from "./scan/tsconfig-aliases.js";
|
|
8
|
+
export * from "./wiring/evaluate-wiring.js";
|
|
9
|
+
export * from "./wiring/scan-wiring-files.js";
|
|
10
|
+
export * from "./policy/extract-templates.js";
|
|
11
|
+
export * from "./policy/evaluate-policy.js";
|
|
12
|
+
export * from "./policy/run-policy.js";
|
|
13
|
+
export * from "./util/safe-regex.js";
|
|
14
|
+
export * from "./util/walk-files.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { IPolicyRule, PolicySurface } from '@shrkcrft/core';
|
|
2
|
+
export declare const POLICY_LINT_SCHEMA: "sharkcraft.policy-lint/v1";
|
|
3
|
+
/**
|
|
4
|
+
* A chunk of content to scan for one rule. For whole files `baseLine` is 1; for
|
|
5
|
+
* an inline template it is the source line where the template body begins, so
|
|
6
|
+
* findings map back to the real file line.
|
|
7
|
+
*/
|
|
8
|
+
export interface IPolicyUnit {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly content: string;
|
|
11
|
+
readonly baseLine: number;
|
|
12
|
+
/** Marks an inline-template unit (for clearer reporting). */
|
|
13
|
+
readonly inlineTemplate?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface IPolicyFinding {
|
|
16
|
+
readonly ruleId: string;
|
|
17
|
+
readonly surface: PolicySurface;
|
|
18
|
+
readonly file: string;
|
|
19
|
+
readonly line: number;
|
|
20
|
+
/** The matched token (capture group 1 if present, else the whole match, truncated). */
|
|
21
|
+
readonly match: string;
|
|
22
|
+
readonly message: string;
|
|
23
|
+
readonly suggest?: string;
|
|
24
|
+
readonly severity: 'error' | 'warning';
|
|
25
|
+
readonly inlineTemplate?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface IPolicyRuleResult {
|
|
28
|
+
readonly ruleId: string;
|
|
29
|
+
readonly surface: PolicySurface;
|
|
30
|
+
readonly severity: 'error' | 'warning';
|
|
31
|
+
readonly findingCount: number;
|
|
32
|
+
readonly error?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface IPolicyReport {
|
|
35
|
+
readonly schema: typeof POLICY_LINT_SCHEMA;
|
|
36
|
+
readonly rules: readonly IPolicyRuleResult[];
|
|
37
|
+
readonly findings: readonly IPolicyFinding[];
|
|
38
|
+
readonly diagnostics: readonly string[];
|
|
39
|
+
readonly verdict: 'pass' | 'errors' | 'warnings';
|
|
40
|
+
}
|
|
41
|
+
/** Resolves the content units to scan for a given rule (injected for purity/testability). */
|
|
42
|
+
export type PolicyUnitResolver = (rule: IPolicyRule) => readonly IPolicyUnit[];
|
|
43
|
+
/**
|
|
44
|
+
* Pure policy evaluation. Each rule's regex is run over the units the resolver
|
|
45
|
+
* supplies; matches become findings (capture group 1 is the reported token when
|
|
46
|
+
* present). A misconfigured rule (uncompilable regex) degrades to a diagnostic
|
|
47
|
+
* — never throws, so one bad rule cannot crash the check.
|
|
48
|
+
*/
|
|
49
|
+
export declare function evaluatePolicy(rules: readonly IPolicyRule[], resolve: PolicyUnitResolver): IPolicyReport;
|
|
50
|
+
//# sourceMappingURL=evaluate-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluate-policy.d.ts","sourceRoot":"","sources":["../../src/policy/evaluate-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGjE,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;CACnC;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,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,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;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,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;AAgB/E;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,EAAE,OAAO,EAAE,kBAAkB,GAAG,aAAa,CAoExG"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { safeCompile } from "../util/safe-regex.js";
|
|
2
|
+
export const POLICY_LINT_SCHEMA = 'sharkcraft.policy-lint/v1';
|
|
3
|
+
function lineWithin(content, index) {
|
|
4
|
+
let line = 1;
|
|
5
|
+
const end = Math.min(index, content.length);
|
|
6
|
+
for (let i = 0; i < end; i += 1) {
|
|
7
|
+
if (content[i] === '\n')
|
|
8
|
+
line += 1;
|
|
9
|
+
}
|
|
10
|
+
return line;
|
|
11
|
+
}
|
|
12
|
+
function truncate(s, max = 120) {
|
|
13
|
+
const oneLine = s.replace(/\s+/g, ' ').trim();
|
|
14
|
+
return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Pure policy evaluation. Each rule's regex is run over the units the resolver
|
|
18
|
+
* supplies; matches become findings (capture group 1 is the reported token when
|
|
19
|
+
* present). A misconfigured rule (uncompilable regex) degrades to a diagnostic
|
|
20
|
+
* — never throws, so one bad rule cannot crash the check.
|
|
21
|
+
*/
|
|
22
|
+
export function evaluatePolicy(rules, resolve) {
|
|
23
|
+
const ruleResults = [];
|
|
24
|
+
const findings = [];
|
|
25
|
+
const diagnostics = [];
|
|
26
|
+
let misconfigError = false;
|
|
27
|
+
let misconfigWarn = false;
|
|
28
|
+
for (const rule of rules) {
|
|
29
|
+
const severity = rule.severity ?? 'error';
|
|
30
|
+
const { re, error } = safeCompile(rule.pattern, rule.flags);
|
|
31
|
+
if (error || !re) {
|
|
32
|
+
const msg = `rule "${rule.id}": ${error}`;
|
|
33
|
+
diagnostics.push(msg);
|
|
34
|
+
if (severity === 'error')
|
|
35
|
+
misconfigError = true;
|
|
36
|
+
else
|
|
37
|
+
misconfigWarn = true;
|
|
38
|
+
ruleResults.push({ ruleId: rule.id, surface: rule.surface, severity, findingCount: 0, error: msg });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
let count = 0;
|
|
42
|
+
let zeroWidth = false;
|
|
43
|
+
for (const unit of resolve(rule)) {
|
|
44
|
+
re.lastIndex = 0;
|
|
45
|
+
let m;
|
|
46
|
+
while ((m = re.exec(unit.content)) !== null) {
|
|
47
|
+
if (m[0] === '') {
|
|
48
|
+
// A zero-width pattern (`a?`, a lookahead, …) would otherwise emit one
|
|
49
|
+
// empty finding per position. Advance and refuse to record; flag it.
|
|
50
|
+
zeroWidth = true;
|
|
51
|
+
re.lastIndex += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const token = m[1] !== undefined ? m[1] : m[0];
|
|
55
|
+
const line = unit.baseLine - 1 + lineWithin(unit.content, m.index);
|
|
56
|
+
findings.push({
|
|
57
|
+
ruleId: rule.id,
|
|
58
|
+
surface: rule.surface,
|
|
59
|
+
file: unit.path,
|
|
60
|
+
line,
|
|
61
|
+
match: truncate(token),
|
|
62
|
+
message: rule.message,
|
|
63
|
+
...(rule.suggest ? { suggest: rule.suggest } : {}),
|
|
64
|
+
severity,
|
|
65
|
+
...(unit.inlineTemplate ? { inlineTemplate: true } : {}),
|
|
66
|
+
});
|
|
67
|
+
count += 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (zeroWidth) {
|
|
71
|
+
const msg = `rule "${rule.id}": pattern matches the empty string (zero-width) — likely a misconfiguration`;
|
|
72
|
+
diagnostics.push(msg);
|
|
73
|
+
if (severity === 'error')
|
|
74
|
+
misconfigError = true;
|
|
75
|
+
else
|
|
76
|
+
misconfigWarn = true;
|
|
77
|
+
ruleResults.push({ ruleId: rule.id, surface: rule.surface, severity, findingCount: count, error: msg });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
ruleResults.push({ ruleId: rule.id, surface: rule.surface, severity, findingCount: count });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const hasError = misconfigError || findings.some((f) => f.severity === 'error');
|
|
84
|
+
const hasWarn = misconfigWarn || findings.some((f) => f.severity === 'warning');
|
|
85
|
+
return {
|
|
86
|
+
schema: POLICY_LINT_SCHEMA,
|
|
87
|
+
rules: ruleResults,
|
|
88
|
+
findings,
|
|
89
|
+
diagnostics,
|
|
90
|
+
verdict: hasError ? 'errors' : hasWarn ? 'warnings' : 'pass',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract inline `template:` string literals from source files so the template
|
|
3
|
+
* policy surface can see markup that AOT/tsc treat as an opaque string. This is
|
|
4
|
+
* a deterministic, framework-agnostic regex extraction (Angular/Vue/Lit-style
|
|
5
|
+
* `template:` properties) — not a full TS parse. `templateUrl` is intentionally
|
|
6
|
+
* NOT followed (the referenced `.html` file is scanned directly).
|
|
7
|
+
*/
|
|
8
|
+
export interface IExtractedTemplate {
|
|
9
|
+
/** The template body (literal contents, quotes/backticks stripped). */
|
|
10
|
+
readonly body: string;
|
|
11
|
+
/** 1-based line in the source file where the body begins. */
|
|
12
|
+
readonly startLine: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function extractInlineTemplates(content: string): IExtractedTemplate[];
|
|
15
|
+
//# sourceMappingURL=extract-templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-templates.d.ts","sourceRoot":"","sources":["../../src/policy/extract-templates.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAmBD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAe5E"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract inline `template:` string literals from source files so the template
|
|
3
|
+
* policy surface can see markup that AOT/tsc treat as an opaque string. This is
|
|
4
|
+
* a deterministic, framework-agnostic regex extraction (Angular/Vue/Lit-style
|
|
5
|
+
* `template:` properties) — not a full TS parse. `templateUrl` is intentionally
|
|
6
|
+
* NOT followed (the referenced `.html` file is scanned directly).
|
|
7
|
+
*/
|
|
8
|
+
// `template:` followed by a single-quoted, double-quoted, or backtick literal.
|
|
9
|
+
// Backtick allows newlines (multi-line templates). The `(?:\\.|[^delim\\])*`
|
|
10
|
+
// form is escape-aware, so an escaped same-delimiter quote (`\"` inside `"…"`)
|
|
11
|
+
// or an escaped backtick does NOT truncate the captured literal (which would
|
|
12
|
+
// silently drop violations after it). A nested backtick inside a `${…}`
|
|
13
|
+
// interpolation is still not handled (rare).
|
|
14
|
+
const TEMPLATE_RE = /\btemplate\s*:\s*(`(?:\\.|[^`\\])*`|'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*")/g;
|
|
15
|
+
function lineOf(content, index) {
|
|
16
|
+
let line = 1;
|
|
17
|
+
const end = Math.min(index, content.length);
|
|
18
|
+
for (let i = 0; i < end; i += 1) {
|
|
19
|
+
if (content[i] === '\n')
|
|
20
|
+
line += 1;
|
|
21
|
+
}
|
|
22
|
+
return line;
|
|
23
|
+
}
|
|
24
|
+
export function extractInlineTemplates(content) {
|
|
25
|
+
const out = [];
|
|
26
|
+
TEMPLATE_RE.lastIndex = 0;
|
|
27
|
+
let m;
|
|
28
|
+
while ((m = TEMPLATE_RE.exec(content)) !== null) {
|
|
29
|
+
if (m.index === TEMPLATE_RE.lastIndex)
|
|
30
|
+
TEMPLATE_RE.lastIndex += 1; // zero-width guard
|
|
31
|
+
const literal = m[1];
|
|
32
|
+
if (!literal)
|
|
33
|
+
continue;
|
|
34
|
+
const body = literal.slice(1, -1);
|
|
35
|
+
if (body.length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
// Index of the opening delimiter, then +1 for the first body char.
|
|
38
|
+
const literalStart = m.index + m[0].length - literal.length;
|
|
39
|
+
out.push({ body, startLine: lineOf(content, literalStart + 1) });
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IPolicyRule, PolicySurface } from '@shrkcrft/core';
|
|
2
|
+
import { type IPolicyReport } from './evaluate-policy.js';
|
|
3
|
+
export interface IRunPolicyOptions {
|
|
4
|
+
/** Restrict to rules on these surfaces. */
|
|
5
|
+
readonly surfaces?: readonly PolicySurface[];
|
|
6
|
+
/** Run only these rule ids. */
|
|
7
|
+
readonly only?: readonly string[];
|
|
8
|
+
/** When true, only run rules whose globs match a changed file. */
|
|
9
|
+
readonly changedOnly?: boolean;
|
|
10
|
+
readonly changedFiles?: readonly string[];
|
|
11
|
+
/** Project-relative directories to prune from the walk (e.g. the SharkCraft asset dir). */
|
|
12
|
+
readonly excludeDirs?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Filesystem-backed policy-lint. Walks the project once; on the `template`
|
|
16
|
+
* surface, source files contribute their inline `template:` bodies (with real
|
|
17
|
+
* source line numbers) while `.html` files are scanned whole. Pure-engine
|
|
18
|
+
* output; the only IO is the read-only walk + reads.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runPolicyLint(projectRoot: string, rules: readonly IPolicyRule[], options?: IRunPolicyOptions): IPolicyReport;
|
|
21
|
+
//# sourceMappingURL=run-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-policy.d.ts","sourceRoot":"","sources":["../../src/policy/run-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIjE,OAAO,EAAkB,KAAK,aAAa,EAAoB,MAAM,sBAAsB,CAAC;AAY5F,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;AAMD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CAsCf"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as nodePath from 'node:path';
|
|
2
|
+
import { matchesAny } from "../scan/glob.js";
|
|
3
|
+
import { readMatchingFiles } from "../util/walk-files.js";
|
|
4
|
+
import { extractInlineTemplates } from "./extract-templates.js";
|
|
5
|
+
import { evaluatePolicy } from "./evaluate-policy.js";
|
|
6
|
+
/** Per-surface default globs when a rule omits `files`. */
|
|
7
|
+
const SURFACE_DEFAULT_GLOBS = {
|
|
8
|
+
// markup files (scanned whole) + source files (inline `template:` extracted).
|
|
9
|
+
template: ['**/*.html', '**/*.htm', '**/*.ts', '**/*.tsx'],
|
|
10
|
+
style: ['**/*.css', '**/*.scss', '**/*.sass', '**/*.less', '**/*.styl'],
|
|
11
|
+
ts: ['**/*.ts', '**/*.tsx'],
|
|
12
|
+
};
|
|
13
|
+
const SOURCE_EXT = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
|
|
14
|
+
function globsFor(rule) {
|
|
15
|
+
return rule.files && rule.files.length > 0 ? rule.files : SURFACE_DEFAULT_GLOBS[rule.surface];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Filesystem-backed policy-lint. Walks the project once; on the `template`
|
|
19
|
+
* surface, source files contribute their inline `template:` bodies (with real
|
|
20
|
+
* source line numbers) while `.html` files are scanned whole. Pure-engine
|
|
21
|
+
* output; the only IO is the read-only walk + reads.
|
|
22
|
+
*/
|
|
23
|
+
export function runPolicyLint(projectRoot, rules, options = {}) {
|
|
24
|
+
let selected = rules;
|
|
25
|
+
if (options.surfaces && options.surfaces.length > 0) {
|
|
26
|
+
const s = new Set(options.surfaces);
|
|
27
|
+
selected = selected.filter((r) => s.has(r.surface));
|
|
28
|
+
}
|
|
29
|
+
if (options.only && options.only.length > 0) {
|
|
30
|
+
const ids = new Set(options.only);
|
|
31
|
+
selected = selected.filter((r) => ids.has(r.id));
|
|
32
|
+
}
|
|
33
|
+
if (options.changedOnly) {
|
|
34
|
+
const changed = options.changedFiles ?? [];
|
|
35
|
+
selected = selected.filter((r) => changed.some((c) => matchesAny(c, globsFor(r))));
|
|
36
|
+
}
|
|
37
|
+
if (selected.length === 0) {
|
|
38
|
+
return { schema: 'sharkcraft.policy-lint/v1', rules: [], findings: [], diagnostics: [], verdict: 'pass' };
|
|
39
|
+
}
|
|
40
|
+
const allGlobs = [...new Set(selected.flatMap((r) => [...globsFor(r)]))];
|
|
41
|
+
const cache = readMatchingFiles(projectRoot, allGlobs, new Set(options.excludeDirs ?? []));
|
|
42
|
+
return evaluatePolicy(selected, (rule) => {
|
|
43
|
+
const globs = globsFor(rule);
|
|
44
|
+
const units = [];
|
|
45
|
+
for (const [path, content] of cache) {
|
|
46
|
+
if (!matchesAny(path, globs))
|
|
47
|
+
continue;
|
|
48
|
+
const ext = nodePath.extname(path).toLowerCase();
|
|
49
|
+
if (rule.surface === 'template' && SOURCE_EXT.has(ext)) {
|
|
50
|
+
for (const tpl of extractInlineTemplates(content)) {
|
|
51
|
+
units.push({ path, content: tpl.body, baseLine: tpl.startLine, inlineTemplate: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// .html on the template surface, and all style/ts files: scan whole.
|
|
56
|
+
units.push({ path, content, baseLine: 1 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return units;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile a user/pack-supplied regex without ever throwing. The `g` flag is
|
|
3
|
+
* always applied (engines scan with `exec` loops); extra flags are merged and
|
|
4
|
+
* de-duped. A bad pattern / bad flags returns a clear `error` string so callers
|
|
5
|
+
* degrade a misconfigured rule to a diagnostic instead of crashing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function safeCompile(pattern: string, flags?: string): {
|
|
8
|
+
re?: RegExp;
|
|
9
|
+
error?: string;
|
|
10
|
+
};
|
|
11
|
+
/** Number of capture groups in a compiled regex (probe via an always-empty variant). */
|
|
12
|
+
export declare function countCaptureGroups(re: RegExp): number;
|
|
13
|
+
//# sourceMappingURL=safe-regex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-regex.d.ts","sourceRoot":"","sources":["../../src/util/safe-regex.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAO5F;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAMrD"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile a user/pack-supplied regex without ever throwing. The `g` flag is
|
|
3
|
+
* always applied (engines scan with `exec` loops); extra flags are merged and
|
|
4
|
+
* de-duped. A bad pattern / bad flags returns a clear `error` string so callers
|
|
5
|
+
* degrade a misconfigured rule to a diagnostic instead of crashing.
|
|
6
|
+
*/
|
|
7
|
+
export function safeCompile(pattern, flags) {
|
|
8
|
+
try {
|
|
9
|
+
const f = [...new Set(['g', ...(flags ?? '').split('')].filter(Boolean))].join('');
|
|
10
|
+
return { re: new RegExp(pattern, f) };
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
return { error: `invalid regex /${pattern}/${flags ?? ''}: ${e.message}` };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Number of capture groups in a compiled regex (probe via an always-empty variant). */
|
|
17
|
+
export function countCaptureGroups(re) {
|
|
18
|
+
try {
|
|
19
|
+
return (new RegExp(re.source + '|').exec('')?.length ?? 1) - 1;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return 1; // can't determine → assume valid
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Vendor / build / VCS dirs never scanned. */
|
|
2
|
+
export declare const SKIP_DIRS: ReadonlySet<string>;
|
|
3
|
+
/** Files larger than this are skipped (regex token extraction over multi-MB blobs is pointless). */
|
|
4
|
+
export declare const MAX_SCAN_FILE_BYTES = 1000000;
|
|
5
|
+
/**
|
|
6
|
+
* Walk `root`, returning project-relative POSIX paths that match any glob.
|
|
7
|
+
* `excludeDirs` is a set of project-relative POSIX directory paths to prune
|
|
8
|
+
* entirely (e.g. the SharkCraft asset/config dir).
|
|
9
|
+
*/
|
|
10
|
+
export declare function walkMatching(root: string, globs: readonly string[], excludeDirs?: ReadonlySet<string>): string[];
|
|
11
|
+
/** 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>;
|
|
13
|
+
//# sourceMappingURL=walk-files.d.ts.map
|
|
@@ -0,0 +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,GAC3C,MAAM,EAAE,CA2BV;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,GAC3C,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBrB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import { matchesAny } from "../scan/glob.js";
|
|
4
|
+
/** Vendor / build / VCS dirs never scanned. */
|
|
5
|
+
export const SKIP_DIRS = new Set([
|
|
6
|
+
'node_modules',
|
|
7
|
+
'.git',
|
|
8
|
+
'dist',
|
|
9
|
+
'build',
|
|
10
|
+
'coverage',
|
|
11
|
+
'.sharkcraft',
|
|
12
|
+
'.next',
|
|
13
|
+
'.turbo',
|
|
14
|
+
'.cache',
|
|
15
|
+
]);
|
|
16
|
+
/** Files larger than this are skipped (regex token extraction over multi-MB blobs is pointless). */
|
|
17
|
+
export const MAX_SCAN_FILE_BYTES = 1_000_000;
|
|
18
|
+
/**
|
|
19
|
+
* Walk `root`, returning project-relative POSIX paths that match any glob.
|
|
20
|
+
* `excludeDirs` is a set of project-relative POSIX directory paths to prune
|
|
21
|
+
* entirely (e.g. the SharkCraft asset/config dir).
|
|
22
|
+
*/
|
|
23
|
+
export function walkMatching(root, globs, excludeDirs = new Set()) {
|
|
24
|
+
const out = [];
|
|
25
|
+
const visit = (abs) => {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = readdirSync(abs, { withFileTypes: true });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (const e of entries) {
|
|
34
|
+
const childAbs = nodePath.join(abs, e.name);
|
|
35
|
+
const rel = nodePath.relative(root, childAbs).split(nodePath.sep).join('/');
|
|
36
|
+
if (e.isDirectory()) {
|
|
37
|
+
// Skip vendor/build/VCS dirs AND every dot-directory (`.yarn`, `.pnp`,
|
|
38
|
+
// `.venv`, `.gradle`, …) — matching the established scan-imports walker,
|
|
39
|
+
// so neither policy-lint nor wiring scans tooling/vendored sources.
|
|
40
|
+
// (Dirent.isDirectory() is false for symlinks, so symlinked dirs are
|
|
41
|
+
// never descended — no loop risk.)
|
|
42
|
+
if (e.name.startsWith('.') || SKIP_DIRS.has(e.name) || excludeDirs.has(rel))
|
|
43
|
+
continue;
|
|
44
|
+
visit(childAbs);
|
|
45
|
+
}
|
|
46
|
+
else if (e.isFile()) {
|
|
47
|
+
if (matchesAny(rel, globs))
|
|
48
|
+
out.push(rel);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
visit(root);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** Walk + read every file matching `globs`, skipping oversized/unreadable files. */
|
|
56
|
+
export function readMatchingFiles(root, globs, excludeDirs = new Set()) {
|
|
57
|
+
const out = new Map();
|
|
58
|
+
for (const rel of walkMatching(root, globs, excludeDirs)) {
|
|
59
|
+
const abs = nodePath.join(root, rel);
|
|
60
|
+
let size = -1;
|
|
61
|
+
try {
|
|
62
|
+
const st = statSync(abs);
|
|
63
|
+
if (!st.isFile())
|
|
64
|
+
continue;
|
|
65
|
+
size = st.size;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (size > MAX_SCAN_FILE_BYTES)
|
|
71
|
+
continue;
|
|
72
|
+
try {
|
|
73
|
+
out.set(rel, readFileSync(abs, 'utf8'));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// unreadable — skip
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { IWiringRule, IWiringSource } from '@shrkcrft/core';
|
|
2
|
+
export declare const WIRING_SCHEMA: "sharkcraft.wiring/v1";
|
|
3
|
+
/** A file made available to the engine. */
|
|
4
|
+
export interface IWiringFileEntry {
|
|
5
|
+
/** Project-relative POSIX path. */
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly content: string;
|
|
8
|
+
}
|
|
9
|
+
/** A captured token + where it was captured (for declared tokens). */
|
|
10
|
+
export interface IWiringTokenSite {
|
|
11
|
+
readonly token: string;
|
|
12
|
+
readonly file: string;
|
|
13
|
+
readonly line: number;
|
|
14
|
+
}
|
|
15
|
+
export interface IWiringViolation {
|
|
16
|
+
readonly ruleId: string;
|
|
17
|
+
/** The declared token that is not present in the registered set. */
|
|
18
|
+
readonly token: string;
|
|
19
|
+
/** Declaring file (project-relative) + 1-based line. */
|
|
20
|
+
readonly file: string;
|
|
21
|
+
readonly line: number;
|
|
22
|
+
readonly severity: 'error' | 'warning';
|
|
23
|
+
readonly hint?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface IWiringRuleResult {
|
|
26
|
+
readonly ruleId: string;
|
|
27
|
+
readonly description?: string;
|
|
28
|
+
readonly severity: 'error' | 'warning';
|
|
29
|
+
readonly declaredCount: number;
|
|
30
|
+
readonly registeredCount: number;
|
|
31
|
+
readonly violations: readonly IWiringViolation[];
|
|
32
|
+
/** Set when the rule is misconfigured (bad regex / no capture group). */
|
|
33
|
+
readonly error?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface IWiringReport {
|
|
36
|
+
readonly schema: typeof WIRING_SCHEMA;
|
|
37
|
+
readonly rules: readonly IWiringRuleResult[];
|
|
38
|
+
readonly violations: readonly IWiringViolation[];
|
|
39
|
+
/** Rule-level misconfiguration messages (never throws — degrades gracefully). */
|
|
40
|
+
readonly diagnostics: readonly string[];
|
|
41
|
+
readonly verdict: 'pass' | 'errors' | 'warnings';
|
|
42
|
+
}
|
|
43
|
+
/** Resolves a rule-side's globs to the concrete files (path + content) to scan. */
|
|
44
|
+
export type WiringFileResolver = (source: IWiringSource) => readonly IWiringFileEntry[];
|
|
45
|
+
/**
|
|
46
|
+
* Pure wiring evaluation. For each rule: every DECLARED token that is not in the
|
|
47
|
+
* REGISTERED set is a violation, reported at its first declaring site. The
|
|
48
|
+
* `resolve` callback supplies the files for a given rule-side (injected so the
|
|
49
|
+
* engine stays pure / testable — see `runWiring` for the fs-backed wiring).
|
|
50
|
+
*/
|
|
51
|
+
export declare function evaluateWiring(rules: readonly IWiringRule[], resolve: WiringFileResolver): IWiringReport;
|
|
52
|
+
//# sourceMappingURL=evaluate-wiring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluate-wiring.d.ts","sourceRoot":"","sources":["../../src/wiring/evaluate-wiring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGjE,eAAO,MAAM,aAAa,EAAG,sBAA+B,CAAC;AAE7D,2CAA2C;AAC3C,MAAM,WAAW,gBAAgB;IAC/B,mCAAmC;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACjD,yEAAyE;IACzE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC7C,QAAQ,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACjD,iFAAiF;IACjF,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;CAClD;AAED,mFAAmF;AACnF,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,aAAa,KAAK,SAAS,gBAAgB,EAAE,CAAC;AAiDxF;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,EAAE,kBAAkB,GAC1B,aAAa,CAkFf"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { safeCompile, countCaptureGroups } from "../util/safe-regex.js";
|
|
2
|
+
export const WIRING_SCHEMA = 'sharkcraft.wiring/v1';
|
|
3
|
+
/**
|
|
4
|
+
* Compile a rule-side's pattern, never throwing. Returns the regex or a clear
|
|
5
|
+
* error string for an uncompilable pattern / bad flags / missing capture group
|
|
6
|
+
* (group 1 is the token contract). A misconfigured rule must degrade to a
|
|
7
|
+
* diagnostic, not crash the check or the whole gate aggregator.
|
|
8
|
+
*/
|
|
9
|
+
function compileSafe(source) {
|
|
10
|
+
const { re, error } = safeCompile(source.pattern, source.flags);
|
|
11
|
+
if (error)
|
|
12
|
+
return { error };
|
|
13
|
+
if (countCaptureGroups(re) < 1) {
|
|
14
|
+
return {
|
|
15
|
+
error: `pattern /${source.pattern}/ has no capture group — group 1 must capture the token`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return { re };
|
|
19
|
+
}
|
|
20
|
+
/** Collect every capture-group-1 token from a precompiled side, with declaring sites. */
|
|
21
|
+
function collectTokens(re, files) {
|
|
22
|
+
const tokens = new Set();
|
|
23
|
+
const sites = [];
|
|
24
|
+
for (const f of files) {
|
|
25
|
+
re.lastIndex = 0;
|
|
26
|
+
let m;
|
|
27
|
+
while ((m = re.exec(f.content)) !== null) {
|
|
28
|
+
// Guard against a zero-width match looping forever.
|
|
29
|
+
if (m.index === re.lastIndex)
|
|
30
|
+
re.lastIndex += 1;
|
|
31
|
+
const token = m[1];
|
|
32
|
+
if (token === undefined || token === '')
|
|
33
|
+
continue;
|
|
34
|
+
tokens.add(token);
|
|
35
|
+
sites.push({ token, file: f.path, line: lineOf(f.content, m.index) });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { tokens, sites };
|
|
39
|
+
}
|
|
40
|
+
function lineOf(content, index) {
|
|
41
|
+
let line = 1;
|
|
42
|
+
for (let i = 0; i < index && i < content.length; i += 1) {
|
|
43
|
+
if (content[i] === '\n')
|
|
44
|
+
line += 1;
|
|
45
|
+
}
|
|
46
|
+
return line;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Pure wiring evaluation. For each rule: every DECLARED token that is not in the
|
|
50
|
+
* REGISTERED set is a violation, reported at its first declaring site. The
|
|
51
|
+
* `resolve` callback supplies the files for a given rule-side (injected so the
|
|
52
|
+
* engine stays pure / testable — see `runWiring` for the fs-backed wiring).
|
|
53
|
+
*/
|
|
54
|
+
export function evaluateWiring(rules, resolve) {
|
|
55
|
+
const ruleResults = [];
|
|
56
|
+
const all = [];
|
|
57
|
+
const diagnostics = [];
|
|
58
|
+
let misconfigError = false;
|
|
59
|
+
let misconfigWarn = false;
|
|
60
|
+
for (const rule of rules) {
|
|
61
|
+
const severity = rule.severity ?? 'error';
|
|
62
|
+
// Compile both sides defensively — a misconfigured rule becomes a
|
|
63
|
+
// diagnostic, never a thrown exception that would crash the gate.
|
|
64
|
+
const declaredRe = compileSafe(rule.declared);
|
|
65
|
+
const registeredRe = compileSafe(rule.registered);
|
|
66
|
+
if (declaredRe.error || registeredRe.error) {
|
|
67
|
+
const parts = [
|
|
68
|
+
declaredRe.error ? `declared ${declaredRe.error}` : '',
|
|
69
|
+
registeredRe.error ? `registered ${registeredRe.error}` : '',
|
|
70
|
+
].filter(Boolean);
|
|
71
|
+
const msg = `rule "${rule.id}": ${parts.join('; ')}`;
|
|
72
|
+
diagnostics.push(msg);
|
|
73
|
+
if (severity === 'error')
|
|
74
|
+
misconfigError = true;
|
|
75
|
+
else
|
|
76
|
+
misconfigWarn = true;
|
|
77
|
+
ruleResults.push({
|
|
78
|
+
ruleId: rule.id,
|
|
79
|
+
...(rule.description ? { description: rule.description } : {}),
|
|
80
|
+
severity,
|
|
81
|
+
declaredCount: 0,
|
|
82
|
+
registeredCount: 0,
|
|
83
|
+
violations: [],
|
|
84
|
+
error: msg,
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const declared = collectTokens(declaredRe.re, resolve(rule.declared));
|
|
89
|
+
const registered = collectTokens(registeredRe.re, resolve(rule.registered));
|
|
90
|
+
// First declaring site per token, in stable (file, line) order.
|
|
91
|
+
const firstSite = new Map();
|
|
92
|
+
for (const s of [...declared.sites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)) {
|
|
93
|
+
if (!firstSite.has(s.token))
|
|
94
|
+
firstSite.set(s.token, s);
|
|
95
|
+
}
|
|
96
|
+
const violations = [];
|
|
97
|
+
for (const token of [...declared.tokens].sort()) {
|
|
98
|
+
if (registered.tokens.has(token))
|
|
99
|
+
continue;
|
|
100
|
+
const site = firstSite.get(token);
|
|
101
|
+
violations.push({
|
|
102
|
+
ruleId: rule.id,
|
|
103
|
+
token,
|
|
104
|
+
file: site?.file ?? '',
|
|
105
|
+
line: site?.line ?? 0,
|
|
106
|
+
severity,
|
|
107
|
+
...(rule.hint ? { hint: rule.hint } : {}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
ruleResults.push({
|
|
111
|
+
ruleId: rule.id,
|
|
112
|
+
...(rule.description ? { description: rule.description } : {}),
|
|
113
|
+
severity,
|
|
114
|
+
declaredCount: declared.tokens.size,
|
|
115
|
+
registeredCount: registered.tokens.size,
|
|
116
|
+
violations,
|
|
117
|
+
});
|
|
118
|
+
all.push(...violations);
|
|
119
|
+
}
|
|
120
|
+
// A misconfigured rule must not pass as a silent green — it counts toward the
|
|
121
|
+
// verdict at its own severity (default error).
|
|
122
|
+
const hasError = misconfigError || all.some((v) => v.severity === 'error');
|
|
123
|
+
const hasWarn = misconfigWarn || all.some((v) => v.severity === 'warning');
|
|
124
|
+
return {
|
|
125
|
+
schema: WIRING_SCHEMA,
|
|
126
|
+
rules: ruleResults,
|
|
127
|
+
violations: all,
|
|
128
|
+
diagnostics,
|
|
129
|
+
verdict: hasError ? 'errors' : hasWarn ? 'warnings' : 'pass',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IWiringRule } from '@shrkcrft/core';
|
|
2
|
+
import { type IWiringReport } from './evaluate-wiring.js';
|
|
3
|
+
export interface IRunWiringOptions {
|
|
4
|
+
/** Only run rules touched by these (project-relative) changed files. */
|
|
5
|
+
readonly changedFiles?: readonly string[];
|
|
6
|
+
/** When true with no changed files, run nothing (matches `--changed-only` with a clean tree). */
|
|
7
|
+
readonly changedOnly?: boolean;
|
|
8
|
+
/** Run only these rule ids. */
|
|
9
|
+
readonly only?: readonly string[];
|
|
10
|
+
/** Project-relative directories to prune from the walk (e.g. the SharkCraft asset dir). */
|
|
11
|
+
readonly excludeDirs?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Filesystem-backed wiring run: walks the project once, reads only the files a
|
|
15
|
+
* rule references (skipping oversized files), and evaluates every (selected)
|
|
16
|
+
* rule. Pure-engine output; the only IO is the read-only tree walk + reads.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runWiring(projectRoot: string, rules: readonly IWiringRule[], options?: IRunWiringOptions): IWiringReport;
|
|
19
|
+
//# sourceMappingURL=scan-wiring-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan-wiring-files.d.ts","sourceRoot":"","sources":["../../src/wiring/scan-wiring-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,iGAAiG;IACjG,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2FAA2F;IAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CAyBf"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { matchesAny } from "../scan/glob.js";
|
|
2
|
+
import { readMatchingFiles } from "../util/walk-files.js";
|
|
3
|
+
import { evaluateWiring, } from "./evaluate-wiring.js";
|
|
4
|
+
/**
|
|
5
|
+
* Filesystem-backed wiring run: walks the project once, reads only the files a
|
|
6
|
+
* rule references (skipping oversized files), and evaluates every (selected)
|
|
7
|
+
* rule. Pure-engine output; the only IO is the read-only tree walk + reads.
|
|
8
|
+
*/
|
|
9
|
+
export function runWiring(projectRoot, rules, options = {}) {
|
|
10
|
+
let selected = rules;
|
|
11
|
+
if (options.only && options.only.length > 0) {
|
|
12
|
+
const ids = new Set(options.only);
|
|
13
|
+
selected = selected.filter((r) => ids.has(r.id));
|
|
14
|
+
}
|
|
15
|
+
if (options.changedOnly) {
|
|
16
|
+
const changed = options.changedFiles ?? [];
|
|
17
|
+
selected = selected.filter((r) => {
|
|
18
|
+
const globs = [...r.declared.files, ...r.registered.files];
|
|
19
|
+
return changed.some((c) => matchesAny(c, globs));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (selected.length === 0) {
|
|
23
|
+
return { schema: 'sharkcraft.wiring/v1', rules: [], violations: [], diagnostics: [], verdict: 'pass' };
|
|
24
|
+
}
|
|
25
|
+
// Union of all globs across selected rules → one tree walk, cached reads.
|
|
26
|
+
const allGlobs = [
|
|
27
|
+
...new Set(selected.flatMap((r) => [...r.declared.files, ...r.registered.files])),
|
|
28
|
+
];
|
|
29
|
+
const cache = readMatchingFiles(projectRoot, allGlobs, new Set(options.excludeDirs ?? []));
|
|
30
|
+
const entries = [...cache.entries()].map(([path, content]) => ({ path, content }));
|
|
31
|
+
return evaluateWiring(selected, (source) => entries.filter((f) => matchesAny(f.path, source.files)));
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shrkcrft/boundaries",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.22",
|
|
4
4
|
"description": "SharkCraft boundary rules: detect when a repository violates its own architecture (forbidden imports across folder/package/layer boundaries).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SharkCraft contributors",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@shrkcrft/core": "^0.1.0-alpha.
|
|
46
|
+
"@shrkcrft/core": "^0.1.0-alpha.22"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|