@hongmaple0820/scale-engine 0.48.0 → 0.49.0

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.
Files changed (77) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/agents/evidenceDiscipline.d.ts +7 -0
  4. package/dist/agents/evidenceDiscipline.js +21 -0
  5. package/dist/agents/evidenceDiscipline.js.map +1 -0
  6. package/dist/agents/profiles.js +8 -1
  7. package/dist/agents/profiles.js.map +1 -1
  8. package/dist/agents/types.d.ts +1 -0
  9. package/dist/artifact/types.d.ts +59 -0
  10. package/dist/artifact/types.js.map +1 -1
  11. package/dist/cli/cortexCommands.d.ts +36 -0
  12. package/dist/cli/cortexCommands.js +76 -4
  13. package/dist/cli/cortexCommands.js.map +1 -1
  14. package/dist/cli/evalCommands.js +12 -1
  15. package/dist/cli/evalCommands.js.map +1 -1
  16. package/dist/cli/phaseCommands.d.ts +53 -1
  17. package/dist/cli/phaseCommands.js +317 -22
  18. package/dist/cli/phaseCommands.js.map +1 -1
  19. package/dist/cortex/InstinctStore.d.ts +32 -1
  20. package/dist/cortex/InstinctStore.js +235 -42
  21. package/dist/cortex/InstinctStore.js.map +1 -1
  22. package/dist/cortex/InstinctValidation.d.ts +9 -0
  23. package/dist/cortex/InstinctValidation.js +55 -0
  24. package/dist/cortex/InstinctValidation.js.map +1 -0
  25. package/dist/cortex/SessionInjector.js +13 -6
  26. package/dist/cortex/SessionInjector.js.map +1 -1
  27. package/dist/eval/BenchmarkPublisher.d.ts +2 -0
  28. package/dist/eval/BenchmarkPublisher.js +43 -0
  29. package/dist/eval/BenchmarkPublisher.js.map +1 -1
  30. package/dist/guardrails/ast/confirmers.d.ts +18 -0
  31. package/dist/guardrails/ast/confirmers.js +69 -0
  32. package/dist/guardrails/ast/confirmers.js.map +1 -0
  33. package/dist/guardrails/ast/parse.d.ts +20 -0
  34. package/dist/guardrails/ast/parse.js +51 -0
  35. package/dist/guardrails/ast/parse.js.map +1 -0
  36. package/dist/output/HTMLDocumentRenderer.d.ts +9 -0
  37. package/dist/output/HTMLDocumentRenderer.js +19 -0
  38. package/dist/output/HTMLDocumentRenderer.js.map +1 -1
  39. package/dist/review/FreshContextVerifier.d.ts +35 -0
  40. package/dist/review/FreshContextVerifier.js +120 -0
  41. package/dist/review/FreshContextVerifier.js.map +1 -0
  42. package/dist/review/JsonLlmClient.d.ts +37 -0
  43. package/dist/review/JsonLlmClient.js +94 -0
  44. package/dist/review/JsonLlmClient.js.map +1 -0
  45. package/dist/review/LlmJudge.d.ts +61 -0
  46. package/dist/review/LlmJudge.js +167 -0
  47. package/dist/review/LlmJudge.js.map +1 -0
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/dist/workflow/BoundaryEnforcement.d.ts +60 -0
  51. package/dist/workflow/BoundaryEnforcement.js +182 -0
  52. package/dist/workflow/BoundaryEnforcement.js.map +1 -0
  53. package/dist/workflow/EngineeringStandards.js +19 -9
  54. package/dist/workflow/EngineeringStandards.js.map +1 -1
  55. package/dist/workflow/GateCatalog.js +12 -2
  56. package/dist/workflow/GateCatalog.js.map +1 -1
  57. package/dist/workflow/ProfileEnforcement.d.ts +7 -0
  58. package/dist/workflow/ProfileEnforcement.js +12 -0
  59. package/dist/workflow/ProfileEnforcement.js.map +1 -0
  60. package/dist/workflow/ReviewStore.d.ts +10 -0
  61. package/dist/workflow/ReviewStore.js.map +1 -1
  62. package/dist/workflow/SurfaceCoverage.d.ts +19 -0
  63. package/dist/workflow/SurfaceCoverage.js +57 -0
  64. package/dist/workflow/SurfaceCoverage.js.map +1 -0
  65. package/dist/workflow/gates/EnhancedGates.js +2 -0
  66. package/dist/workflow/gates/EnhancedGates.js.map +1 -1
  67. package/dist/workflow/gates/TestIntegrityGate.d.ts +51 -0
  68. package/dist/workflow/gates/TestIntegrityGate.js +175 -0
  69. package/dist/workflow/gates/TestIntegrityGate.js.map +1 -0
  70. package/dist/workflow/types.d.ts +1 -1
  71. package/docs/guides/DEVELOPMENT_WORKFLOW.md +28 -0
  72. package/docs/workflow/E2E_EXAMPLE.md +133 -0
  73. package/docs/workflow/README.md +6 -0
  74. package/docs/workflow/TEMPLATE_GUIDE.md +162 -0
  75. package/docs/workflow/templates/plan.md +26 -0
  76. package/docs/workflow/templates/spec.md +28 -0
  77. package/package.json +2 -1
@@ -0,0 +1,167 @@
1
+ // SCALE Engine — LLM-as-Judge (P1.4)
2
+ // Independent, advisory check of whether a diff actually satisfies the Spec's
3
+ // declared outcome / verificationSurface. The verdict is written into the
4
+ // review record as *advisory* evidence (decision K1): it never participates in
5
+ // the pass/fail decision and never blocks ship.
6
+ //
7
+ // Like ReflexionEngine, the judge runs an env-gated LLM when SCALE_LOCAL_MODEL
8
+ // is configured and otherwise falls back to a deterministic heuristic, so the
9
+ // default developer + CI flow stays offline, free and reproducible.
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { logger } from '../core/logger.js';
13
+ import { JsonLlmClient } from './JsonLlmClient.js';
14
+ const DEFAULT_PROMPT = {
15
+ id: 'spec-conformance',
16
+ version: 'v1',
17
+ system: 'You are an independent code-review judge. Decide only whether the diff actually achieves the stated outcome and exercises every declared verification surface. ' +
18
+ 'You are advisory: do not approve work that lacks evidence. Output strict JSON only.',
19
+ rubric: 'pass = every verification surface is plausibly addressed by the diff and no critical/high review finding contradicts the outcome. ' +
20
+ 'fail = the diff clearly does not achieve the outcome or leaves a declared surface unaddressed. ' +
21
+ 'uncertain = evidence is insufficient to decide.',
22
+ createdAt: 0,
23
+ };
24
+ /**
25
+ * Loads/persists the versioned judge prompt under `.scale/judges/<id>.json`
26
+ * (decision L1) so the rubric is auditable and can drift independently of code.
27
+ */
28
+ export class JudgePromptStore {
29
+ constructor(scaleDir = process.env.SCALE_DIR ?? '.scale') {
30
+ this.dir = join(scaleDir, 'judges');
31
+ }
32
+ load(id = DEFAULT_PROMPT.id) {
33
+ const file = join(this.dir, `${id}.json`);
34
+ if (existsSync(file)) {
35
+ try {
36
+ return JSON.parse(readFileSync(file, 'utf-8'));
37
+ }
38
+ catch {
39
+ logger.warn({ file }, 'JudgePromptStore: corrupt prompt file, using bundled default');
40
+ }
41
+ }
42
+ else if (id === DEFAULT_PROMPT.id) {
43
+ this.write({ ...DEFAULT_PROMPT, createdAt: Date.now() });
44
+ }
45
+ return { ...DEFAULT_PROMPT, createdAt: DEFAULT_PROMPT.createdAt || Date.now() };
46
+ }
47
+ write(record) {
48
+ if (!existsSync(this.dir))
49
+ mkdirSync(this.dir, { recursive: true });
50
+ writeFileSync(join(this.dir, `${record.id}.json`), JSON.stringify(record, null, 2), 'utf-8');
51
+ }
52
+ }
53
+ export class LlmJudge {
54
+ constructor(client = new JsonLlmClient(), promptStore = new JudgePromptStore()) {
55
+ this.client = client;
56
+ this.promptStore = promptStore;
57
+ }
58
+ async judge(input) {
59
+ const prompt = this.promptStore.load();
60
+ const promptVersion = `${prompt.id}.${prompt.version}`;
61
+ if (!this.client.isEnabled()) {
62
+ return this.heuristicVerdict(input, promptVersion);
63
+ }
64
+ try {
65
+ const { data, modelUsed } = await this.client.completeJson({
66
+ system: `${prompt.system}\n\nRubric: ${prompt.rubric}`,
67
+ user: this.buildUserPrompt(input),
68
+ });
69
+ return {
70
+ decision: normalizeDecision(data.decision),
71
+ confidence: clampConfidence(data.confidence),
72
+ rationale: (data.rationale ?? '').slice(0, 1000) || 'No rationale provided.',
73
+ unmetSurfaces: Array.isArray(data.unmetSurfaces) ? data.unmetSurfaces.slice(0, 50) : [],
74
+ modelUsed,
75
+ promptVersion,
76
+ advisory: true,
77
+ createdAt: Date.now(),
78
+ };
79
+ }
80
+ catch (err) {
81
+ logger.warn({ err }, 'LlmJudge: LLM call failed, falling back to heuristic');
82
+ return this.heuristicVerdict(input, promptVersion);
83
+ }
84
+ }
85
+ buildUserPrompt(input) {
86
+ return [
87
+ `Outcome: ${input.outcome ?? '(not declared)'}`,
88
+ '',
89
+ 'Verification surfaces (each must be addressed):',
90
+ ...(input.verificationSurface.length ? input.verificationSurface.map(s => `- ${s}`) : ['- (none declared)']),
91
+ '',
92
+ 'Review findings:',
93
+ `critical=${input.reviewFindings.critical} high=${input.reviewFindings.high} medium=${input.reviewFindings.medium} low=${input.reviewFindings.low}`,
94
+ '',
95
+ 'Diff summary:',
96
+ input.diffSummary.slice(0, 6000) || '(empty diff)',
97
+ '',
98
+ 'Output JSON: { "decision": "pass|fail|uncertain", "confidence": 0.0-1.0, "rationale": "...", "unmetSurfaces": ["..."] }',
99
+ ].join('\n');
100
+ }
101
+ /**
102
+ * Deterministic fallback: a surface is "unmet" when none of its significant
103
+ * tokens appear in the diff summary; any unmet surface or any critical/high
104
+ * review finding turns the advisory verdict negative.
105
+ */
106
+ heuristicVerdict(input, promptVersion) {
107
+ const haystack = input.diffSummary.toLowerCase();
108
+ const unmetSurfaces = input.verificationSurface.filter(surface => !surfaceMentioned(surface, haystack));
109
+ const blockingFindings = input.reviewFindings.critical + input.reviewFindings.high;
110
+ let decision;
111
+ let confidence;
112
+ let rationale;
113
+ if (blockingFindings > 0) {
114
+ decision = 'fail';
115
+ confidence = 0.6;
116
+ rationale = `${blockingFindings} critical/high review finding(s) contradict a "done" claim.`;
117
+ }
118
+ else if (input.verificationSurface.length === 0) {
119
+ decision = 'uncertain';
120
+ confidence = 0.3;
121
+ rationale = 'No verification surface declared; cannot judge conformance from the diff alone.';
122
+ }
123
+ else if (unmetSurfaces.length > 0) {
124
+ decision = 'uncertain';
125
+ confidence = 0.4;
126
+ rationale = `${unmetSurfaces.length}/${input.verificationSurface.length} verification surface(s) have no matching evidence in the diff.`;
127
+ }
128
+ else {
129
+ decision = 'pass';
130
+ confidence = 0.5;
131
+ rationale = 'All declared verification surfaces appear in the diff and no critical/high findings were raised.';
132
+ }
133
+ return {
134
+ decision,
135
+ confidence,
136
+ rationale,
137
+ unmetSurfaces,
138
+ modelUsed: 'heuristic',
139
+ promptVersion,
140
+ advisory: true,
141
+ createdAt: Date.now(),
142
+ };
143
+ }
144
+ }
145
+ function surfaceMentioned(surface, haystackLower) {
146
+ const tokens = surface
147
+ .toLowerCase()
148
+ .split(/[^a-z0-9]+/i)
149
+ .filter(token => token.length >= 4);
150
+ if (tokens.length === 0)
151
+ return haystackLower.includes(surface.toLowerCase());
152
+ return tokens.some(token => haystackLower.includes(token));
153
+ }
154
+ function normalizeDecision(value) {
155
+ const normalized = (value ?? '').toLowerCase();
156
+ if (normalized === 'pass')
157
+ return 'pass';
158
+ if (normalized === 'fail')
159
+ return 'fail';
160
+ return 'uncertain';
161
+ }
162
+ function clampConfidence(value) {
163
+ if (typeof value !== 'number' || Number.isNaN(value))
164
+ return 0.5;
165
+ return Math.max(0, Math.min(1, value));
166
+ }
167
+ //# sourceMappingURL=LlmJudge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LlmJudge.js","sourceRoot":"","sources":["../../src/review/LlmJudge.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export declare const FALLBACK_SCALE_ENGINE_VERSION = "0.48.0";
1
+ export declare const FALLBACK_SCALE_ENGINE_VERSION = "0.49.0";
2
2
  export declare function getScaleEngineVersion(): string;
3
3
  export declare const SCALE_ENGINE_VERSION: string;
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
- export const FALLBACK_SCALE_ENGINE_VERSION = '0.48.0';
2
+ export const FALLBACK_SCALE_ENGINE_VERSION = '0.49.0';
3
3
  export function getScaleEngineVersion() {
4
4
  try {
5
5
  const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
@@ -0,0 +1,60 @@
1
+ import type { SpecBoundaries } from '../artifact/types.js';
2
+ import { isEnforcedProfile } from './ProfileEnforcement.js';
3
+ export type BoundaryViolationKind = 'forbidden-touched' | 'outside-allowed';
4
+ export interface BoundaryViolation {
5
+ /** Repo-relative path of the offending changed file. */
6
+ file: string;
7
+ kind: BoundaryViolationKind;
8
+ /** For `forbidden-touched`: the forbidden glob that matched. */
9
+ matchedGlob?: string;
10
+ }
11
+ export interface BoundaryEnforcementReport {
12
+ declaredAllowed: number;
13
+ declaredForbidden: number;
14
+ changedFiles: number;
15
+ violations: BoundaryViolation[];
16
+ /** `true` under default/auto (warn only); `false` under an enforced profile. */
17
+ advisory: boolean;
18
+ }
19
+ export interface ConstraintCoverageReport {
20
+ declared: number;
21
+ covered: number;
22
+ /** Constraints with no verificationSurface token overlap. */
23
+ uncovered: string[];
24
+ advisory: boolean;
25
+ }
26
+ /**
27
+ * Profiles in which boundary/constraint findings hard-block Task completion
28
+ * (decision E1, shared with G23). `default`/`auto` stay advisory; a profile
29
+ * whose name is or ends with `full`/`ci`/`strict` enforces.
30
+ */
31
+ export declare function isEnforcedBoundaryProfile(profileName: string | undefined): boolean;
32
+ export { isEnforcedProfile };
33
+ /**
34
+ * Does a (normalised) changed-file path match a boundary glob?
35
+ * Supports exact paths, bare directory prefixes (`src` ⇒ `src/**`) and globs.
36
+ */
37
+ export declare function pathMatchesGlob(file: string, glob: string): boolean;
38
+ /**
39
+ * Compare the changed-file set against a Spec's executional boundaries.
40
+ * Returns `undefined` when there is nothing to enforce (no boundaries, or
41
+ * neither `files` nor `forbidden` declared).
42
+ */
43
+ export declare function evaluateBoundaries(changedFiles: Array<string | undefined | null> | undefined, boundaries: SpecBoundaries | undefined, enforced?: boolean): BoundaryEnforcementReport | undefined;
44
+ /**
45
+ * Check that every declared constraint is guarded by some verificationSurface
46
+ * item. Returns `undefined` when no constraints are declared.
47
+ */
48
+ export declare function evaluateConstraints(constraints: string[] | undefined, verificationSurface: string[] | undefined, enforced?: boolean): ConstraintCoverageReport | undefined;
49
+ /**
50
+ * Count the findings that block Task completion under an enforced profile:
51
+ * every boundary violation plus every unguarded constraint. Returns 0 when both
52
+ * reports are clean or undefined. The caller decides whether to act on the
53
+ * count based on the active profile (advisory reports are not blocked even when
54
+ * this count is non-zero).
55
+ */
56
+ export declare function countBoundaryBlockers(boundary: BoundaryEnforcementReport | undefined, constraint: ConstraintCoverageReport | undefined): number;
57
+ /** Render boundary warning lines (empty when there is nothing to warn about). */
58
+ export declare function formatBoundaryWarnings(report: BoundaryEnforcementReport | undefined): string[];
59
+ /** Render constraint-coverage warning lines (empty when fully covered). */
60
+ export declare function formatConstraintWarnings(report: ConstraintCoverageReport | undefined): string[];
@@ -0,0 +1,182 @@
1
+ // SCALE Engine - P0+ Boundary & Constraint enforcement
2
+ //
3
+ // Two checks layered on top of the P0 six-element Spec contract:
4
+ //
5
+ // 1. Boundary enforcement — compares the files a task actually changed against
6
+ // the Spec's executional `boundaries` (allowed `files` globs + explicitly
7
+ // `forbidden` globs). Touching a forbidden path, or editing a file outside
8
+ // the declared allow-list, is reported as a violation.
9
+ //
10
+ // 2. Constraint coverage — checks each declared `constraint` (an invariant that
11
+ // must not regress) against the Spec's `verificationSurface`. A constraint
12
+ // with no surface guarding it is a silent-regression risk.
13
+ //
14
+ // Enforcement is profile-gated (mirrors G23's E1 escalation): under `default`
15
+ // and `auto` both checks are advisory — violations are surfaced as warnings and
16
+ // recorded as evidence but never flip `passed` or block ship. Under an enforced
17
+ // profile (`full`/`ci`/`strict`), the same findings are blocking: any boundary
18
+ // violation or unguarded constraint stops Task completion. The `enforced` flag
19
+ // threaded into the evaluators only sets the report's `advisory` mode; the
20
+ // detection logic is identical, so a re-verify under `default` cannot silence a
21
+ // finding, only downgrade how it is reported.
22
+ import { isEnforcedProfile } from './ProfileEnforcement.js';
23
+ /**
24
+ * Profiles in which boundary/constraint findings hard-block Task completion
25
+ * (decision E1, shared with G23). `default`/`auto` stay advisory; a profile
26
+ * whose name is or ends with `full`/`ci`/`strict` enforces.
27
+ */
28
+ export function isEnforcedBoundaryProfile(profileName) {
29
+ return isEnforcedProfile(profileName);
30
+ }
31
+ export { isEnforcedProfile };
32
+ function norm(value) {
33
+ return value.replace(/\\/g, '/').trim().toLowerCase();
34
+ }
35
+ /**
36
+ * Translate a minimatch-style glob to an anchored, full-path RegExp.
37
+ * `**` matches across `/`, `*` matches within a segment, `?` matches one
38
+ * non-`/` char. All other regex metacharacters are escaped.
39
+ */
40
+ function globToRegExp(glob) {
41
+ const body = glob
42
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape specials, leaving * and ?
43
+ .replace(/\*\*/g, '\u0000') // globstar placeholder
44
+ .replace(/\*/g, '[^/]*')
45
+ .replace(/\u0000/g, '.*')
46
+ .replace(/\?/g, '[^/]');
47
+ return new RegExp(`^${body}$`);
48
+ }
49
+ /**
50
+ * Does a (normalised) changed-file path match a boundary glob?
51
+ * Supports exact paths, bare directory prefixes (`src` ⇒ `src/**`) and globs.
52
+ */
53
+ export function pathMatchesGlob(file, glob) {
54
+ const f = norm(file);
55
+ const g = norm(glob);
56
+ if (!f || !g)
57
+ return false;
58
+ if (f === g)
59
+ return true;
60
+ // Bare directory (no wildcards) acts as a recursive prefix.
61
+ if (!/[*?]/.test(g)) {
62
+ const dir = g.endsWith('/') ? g : `${g}/`;
63
+ if (f.startsWith(dir))
64
+ return true;
65
+ }
66
+ try {
67
+ return globToRegExp(g).test(f);
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ /**
74
+ * Compare the changed-file set against a Spec's executional boundaries.
75
+ * Returns `undefined` when there is nothing to enforce (no boundaries, or
76
+ * neither `files` nor `forbidden` declared).
77
+ */
78
+ export function evaluateBoundaries(changedFiles, boundaries, enforced = false) {
79
+ if (!boundaries)
80
+ return undefined;
81
+ const allowed = (boundaries.files ?? []).map(norm).filter(Boolean);
82
+ const forbidden = (boundaries.forbidden ?? []).map(norm).filter(Boolean);
83
+ if (allowed.length === 0 && forbidden.length === 0)
84
+ return undefined;
85
+ const files = [...new Set((changedFiles ?? []).map(f => norm(f ?? '')).filter(Boolean))];
86
+ const violations = [];
87
+ for (const file of files) {
88
+ const hitForbidden = forbidden.find(glob => pathMatchesGlob(file, glob));
89
+ if (hitForbidden) {
90
+ violations.push({ file, kind: 'forbidden-touched', matchedGlob: hitForbidden });
91
+ continue;
92
+ }
93
+ if (allowed.length > 0 && !allowed.some(glob => pathMatchesGlob(file, glob))) {
94
+ violations.push({ file, kind: 'outside-allowed' });
95
+ }
96
+ }
97
+ return {
98
+ declaredAllowed: allowed.length,
99
+ declaredForbidden: forbidden.length,
100
+ changedFiles: files.length,
101
+ violations,
102
+ advisory: !enforced,
103
+ };
104
+ }
105
+ function significantTokens(value) {
106
+ return [
107
+ ...new Set(norm(value)
108
+ .split(/[^a-z0-9]+/)
109
+ .filter(token => token.length >= 4)),
110
+ ];
111
+ }
112
+ /**
113
+ * A constraint is "covered" when at least one of its significant tokens appears
114
+ * in some verificationSurface item — i.e. there is a declared check that could
115
+ * catch a regression of that invariant.
116
+ */
117
+ function constraintCovered(constraint, surfaces) {
118
+ const tokens = significantTokens(constraint);
119
+ if (tokens.length === 0)
120
+ return false;
121
+ return surfaces.some(surface => tokens.some(token => surface.includes(token)));
122
+ }
123
+ /**
124
+ * Check that every declared constraint is guarded by some verificationSurface
125
+ * item. Returns `undefined` when no constraints are declared.
126
+ */
127
+ export function evaluateConstraints(constraints, verificationSurface, enforced = false) {
128
+ const declared = (constraints ?? []).map(c => c.trim()).filter(Boolean);
129
+ if (declared.length === 0)
130
+ return undefined;
131
+ const surfaces = (verificationSurface ?? []).map(norm).filter(Boolean);
132
+ const uncovered = declared.filter(constraint => !constraintCovered(constraint, surfaces));
133
+ return {
134
+ declared: declared.length,
135
+ covered: declared.length - uncovered.length,
136
+ uncovered,
137
+ advisory: !enforced,
138
+ };
139
+ }
140
+ /**
141
+ * Count the findings that block Task completion under an enforced profile:
142
+ * every boundary violation plus every unguarded constraint. Returns 0 when both
143
+ * reports are clean or undefined. The caller decides whether to act on the
144
+ * count based on the active profile (advisory reports are not blocked even when
145
+ * this count is non-zero).
146
+ */
147
+ export function countBoundaryBlockers(boundary, constraint) {
148
+ return (boundary?.violations.length ?? 0) + (constraint?.uncovered.length ?? 0);
149
+ }
150
+ /** Render boundary warning lines (empty when there is nothing to warn about). */
151
+ export function formatBoundaryWarnings(report) {
152
+ if (!report || report.violations.length === 0)
153
+ return [];
154
+ const tag = report.advisory ? '[WARN]' : '[BLOCKER]';
155
+ const mode = report.advisory ? 'advisory, not blocking' : 'blocking under enforced profile';
156
+ const lines = [
157
+ `${tag} boundary enforcement: ${report.violations.length} violation(s) (${mode})`,
158
+ ];
159
+ for (const v of report.violations) {
160
+ lines.push(v.kind === 'forbidden-touched'
161
+ ? ` [FORBIDDEN] ${v.file} (matched ${v.matchedGlob})`
162
+ : ` [OUTSIDE-ALLOWED] ${v.file}`);
163
+ }
164
+ lines.push(' Keep edits inside the Spec boundaries (or widen them in the Spec).');
165
+ return lines;
166
+ }
167
+ /** Render constraint-coverage warning lines (empty when fully covered). */
168
+ export function formatConstraintWarnings(report) {
169
+ if (!report || report.uncovered.length === 0)
170
+ return [];
171
+ const tag = report.advisory ? '[WARN]' : '[BLOCKER]';
172
+ const mode = report.advisory ? 'advisory, not blocking' : 'blocking under enforced profile';
173
+ const lines = [
174
+ `${tag} constraint coverage: ${report.covered}/${report.declared} guarded by verificationSurface (${mode})`,
175
+ ];
176
+ for (const constraint of report.uncovered) {
177
+ lines.push(` [UNGUARDED] ${constraint}`);
178
+ }
179
+ lines.push(' Add a verificationSurface item that would catch a regression of each constraint.');
180
+ return lines;
181
+ }
182
+ //# sourceMappingURL=BoundaryEnforcement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BoundaryEnforcement.js","sourceRoot":"","sources":["../../src/workflow/BoundaryEnforcement.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,iEAAiE;AACjE,EAAE;AACF,gFAAgF;AAChF,8EAA8E;AAC9E,+EAA+E;AAC/E,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,+EAA+E;AAC/E,+DAA+D;AAC/D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,gFAAgF;AAChF,+EAA+E;AAC/E,+EAA+E;AAC/E,2EAA2E;AAC3E,gFAAgF;AAChF,8CAA8C;AAG9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AA6B3D;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,WAA+B;IACvE,OAAO,iBAAiB,CAAC,WAAW,CAAC,CAAA;AACvC,CAAC;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAA;AAE5B,SAAS,IAAI,CAAC,KAAa;IACzB,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;AACvD,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,IAAI,GAAG,IAAI;SACd,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,mCAAmC;SACxE,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,uBAAuB;SAClD,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC;SACvB,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC;SACxB,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IACzB,OAAO,IAAI,MAAM,CAAC,IAAI,IAAI,GAAG,CAAC,CAAA;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,IAAY;IACxD,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAA;IACpB,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAA;IACpB,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAA;IAC1B,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACxB,4DAA4D;IAC5D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACpB,MAAM,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAA;QACzC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAA;IACpC,CAAC;IACD,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,YAA0D,EAC1D,UAAsC,EACtC,QAAQ,GAAG,KAAK;IAEhB,IAAI,CAAC,UAAU;QAAE,OAAO,SAAS,CAAA;IACjC,MAAM,OAAO,GAAG,CAAC,UAAU,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAClE,MAAM,SAAS,GAAG,CAAC,UAAU,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACxE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAEpE,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;IACxF,MAAM,UAAU,GAAwB,EAAE,CAAA;IAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,YAAY,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;QACxE,IAAI,YAAY,EAAE,CAAC;YACjB,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,mBAAmB,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC,CAAA;YAC/E,SAAQ;QACV,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;YAC7E,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAA;QACpD,CAAC;IACH,CAAC;IAED,OAAO;QACL,eAAe,EAAE,OAAO,CAAC,MAAM;QAC/B,iBAAiB,EAAE,SAAS,CAAC,MAAM;QACnC,YAAY,EAAE,KAAK,CAAC,MAAM;QAC1B,UAAU;QACV,QAAQ,EAAE,CAAC,QAAQ;KACpB,CAAA;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO;QACL,GAAG,IAAI,GAAG,CACR,IAAI,CAAC,KAAK,CAAC;aACR,KAAK,CAAC,YAAY,CAAC;aACnB,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,CACtC;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,UAAkB,EAAE,QAAkB;IAC/D,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;IAC5C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACrC,OAAO,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAChF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,WAAiC,EACjC,mBAAyC,EACzC,QAAQ,GAAG,KAAK;IAEhB,MAAM,QAAQ,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACvE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAC3C,MAAM,QAAQ,GAAG,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACtE,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,iBAAiB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAA;IACzF,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,MAAM;QACzB,OAAO,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM;QAC3C,SAAS;QACT,QAAQ,EAAE,CAAC,QAAQ;KACpB,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAA+C,EAC/C,UAAgD;IAEhD,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,EAAE,SAAS,CAAC,MAAM,IAAI,CAAC,CAAC,CAAA;AACjF,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,sBAAsB,CAAC,MAA6C;IAClF,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACxD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAA;IACpD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,iCAAiC,CAAA;IAC3F,MAAM,KAAK,GAAG;QACZ,GAAG,GAAG,0BAA0B,MAAM,CAAC,UAAU,CAAC,MAAM,kBAAkB,IAAI,GAAG;KAClF,CAAA;IACD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CACR,CAAC,CAAC,IAAI,KAAK,mBAAmB;YAC5B,CAAC,CAAC,kBAAkB,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,WAAW,GAAG;YACvD,CAAC,CAAC,wBAAwB,CAAC,CAAC,IAAI,EAAE,CACrC,CAAA;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAA;IACnF,OAAO,KAAK,CAAA;AACd,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,wBAAwB,CAAC,MAA4C;IACnF,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACvD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAA;IACpD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,iCAAiC,CAAA;IAC3F,MAAM,KAAK,GAAG;QACZ,GAAG,GAAG,yBAAyB,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,QAAQ,oCAAoC,IAAI,GAAG;KAC5G,CAAA;IACD,KAAK,MAAM,UAAU,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,kBAAkB,UAAU,EAAE,CAAC,CAAA;IAC5C,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,qFAAqF,CAAC,CAAA;IACjG,OAAO,KAAK,CAAA;AACd,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { createAstConfirmer } from '../guardrails/ast/confirmers.js';
3
4
  const DEFAULT_SOURCE_DIRECTORIES = ['src', 'app', 'packages', 'services', 'cmd', 'internal', 'pkg'];
4
5
  const DEFAULT_IGNORED_DIRECTORIES = [
5
6
  '.git',
@@ -379,6 +380,11 @@ function scanFile(projectDir, absolutePath, policy, frameworks) {
379
380
  const content = readFileSync(absolutePath, 'utf-8');
380
381
  const lines = content.split(/\r?\n/);
381
382
  const findings = [];
383
+ // AST confirmation layer (P1.1): parse once per file so the regex pre-filter
384
+ // hits below can be confirmed against the real AST. null = parse failed, in
385
+ // which case every confirmer falls back to the raw regex result.
386
+ const ext = extname(absolutePath);
387
+ const astConfirmer = createAstConfirmer(content, { jsx: ext === '.tsx' || ext === '.jsx' });
382
388
  if (lines.length > policy.maxFileLines) {
383
389
  findings.push({
384
390
  severity: 'warn',
@@ -397,12 +403,12 @@ function scanFile(projectDir, absolutePath, policy, frameworks) {
397
403
  if (startedInsideTemplateLiteral || isNonExecutablePatternLine(line))
398
404
  continue;
399
405
  const lineNumber = index + 1;
400
- findings.push(...scanLine(path, line, lineNumber, policy, frameworks));
406
+ findings.push(...scanLine(path, line, lineNumber, policy, frameworks, astConfirmer));
401
407
  }
402
- findings.push(...findEmptyCatchBlocks(path, lines));
408
+ findings.push(...findEmptyCatchBlocks(path, lines, astConfirmer));
403
409
  return dedupeFindings(findings);
404
410
  }
405
- function scanLine(path, line, lineNumber, policy, frameworks) {
411
+ function scanLine(path, line, lineNumber, policy, frameworks, ast) {
406
412
  const findings = [];
407
413
  const sensitiveMatcher = sensitiveFieldPattern(policy);
408
414
  const evidence = line.trim().slice(0, 160);
@@ -467,7 +473,7 @@ function scanLine(path, line, lineNumber, policy, frameworks) {
467
473
  fix: 'Use text rendering or sanitize trusted HTML with an approved sanitizer.',
468
474
  });
469
475
  }
470
- if (/\beval\s*\(|new\s+Function\s*\(/.test(line)) {
476
+ if (/\beval\s*\(|new\s+Function\s*\(/.test(line) && (!ast || ast.hasUnsafeCodeExecution(lineNumber))) {
471
477
  findings.push({
472
478
  severity: 'fail',
473
479
  category: 'security',
@@ -479,7 +485,7 @@ function scanLine(path, line, lineNumber, policy, frameworks) {
479
485
  fix: 'Replace eval or Function with a typed parser, dispatch table, or safe interpreter.',
480
486
  });
481
487
  }
482
- if (/^\s*(?:\/\/|\/\*)\s*@ts-ignore\b/.test(line)) {
488
+ if (/^\s*(?:\/\/|\/\*)\s*@ts-ignore\b/.test(line) && (!ast || ast.hasTsIgnore(lineNumber))) {
483
489
  findings.push({
484
490
  severity: 'fail',
485
491
  category: 'code-quality',
@@ -491,7 +497,7 @@ function scanLine(path, line, lineNumber, policy, frameworks) {
491
497
  fix: 'Fix the type boundary or use a narrow typed adapter with a documented reason.',
492
498
  });
493
499
  }
494
- if (/\bas\s+any\b|:\s*any\b|<any\b|Array<any>|Promise<any>|Record<[^>]+,\s*any>/.test(line)) {
500
+ if (/\bas\s+any\b|:\s*any\b|<any\b|Array<any>|Promise<any>|Record<[^>]+,\s*any>/.test(line) && (!ast || ast.hasAnyType(lineNumber))) {
495
501
  findings.push({
496
502
  severity: 'warn',
497
503
  category: 'code-quality',
@@ -529,12 +535,16 @@ function scanLine(path, line, lineNumber, policy, frameworks) {
529
535
  }
530
536
  return findings;
531
537
  }
532
- function findEmptyCatchBlocks(path, lines) {
538
+ function findEmptyCatchBlocks(path, lines, ast) {
533
539
  const findings = [];
540
+ // AST confirmation: only emit when the catch block genuinely has zero
541
+ // statements. Falls back to the regex result when the file did not parse.
542
+ const confirm = (line) => !ast || ast.hasEmptyCatch(line);
534
543
  for (let index = 0; index < lines.length; index += 1) {
535
544
  const line = lines[index];
536
545
  if (/catch\s*(?:\([^)]*\))?\s*\{\s*(?:\/\*.*?\*\/|\/\/.*)?\s*\}/.test(line)) {
537
- findings.push(emptyCatchFinding(path, index + 1, line));
546
+ if (confirm(index + 1))
547
+ findings.push(emptyCatchFinding(path, index + 1, line));
538
548
  continue;
539
549
  }
540
550
  if (!/catch\s*(?:\([^)]*\))?\s*\{\s*$/.test(line))
@@ -543,7 +553,7 @@ function findEmptyCatchBlocks(path, lines) {
543
553
  const trimmed = next.trim();
544
554
  if (trimmed === '' || trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
545
555
  continue;
546
- if (/^}\s*[),;]?$/.test(trimmed))
556
+ if (/^}\s*[),;]?$/.test(trimmed) && confirm(index + 1))
547
557
  findings.push(emptyCatchFinding(path, index + 1, line));
548
558
  break;
549
559
  }