@shrkcrft/quality-gates 0.1.0-alpha.21 → 0.1.0-alpha.23

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.
@@ -9,9 +9,13 @@ import type { IGateResult } from '../schema/quality-gate.js';
9
9
  * This stops the gate from being a perpetual red on baseline debt the current
10
10
  * diff never introduced (an agent learns to ignore a gate that's always red).
11
11
  *
12
- * When no baseline is frozen (or `baselineRelative: false`), it preserves the
13
- * legacy behavior fail on ANY error so nothing regresses for projects that
14
- * never opted in, and a clean-tree CI demand stays expressible.
12
+ * When no baseline is frozen, errors are reported as `warn` (not `fail`) so the
13
+ * gate isn't a perpetual red on inherited debt the current diff never
14
+ * introduced an agent learns to ignore a gate that's always red. `--strict`
15
+ * still escalates the warn to a failure for CI, and freezing a baseline switches
16
+ * on NEW-only gating. When `baselineRelative: false` (the `--arch-all` opt-in),
17
+ * the gate fails on ANY error regardless of the baseline, keeping a clean-tree
18
+ * CI demand expressible.
15
19
  *
16
20
  * Warnings never fail the gate; they are reported as `warn`.
17
21
  */
@@ -1 +1 @@
1
- {"version":3,"file":"arch-gate.d.ts","sourceRoot":"","sources":["../../src/gates/arch-gate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,WAAW,CA6FzF"}
1
+ {"version":3,"file":"arch-gate.d.ts","sourceRoot":"","sources":["../../src/gates/arch-gate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,WAAW,CA+GzF"}
@@ -8,9 +8,13 @@ import { ArchReportStore, diffSnapshots, runArchCheck, snapshotFromReport, viola
8
8
  * This stops the gate from being a perpetual red on baseline debt the current
9
9
  * diff never introduced (an agent learns to ignore a gate that's always red).
10
10
  *
11
- * When no baseline is frozen (or `baselineRelative: false`), it preserves the
12
- * legacy behavior fail on ANY error so nothing regresses for projects that
13
- * never opted in, and a clean-tree CI demand stays expressible.
11
+ * When no baseline is frozen, errors are reported as `warn` (not `fail`) so the
12
+ * gate isn't a perpetual red on inherited debt the current diff never
13
+ * introduced an agent learns to ignore a gate that's always red. `--strict`
14
+ * still escalates the warn to a failure for CI, and freezing a baseline switches
15
+ * on NEW-only gating. When `baselineRelative: false` (the `--arch-all` opt-in),
16
+ * the gate fails on ANY error regardless of the baseline, keeping a clean-tree
17
+ * CI demand expressible.
14
18
  *
15
19
  * Warnings never fail the gate; they are reported as `warn`.
16
20
  */
@@ -71,16 +75,34 @@ export function archGate(projectRoot, options = {}) {
71
75
  durationMs: Date.now() - start,
72
76
  };
73
77
  }
74
- // No baseline (or baseline-relative disabled): fail on any error (legacy), but
75
- // hint how to opt into NEW-only gating.
78
+ // No baseline available: either none is frozen (the default state of a fresh
79
+ // repo) or the caller disabled baseline-relative gating via `--arch-all`.
76
80
  if (errors > 0) {
81
+ if (options.baselineRelative === false) {
82
+ // Explicit opt-in (`--arch-all`): fail on TOTAL errors so a clean-tree CI
83
+ // demand stays expressible.
84
+ return {
85
+ id: 'arch',
86
+ label: 'Architecture',
87
+ status: 'fail',
88
+ message: `${errors} architecture error(s).`,
89
+ details: { errors, warnings, kinds: report.countsByKind },
90
+ nextCommands: ['shrk arch check', 'shrk arch baseline write'],
91
+ durationMs: Date.now() - start,
92
+ };
93
+ }
94
+ // Default: no baseline frozen. Don't hard-fail on pre-existing debt the
95
+ // current diff never introduced — warn (so the signal stays visible) and
96
+ // point at freezing a baseline to switch on NEW-only gating. `--strict`
97
+ // still turns this warn into a hard failure for CI, and `--arch-all` fails
98
+ // on the total.
77
99
  return {
78
100
  id: 'arch',
79
101
  label: 'Architecture',
80
- status: 'fail',
81
- message: `${errors} architecture error(s).`,
82
- details: { errors, warnings, kinds: report.countsByKind },
83
- nextCommands: ['shrk arch check', 'shrk arch baseline write'],
102
+ status: 'warn',
103
+ message: `${errors} architecture error(s), no baseline frozen — not failing the gate (freeze one with \`shrk arch baseline write\` to track regressions; use \`--arch-all\` or \`--strict\` to fail).`,
104
+ details: { errors, warnings, kinds: report.countsByKind, noBaseline: true },
105
+ nextCommands: ['shrk arch baseline write', 'shrk arch check'],
84
106
  durationMs: Date.now() - start,
85
107
  };
86
108
  }
@@ -4,12 +4,24 @@ export interface IImpactGateOptions {
4
4
  sinceRef?: string;
5
5
  /** Risk levels that fail the gate. Default ['critical']. */
6
6
  failOn?: readonly ('high' | 'critical')[];
7
+ /**
8
+ * Explicit changed-file set to analyze. Used by `--changed-only` / `--staged`
9
+ * / `--files` when no `sinceRef` gitref is given. Ignored when `sinceRef` is
10
+ * set (the gitref diff wins). An empty array yields the `skipped` path.
11
+ */
12
+ files?: readonly string[];
7
13
  }
8
14
  /**
9
- * Impact-engine gate. Reads `git diff --name-only` against `sinceRef`
10
- * (default 'main') and runs `analyzeGraphImpact`. Fails when the
11
- * computed risk is in `failOn`. Skipped when no files changed or the
12
- * graph isn't indexed.
15
+ * Impact-engine gate. Analyzes the changeset and runs `analyzeGraphImpact`.
16
+ * When `files` is provided (and no `sinceRef`), the given changed-file set is
17
+ * analyzed directly; otherwise `git diff --name-only` is run against `sinceRef`
18
+ * (default 'main').
19
+ *
20
+ * Failure is THRESHOLD-based, not exact set-membership: the gate ranks
21
+ * low<medium<high<critical and fails when the computed risk is at least as
22
+ * severe as the *least* severe entry in `failOn`. So `--fail-on high` also
23
+ * fails a `critical` change. With the default `['critical']`, `critical` fails
24
+ * and `high` warns. Skipped when no files changed or the graph isn't indexed.
13
25
  */
14
26
  export declare function impactGate(projectRoot: string, options?: IImpactGateOptions): IGateResult;
15
27
  //# sourceMappingURL=impact-gate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"impact-gate.d.ts","sourceRoot":"","sources":["../../src/gates/impact-gate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,UAAU,CAAC,EAAE,CAAC;CAC3C;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,WAAW,CA+C7F"}
1
+ {"version":3,"file":"impact-gate.d.ts","sourceRoot":"","sources":["../../src/gates/impact-gate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,UAAU,CAAC,EAAE,CAAC;IAC1C;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAyD7F"}
@@ -1,19 +1,42 @@
1
1
  import { analyzeGraphImpact } from '@shrkcrft/impact-engine';
2
+ /** low < medium < high < critical — used for threshold comparison. */
3
+ const RISK_RANK = {
4
+ low: 0,
5
+ medium: 1,
6
+ high: 2,
7
+ critical: 3,
8
+ };
9
+ function rankOf(risk) {
10
+ return RISK_RANK[risk] ?? 0;
11
+ }
2
12
  /**
3
- * Impact-engine gate. Reads `git diff --name-only` against `sinceRef`
4
- * (default 'main') and runs `analyzeGraphImpact`. Fails when the
5
- * computed risk is in `failOn`. Skipped when no files changed or the
6
- * graph isn't indexed.
13
+ * Impact-engine gate. Analyzes the changeset and runs `analyzeGraphImpact`.
14
+ * When `files` is provided (and no `sinceRef`), the given changed-file set is
15
+ * analyzed directly; otherwise `git diff --name-only` is run against `sinceRef`
16
+ * (default 'main').
17
+ *
18
+ * Failure is THRESHOLD-based, not exact set-membership: the gate ranks
19
+ * low<medium<high<critical and fails when the computed risk is at least as
20
+ * severe as the *least* severe entry in `failOn`. So `--fail-on high` also
21
+ * fails a `critical` change. With the default `['critical']`, `critical` fails
22
+ * and `high` warns. Skipped when no files changed or the graph isn't indexed.
7
23
  */
8
24
  export function impactGate(projectRoot, options = {}) {
9
25
  const start = Date.now();
26
+ const failOn = options.failOn && options.failOn.length > 0 ? options.failOn : ['critical'];
27
+ const failThreshold = Math.min(...failOn.map((f) => rankOf(f)));
28
+ // Scope: an explicit changed-file set (when no gitref is given) is analyzed
29
+ // directly; otherwise diff against the gitref (default 'main').
30
+ const useFiles = !options.sinceRef && options.files !== undefined;
10
31
  const sinceRef = options.sinceRef ?? 'main';
11
- const failOn = new Set(options.failOn ?? ['critical']);
12
- const analysis = analyzeGraphImpact({ kind: 'gitref', ref: sinceRef }, { projectRoot });
32
+ const label = useFiles ? 'Impact (changed files)' : 'Impact (since ' + sinceRef + ')';
33
+ const analysis = useFiles
34
+ ? analyzeGraphImpact({ kind: 'files', files: options.files ?? [] }, { projectRoot })
35
+ : analyzeGraphImpact({ kind: 'gitref', ref: sinceRef }, { projectRoot });
13
36
  if (analysis.diagnostics.some((d) => d.includes('code-graph store missing'))) {
14
37
  return {
15
38
  id: 'impact',
16
- label: 'Impact (since ' + sinceRef + ')',
39
+ label,
17
40
  status: 'skipped',
18
41
  message: 'Skipped — graph index missing.',
19
42
  nextCommands: ['shrk graph index'],
@@ -23,13 +46,13 @@ export function impactGate(projectRoot, options = {}) {
23
46
  if (analysis.normalizedTargets.length === 0) {
24
47
  return {
25
48
  id: 'impact',
26
- label: 'Impact (since ' + sinceRef + ')',
49
+ label,
27
50
  status: 'skipped',
28
- message: `No files changed since ${sinceRef}.`,
51
+ message: useFiles ? 'No files changed.' : `No files changed since ${sinceRef}.`,
29
52
  durationMs: Date.now() - start,
30
53
  };
31
54
  }
32
- const status = failOn.has(analysis.risk) ? 'fail' : analysis.risk === 'high' ? 'warn' : 'pass';
55
+ const status = rankOf(analysis.risk) >= failThreshold ? 'fail' : analysis.risk === 'high' ? 'warn' : 'pass';
33
56
  const message = status === 'fail'
34
57
  ? `Risk: ${analysis.risk}. ${analysis.directDependents.length} direct dependents.`
35
58
  : status === 'warn'
@@ -37,7 +60,7 @@ export function impactGate(projectRoot, options = {}) {
37
60
  : `Risk: ${analysis.risk}. Looks safe.`;
38
61
  return {
39
62
  id: 'impact',
40
- label: 'Impact (since ' + sinceRef + ')',
63
+ label,
41
64
  status,
42
65
  message,
43
66
  details: {
@@ -0,0 +1,27 @@
1
+ import { type ISharkcraftInspection } from '@shrkcrft/inspector';
2
+ import type { IGateResult } from '../schema/quality-gate.js';
3
+ export interface IKnowledgeSymbolGateOptions {
4
+ /**
5
+ * Pre-loaded inspection (knowledge entries + project root). Loading the
6
+ * inspection is async, so the caller (the gate command) builds it and
7
+ * injects it here. The gate is skipped when omitted.
8
+ */
9
+ inspection?: ISharkcraftInspection;
10
+ /** Restrict to entries whose references touch one of these changed files. */
11
+ changedFiles?: readonly string[];
12
+ /**
13
+ * When true (default), a moved / renamed / missing symbol or file ref fails
14
+ * the gate; otherwise it warns.
15
+ */
16
+ failOnStale?: boolean;
17
+ }
18
+ /**
19
+ * Knowledge symbol-ref integrity gate. Walks every knowledge entry's symbol
20
+ * and file references and verifies each still resolves. Resolution is
21
+ * graph-backed: when the code graph is indexed it is passed to the stale-check
22
+ * so a *moved* symbol (now declared in a different file) is detected as stale
23
+ * rather than silently passing — single-file AST cannot see that. Skipped when
24
+ * no inspection is supplied or there are no symbol/file references in scope.
25
+ */
26
+ export declare function knowledgeSymbolGate(projectRoot: string, options?: IKnowledgeSymbolGateOptions): IGateResult;
27
+ //# sourceMappingURL=knowledge-symbol-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge-symbol-gate.d.ts","sourceRoot":"","sources":["../../src/gates/knowledge-symbol-gate.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAK7D,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,6EAA6E;IAC7E,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,2BAAgC,GACxC,WAAW,CA0Eb"}
@@ -0,0 +1,80 @@
1
+ import { loadGraphApiCached } from '@shrkcrft/graph';
2
+ import { buildKnowledgeStaleReport, ReferenceCheckOutcome, } from '@shrkcrft/inspector';
3
+ /** Reference kinds this gate is responsible for (symbol-ref integrity). */
4
+ const SCOPED_REF_KINDS = new Set(['symbol', 'file']);
5
+ /**
6
+ * Knowledge symbol-ref integrity gate. Walks every knowledge entry's symbol
7
+ * and file references and verifies each still resolves. Resolution is
8
+ * graph-backed: when the code graph is indexed it is passed to the stale-check
9
+ * so a *moved* symbol (now declared in a different file) is detected as stale
10
+ * rather than silently passing — single-file AST cannot see that. Skipped when
11
+ * no inspection is supplied or there are no symbol/file references in scope.
12
+ */
13
+ export function knowledgeSymbolGate(projectRoot, options = {}) {
14
+ const start = Date.now();
15
+ const inspection = options.inspection;
16
+ if (!inspection) {
17
+ return {
18
+ id: 'knowledge-symbol',
19
+ label: 'Knowledge symbol refs',
20
+ status: 'skipped',
21
+ message: 'Skipped — no knowledge inspection supplied.',
22
+ durationMs: Date.now() - start,
23
+ };
24
+ }
25
+ if (inspection.knowledgeEntries.length === 0) {
26
+ return {
27
+ id: 'knowledge-symbol',
28
+ label: 'Knowledge symbol refs',
29
+ status: 'skipped',
30
+ message: 'No knowledge entries — nothing to verify.',
31
+ durationMs: Date.now() - start,
32
+ };
33
+ }
34
+ // Graph-resolved when available; null falls the stale-check back to AST.
35
+ const graph = loadGraphApiCached(projectRoot) ?? undefined;
36
+ const report = buildKnowledgeStaleReport(inspection, {
37
+ ...(options.changedFiles ? { changedFiles: options.changedFiles } : {}),
38
+ ...(graph ? { graph } : {}),
39
+ });
40
+ const scoped = report.referenceChecks.filter((c) => SCOPED_REF_KINDS.has(c.reference.kind));
41
+ const evaluated = scoped.length;
42
+ if (evaluated === 0) {
43
+ return {
44
+ id: 'knowledge-symbol',
45
+ label: 'Knowledge symbol refs',
46
+ status: 'skipped',
47
+ message: 'No symbol/file references in scope — nothing evaluated.',
48
+ details: { evaluated: 0, graphResolved: Boolean(graph) },
49
+ durationMs: Date.now() - start,
50
+ };
51
+ }
52
+ const broken = scoped.filter((c) => c.outcome === ReferenceCheckOutcome.Stale || c.outcome === ReferenceCheckOutcome.Missing);
53
+ if (broken.length === 0) {
54
+ return {
55
+ id: 'knowledge-symbol',
56
+ label: 'Knowledge symbol refs',
57
+ status: 'pass',
58
+ message: `${evaluated} symbol/file reference(s) resolve.`,
59
+ details: { evaluated, graphResolved: Boolean(graph) },
60
+ durationMs: Date.now() - start,
61
+ };
62
+ }
63
+ const samples = broken.slice(0, 8).map((c) => {
64
+ const target = c.reference.symbol
65
+ ? `symbol \`${c.reference.symbol}\``
66
+ : (c.reference.path ?? '?');
67
+ const where = c.reference.path && c.reference.symbol ? ` (${c.reference.path})` : '';
68
+ return `${c.entryId}: ${target}${where} — ${c.message}`;
69
+ });
70
+ const failOnStale = options.failOnStale ?? true;
71
+ return {
72
+ id: 'knowledge-symbol',
73
+ label: 'Knowledge symbol refs',
74
+ status: failOnStale ? 'fail' : 'warn',
75
+ message: `${broken.length}/${evaluated} symbol/file reference(s) stale or missing (moved/renamed).`,
76
+ details: { evaluated, broken: broken.length, samples, graphResolved: Boolean(graph) },
77
+ nextCommands: ['shrk knowledge audit', 'shrk doctor'],
78
+ durationMs: Date.now() - start,
79
+ };
80
+ }
@@ -0,0 +1,25 @@
1
+ import type { IPolicyRule } from '@shrkcrft/core';
2
+ import type { IGateResult } from '../schema/quality-gate.js';
3
+ export interface IPolicyLintGateOptions {
4
+ /** The project's policy rules (from `sharkcraft.config.ts` `policyRules[]`). */
5
+ rules?: readonly IPolicyRule[];
6
+ /** Restrict to rules whose globs match one of these (project-relative) changed files. */
7
+ changedFiles?: readonly string[];
8
+ /** When true, only run rules touched by `changedFiles`. */
9
+ changedOnly?: boolean;
10
+ /**
11
+ * Set when the project config could not be loaded/validated. The gate surfaces
12
+ * it (warn) instead of silently skipping — a malformed config must not quietly
13
+ * disable the policy plane.
14
+ */
15
+ configError?: string;
16
+ }
17
+ /**
18
+ * The "policy plane" gate: runs the project's data-defined policy-lint rules
19
+ * (template / markup / stylesheet / AOT-invisible TS surfaces that tsc cannot
20
+ * see). Skipped — never red — when no rules are configured, so it's inert for
21
+ * projects that don't opt in. Honors the loud-zero contract: rules that exist
22
+ * but fall out of the changed-only scope report `skipped`, never a silent pass.
23
+ */
24
+ export declare function policyLintGate(projectRoot: string, options?: IPolicyLintGateOptions): IGateResult;
25
+ //# sourceMappingURL=policy-lint-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-lint-gate.d.ts","sourceRoot":"","sources":["../../src/gates/policy-lint-gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,MAAM,WAAW,sBAAsB;IACrC,gFAAgF;IAChF,KAAK,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IAC/B,yFAAyF;IACzF,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,sBAA2B,GAAG,WAAW,CA2ErG"}
@@ -0,0 +1,80 @@
1
+ import { runPolicyLint } from '@shrkcrft/boundaries';
2
+ /**
3
+ * The "policy plane" gate: runs the project's data-defined policy-lint rules
4
+ * (template / markup / stylesheet / AOT-invisible TS surfaces that tsc cannot
5
+ * see). Skipped — never red — when no rules are configured, so it's inert for
6
+ * projects that don't opt in. Honors the loud-zero contract: rules that exist
7
+ * but fall out of the changed-only scope report `skipped`, never a silent pass.
8
+ */
9
+ export function policyLintGate(projectRoot, options = {}) {
10
+ const start = Date.now();
11
+ if (options.configError) {
12
+ return {
13
+ id: 'policy',
14
+ label: 'Policy lint',
15
+ status: 'warn',
16
+ message: `Config could not be loaded — policy rules not evaluated: ${options.configError}`,
17
+ nextCommands: ['shrk doctor'],
18
+ durationMs: Date.now() - start,
19
+ };
20
+ }
21
+ const rules = options.rules ?? [];
22
+ if (rules.length === 0) {
23
+ return {
24
+ id: 'policy',
25
+ label: 'Policy lint',
26
+ status: 'skipped',
27
+ message: 'No policy rules configured (sharkcraft.config.ts policyRules[]).',
28
+ durationMs: Date.now() - start,
29
+ };
30
+ }
31
+ const report = runPolicyLint(projectRoot, rules, {
32
+ ...(options.changedOnly ? { changedOnly: true, changedFiles: options.changedFiles ?? [] } : {}),
33
+ });
34
+ // Loud zero case: rules exist but none actually scanned a file (their globs
35
+ // matched no files — e.g. a `style` rule in a project with no stylesheets — or
36
+ // `--changed-only` filtered them all out). Surface it as skipped rather than a
37
+ // silent green pass.
38
+ if (report.rules.length === 0 || report.evaluated === 0) {
39
+ return {
40
+ id: 'policy',
41
+ label: 'Policy lint',
42
+ status: 'skipped',
43
+ message: 'No policy rules in scope — nothing evaluated.',
44
+ details: { evaluated: 0 },
45
+ durationMs: Date.now() - start,
46
+ };
47
+ }
48
+ const errors = report.findings.filter((f) => f.severity === 'error').length;
49
+ const warnings = report.findings.filter((f) => f.severity === 'warning').length;
50
+ const samples = report.findings
51
+ .slice(0, 8)
52
+ .map((f) => `${f.ruleId}: ${f.match} (${f.file}:${f.line})`);
53
+ const diagnostics = report.diagnostics;
54
+ if (report.verdict === 'pass') {
55
+ return {
56
+ id: 'policy',
57
+ label: 'Policy lint',
58
+ status: 'pass',
59
+ message: `${report.rules.length} policy rule(s) — no violations.`,
60
+ details: { rules: report.rules.length, evaluated: report.evaluated },
61
+ durationMs: Date.now() - start,
62
+ };
63
+ }
64
+ const misconfig = diagnostics.length > 0 ? `, ${diagnostics.length} misconfigured rule(s)` : '';
65
+ return {
66
+ id: 'policy',
67
+ label: 'Policy lint',
68
+ status: report.verdict === 'errors' ? 'fail' : 'warn',
69
+ message: `${errors} error(s), ${warnings} warning(s)${misconfig}: policy violation(s).`,
70
+ details: {
71
+ errors,
72
+ warnings,
73
+ samples,
74
+ evaluated: report.evaluated,
75
+ ...(diagnostics.length > 0 ? { diagnostics } : {}),
76
+ },
77
+ nextCommands: ['shrk policy-lint'],
78
+ durationMs: Date.now() - start,
79
+ };
80
+ }
@@ -0,0 +1,23 @@
1
+ import type { IWiringRule } from '@shrkcrft/core';
2
+ import type { IGateResult } from '../schema/quality-gate.js';
3
+ export interface IWiringGateOptions {
4
+ /** The project's wiring rules (from `sharkcraft.config.ts` `wiringRules[]`). */
5
+ rules?: readonly IWiringRule[];
6
+ /** Restrict to rules touched by these (project-relative) changed files. */
7
+ changedFiles?: readonly string[];
8
+ /** When true, only run rules touched by `changedFiles`. */
9
+ changedOnly?: boolean;
10
+ /**
11
+ * Set when the project config could not be loaded/validated. The gate surfaces
12
+ * it (warn) instead of silently skipping — a malformed config must not quietly
13
+ * disable the wiring plane.
14
+ */
15
+ configError?: string;
16
+ }
17
+ /**
18
+ * The "completeness plane" gate: runs the project's data-defined wiring rules
19
+ * (declared token set ⊆ registered token set). Skipped — never red — when no
20
+ * rules are configured, so it's inert for projects that don't opt in.
21
+ */
22
+ export declare function wiringGate(projectRoot: string, options?: IWiringGateOptions): IGateResult;
23
+ //# sourceMappingURL=wiring-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wiring-gate.d.ts","sourceRoot":"","sources":["../../src/gates/wiring-gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,gFAAgF;IAChF,KAAK,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IAC/B,2EAA2E;IAC3E,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAwE7F"}
@@ -0,0 +1,75 @@
1
+ import { runWiring } from '@shrkcrft/boundaries';
2
+ /**
3
+ * The "completeness plane" gate: runs the project's data-defined wiring rules
4
+ * (declared token set ⊆ registered token set). Skipped — never red — when no
5
+ * rules are configured, so it's inert for projects that don't opt in.
6
+ */
7
+ export function wiringGate(projectRoot, options = {}) {
8
+ const start = Date.now();
9
+ if (options.configError) {
10
+ return {
11
+ id: 'wiring',
12
+ label: 'Wiring (completeness)',
13
+ status: 'warn',
14
+ message: `Config could not be loaded — wiring rules not evaluated: ${options.configError}`,
15
+ nextCommands: ['shrk doctor'],
16
+ durationMs: Date.now() - start,
17
+ };
18
+ }
19
+ const rules = options.rules ?? [];
20
+ if (rules.length === 0) {
21
+ return {
22
+ id: 'wiring',
23
+ label: 'Wiring (completeness)',
24
+ status: 'skipped',
25
+ message: 'No wiring rules configured (sharkcraft.config.ts wiringRules[]).',
26
+ durationMs: Date.now() - start,
27
+ };
28
+ }
29
+ const report = runWiring(projectRoot, rules, {
30
+ ...(options.changedOnly ? { changedOnly: true, changedFiles: options.changedFiles ?? [] } : {}),
31
+ });
32
+ // Loud zero case: rules exist but none actually ran a comparison (their globs
33
+ // matched no files, or `--changed-only` filtered them all out). Surface it as
34
+ // skipped rather than a silent green pass.
35
+ if (report.rules.length === 0 || report.evaluated === 0) {
36
+ return {
37
+ id: 'wiring',
38
+ label: 'Wiring (completeness)',
39
+ status: 'skipped',
40
+ message: 'No wiring rules in scope — nothing evaluated.',
41
+ details: { evaluated: 0 },
42
+ durationMs: Date.now() - start,
43
+ };
44
+ }
45
+ const errors = report.violations.filter((v) => v.severity === 'error').length;
46
+ const warnings = report.violations.filter((v) => v.severity === 'warning').length;
47
+ const samples = report.violations.slice(0, 8).map((v) => `${v.ruleId}: ${v.token} (${v.file}:${v.line})`);
48
+ const diagnostics = report.diagnostics;
49
+ if (report.verdict === 'pass') {
50
+ return {
51
+ id: 'wiring',
52
+ label: 'Wiring (completeness)',
53
+ status: 'pass',
54
+ message: `${report.rules.length} wiring rule(s) — every declared token is wired.`,
55
+ details: { rules: report.rules.length, evaluated: report.evaluated },
56
+ durationMs: Date.now() - start,
57
+ };
58
+ }
59
+ const misconfig = diagnostics.length > 0 ? `, ${diagnostics.length} misconfigured rule(s)` : '';
60
+ return {
61
+ id: 'wiring',
62
+ label: 'Wiring (completeness)',
63
+ status: report.verdict === 'errors' ? 'fail' : 'warn',
64
+ message: `${errors} error(s), ${warnings} warning(s)${misconfig}: declared but not wired.`,
65
+ details: {
66
+ errors,
67
+ warnings,
68
+ samples,
69
+ evaluated: report.evaluated,
70
+ ...(diagnostics.length > 0 ? { diagnostics } : {}),
71
+ },
72
+ nextCommands: ['shrk check wiring'],
73
+ durationMs: Date.now() - start,
74
+ };
75
+ }
package/dist/index.d.ts CHANGED
@@ -9,6 +9,9 @@ export * from './gates/graph-unresolved-gate.js';
9
9
  export * from './gates/impact-baseline-gate.js';
10
10
  export * from './gates/structural-patterns-gate.js';
11
11
  export * from './gates/intent-classifier-gate.js';
12
+ export * from './gates/wiring-gate.js';
13
+ export * from './gates/policy-lint-gate.js';
14
+ export * from './gates/knowledge-symbol-gate.js';
12
15
  export * from './runner/run-gates.js';
13
16
  export * from './runner/report-store.js';
14
17
  export * from './runner/render-markdown.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAClD,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kCAAkC,CAAC;AACjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAClD,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kCAAkC,CAAC;AACjD,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC"}
package/dist/index.js CHANGED
@@ -9,6 +9,9 @@ export * from "./gates/graph-unresolved-gate.js";
9
9
  export * from "./gates/impact-baseline-gate.js";
10
10
  export * from "./gates/structural-patterns-gate.js";
11
11
  export * from "./gates/intent-classifier-gate.js";
12
+ export * from "./gates/wiring-gate.js";
13
+ export * from "./gates/policy-lint-gate.js";
14
+ export * from "./gates/knowledge-symbol-gate.js";
12
15
  export * from "./runner/run-gates.js";
13
16
  export * from "./runner/report-store.js";
14
17
  export * from "./runner/render-markdown.js";
@@ -6,6 +6,9 @@ import { type IImpactGateOptions } from '../gates/impact-gate.js';
6
6
  import { type IImpactBaselineGateOptions } from '../gates/impact-baseline-gate.js';
7
7
  import { type IIntentClassifierGateOptions } from '../gates/intent-classifier-gate.js';
8
8
  import { type IStructuralPatternsGateOptions } from '../gates/structural-patterns-gate.js';
9
+ import { type IWiringGateOptions } from '../gates/wiring-gate.js';
10
+ import { type IPolicyLintGateOptions } from '../gates/policy-lint-gate.js';
11
+ import { type IKnowledgeSymbolGateOptions } from '../gates/knowledge-symbol-gate.js';
9
12
  import { type IQualityGateReport } from '../schema/quality-gate.js';
10
13
  export interface IRunGatesOptions {
11
14
  projectRoot: string;
@@ -28,6 +31,15 @@ export interface IRunGatesOptions {
28
31
  structuralPatterns?: IStructuralPatternsGateOptions;
29
32
  /** Optional intent-classifier gate config. */
30
33
  intentClassifier?: IIntentClassifierGateOptions;
34
+ /** Optional wiring gate config (the project's wiringRules + change scope). */
35
+ wiring?: IWiringGateOptions;
36
+ /** Optional policy-lint gate config (the project's policyRules + change scope). */
37
+ policy?: IPolicyLintGateOptions;
38
+ /**
39
+ * Optional knowledge symbol-ref gate config. When omitted, the gate is
40
+ * skipped — the inspection (async to load) is what opts a project in.
41
+ */
42
+ knowledgeSymbol?: IKnowledgeSymbolGateOptions;
31
43
  /** Disable specific gates by id. */
32
44
  disable?: readonly string[];
33
45
  }
@@ -1 +1 @@
1
- {"version":3,"file":"run-gates.d.ts","sourceRoot":"","sources":["../../src/runner/run-gates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAElF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAmB,KAAK,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE9F,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAc,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAEL,KAAK,8BAA8B,EACpC,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,sDAAsD;IACtD,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B;;;OAGG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,yCAAyC;IACzC,WAAW,CAAC,EAAE,uBAAuB,CAAC;IACtC,6CAA6C;IAC7C,eAAe,CAAC,EAAE,2BAA2B,CAAC;IAC9C,4CAA4C;IAC5C,cAAc,CAAC,EAAE,0BAA0B,CAAC;IAC5C,gDAAgD;IAChD,kBAAkB,CAAC,EAAE,8BAA8B,CAAC;IACpD,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,4BAA4B,CAAC;IAChD,oCAAoC;IACpC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,kBAAkB,CAkD7E"}
1
+ {"version":3,"file":"run-gates.d.ts","sourceRoot":"","sources":["../../src/runner/run-gates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAElF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAmB,KAAK,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE9F,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAc,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAEL,KAAK,8BAA8B,EACpC,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAc,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAkB,KAAK,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3F,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,sDAAsD;IACtD,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B;;;OAGG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,yCAAyC;IACzC,WAAW,CAAC,EAAE,uBAAuB,CAAC;IACtC,6CAA6C;IAC7C,eAAe,CAAC,EAAE,2BAA2B,CAAC;IAC9C,4CAA4C;IAC5C,cAAc,CAAC,EAAE,0BAA0B,CAAC;IAC5C,gDAAgD;IAChD,kBAAkB,CAAC,EAAE,8BAA8B,CAAC;IACpD,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,4BAA4B,CAAC;IAChD,8EAA8E;IAC9E,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,mFAAmF;IACnF,MAAM,CAAC,EAAE,sBAAsB,CAAC;IAChC;;;OAGG;IACH,eAAe,CAAC,EAAE,2BAA2B,CAAC;IAC9C,oCAAoC;IACpC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,kBAAkB,CA2D7E"}
@@ -7,6 +7,9 @@ import { impactGate } from "../gates/impact-gate.js";
7
7
  import { impactBaselineGate, } from "../gates/impact-baseline-gate.js";
8
8
  import { intentClassifierGate, } from "../gates/intent-classifier-gate.js";
9
9
  import { structuralPatternsGate, } from "../gates/structural-patterns-gate.js";
10
+ import { wiringGate } from "../gates/wiring-gate.js";
11
+ import { policyLintGate } from "../gates/policy-lint-gate.js";
12
+ import { knowledgeSymbolGate, } from "../gates/knowledge-symbol-gate.js";
10
13
  import { QUALITY_GATE_SCHEMA, } from "../schema/quality-gate.js";
11
14
  /**
12
15
  * Run the quality-gate aggregator over a project.
@@ -60,6 +63,15 @@ export function runQualityGates(options) {
60
63
  if (!disabled.has('intent-classifier')) {
61
64
  gates.push(intentClassifierGate(options.projectRoot, options.intentClassifier ?? {}));
62
65
  }
66
+ if (!disabled.has('wiring')) {
67
+ gates.push(wiringGate(options.projectRoot, options.wiring ?? {}));
68
+ }
69
+ if (!disabled.has('policy')) {
70
+ gates.push(policyLintGate(options.projectRoot, options.policy ?? {}));
71
+ }
72
+ if (!disabled.has('knowledge-symbol') && options.knowledgeSymbol) {
73
+ gates.push(knowledgeSymbolGate(options.projectRoot, options.knowledgeSymbol));
74
+ }
63
75
  if (!disabled.has('api-diff') && options.apiDiff) {
64
76
  gates.push(apiDiffGate(options.projectRoot, options.apiDiff));
65
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/quality-gates",
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.23",
4
4
  "description": "SharkCraft quality gates: single aggregator that runs the code-intelligence checks (graph index, boundaries, architecture, impact summary) and emits one pass/fail report. The pre-merge gate for AI-agent-authored changes.",
5
5
  "license": "MIT",
6
6
  "author": "SharkCraft contributors",
@@ -43,14 +43,15 @@
43
43
  "typecheck": "tsc --noEmit -p tsconfig.json"
44
44
  },
45
45
  "dependencies": {
46
- "@shrkcrft/core": "^0.1.0-alpha.21",
47
- "@shrkcrft/graph": "^0.1.0-alpha.21",
48
- "@shrkcrft/architecture-guard": "^0.1.0-alpha.21",
49
- "@shrkcrft/impact-engine": "^0.1.0-alpha.21",
50
- "@shrkcrft/api-surface-diff": "^0.1.0-alpha.21",
51
- "@shrkcrft/boundaries": "^0.1.0-alpha.21",
52
- "@shrkcrft/structural-search": "^0.1.0-alpha.21",
53
- "@shrkcrft/context-planner": "^0.1.0-alpha.21"
46
+ "@shrkcrft/core": "^0.1.0-alpha.23",
47
+ "@shrkcrft/graph": "^0.1.0-alpha.23",
48
+ "@shrkcrft/architecture-guard": "^0.1.0-alpha.23",
49
+ "@shrkcrft/impact-engine": "^0.1.0-alpha.23",
50
+ "@shrkcrft/api-surface-diff": "^0.1.0-alpha.23",
51
+ "@shrkcrft/boundaries": "^0.1.0-alpha.23",
52
+ "@shrkcrft/structural-search": "^0.1.0-alpha.23",
53
+ "@shrkcrft/context-planner": "^0.1.0-alpha.23",
54
+ "@shrkcrft/inspector": "^0.1.0-alpha.23"
54
55
  },
55
56
  "publishConfig": {
56
57
  "access": "public"