@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.
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/agents/evidenceDiscipline.d.ts +7 -0
- package/dist/agents/evidenceDiscipline.js +21 -0
- package/dist/agents/evidenceDiscipline.js.map +1 -0
- package/dist/agents/profiles.js +8 -1
- package/dist/agents/profiles.js.map +1 -1
- package/dist/agents/types.d.ts +1 -0
- package/dist/artifact/types.d.ts +59 -0
- package/dist/artifact/types.js.map +1 -1
- package/dist/cli/cortexCommands.d.ts +36 -0
- package/dist/cli/cortexCommands.js +76 -4
- package/dist/cli/cortexCommands.js.map +1 -1
- package/dist/cli/evalCommands.js +12 -1
- package/dist/cli/evalCommands.js.map +1 -1
- package/dist/cli/phaseCommands.d.ts +53 -1
- package/dist/cli/phaseCommands.js +317 -22
- package/dist/cli/phaseCommands.js.map +1 -1
- package/dist/cortex/InstinctStore.d.ts +32 -1
- package/dist/cortex/InstinctStore.js +235 -42
- package/dist/cortex/InstinctStore.js.map +1 -1
- package/dist/cortex/InstinctValidation.d.ts +9 -0
- package/dist/cortex/InstinctValidation.js +55 -0
- package/dist/cortex/InstinctValidation.js.map +1 -0
- package/dist/cortex/SessionInjector.js +13 -6
- package/dist/cortex/SessionInjector.js.map +1 -1
- package/dist/eval/BenchmarkPublisher.d.ts +2 -0
- package/dist/eval/BenchmarkPublisher.js +43 -0
- package/dist/eval/BenchmarkPublisher.js.map +1 -1
- package/dist/guardrails/ast/confirmers.d.ts +18 -0
- package/dist/guardrails/ast/confirmers.js +69 -0
- package/dist/guardrails/ast/confirmers.js.map +1 -0
- package/dist/guardrails/ast/parse.d.ts +20 -0
- package/dist/guardrails/ast/parse.js +51 -0
- package/dist/guardrails/ast/parse.js.map +1 -0
- package/dist/output/HTMLDocumentRenderer.d.ts +9 -0
- package/dist/output/HTMLDocumentRenderer.js +19 -0
- package/dist/output/HTMLDocumentRenderer.js.map +1 -1
- package/dist/review/FreshContextVerifier.d.ts +35 -0
- package/dist/review/FreshContextVerifier.js +120 -0
- package/dist/review/FreshContextVerifier.js.map +1 -0
- package/dist/review/JsonLlmClient.d.ts +37 -0
- package/dist/review/JsonLlmClient.js +94 -0
- package/dist/review/JsonLlmClient.js.map +1 -0
- package/dist/review/LlmJudge.d.ts +61 -0
- package/dist/review/LlmJudge.js +167 -0
- package/dist/review/LlmJudge.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workflow/BoundaryEnforcement.d.ts +60 -0
- package/dist/workflow/BoundaryEnforcement.js +182 -0
- package/dist/workflow/BoundaryEnforcement.js.map +1 -0
- package/dist/workflow/EngineeringStandards.js +19 -9
- package/dist/workflow/EngineeringStandards.js.map +1 -1
- package/dist/workflow/GateCatalog.js +12 -2
- package/dist/workflow/GateCatalog.js.map +1 -1
- package/dist/workflow/ProfileEnforcement.d.ts +7 -0
- package/dist/workflow/ProfileEnforcement.js +12 -0
- package/dist/workflow/ProfileEnforcement.js.map +1 -0
- package/dist/workflow/ReviewStore.d.ts +10 -0
- package/dist/workflow/ReviewStore.js.map +1 -1
- package/dist/workflow/SurfaceCoverage.d.ts +19 -0
- package/dist/workflow/SurfaceCoverage.js +57 -0
- package/dist/workflow/SurfaceCoverage.js.map +1 -0
- package/dist/workflow/gates/EnhancedGates.js +2 -0
- package/dist/workflow/gates/EnhancedGates.js.map +1 -1
- package/dist/workflow/gates/TestIntegrityGate.d.ts +51 -0
- package/dist/workflow/gates/TestIntegrityGate.js +175 -0
- package/dist/workflow/gates/TestIntegrityGate.js.map +1 -0
- package/dist/workflow/types.d.ts +1 -1
- package/docs/guides/DEVELOPMENT_WORKFLOW.md +28 -0
- package/docs/workflow/E2E_EXAMPLE.md +133 -0
- package/docs/workflow/README.md +6 -0
- package/docs/workflow/TEMPLATE_GUIDE.md +162 -0
- package/docs/workflow/templates/plan.md +26 -0
- package/docs/workflow/templates/spec.md +28 -0
- 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
package/dist/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
export const FALLBACK_SCALE_ENGINE_VERSION = '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":"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"}
|
|
@@ -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
|
-
|
|
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
|
}
|