@deftai/directive-core 0.96.0 → 0.98.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/dist/authz/classify.js +291 -0
- package/dist/cache/archive.js +10 -4
- package/dist/check/cached-orchestrator.d.ts +5 -0
- package/dist/check/cached-orchestrator.js +18 -1
- package/dist/check/gate-lists.d.ts +20 -0
- package/dist/check/gate-lists.js +54 -9
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +1 -1
- package/dist/check/orchestrator.d.ts +4 -0
- package/dist/check/orchestrator.js +4 -0
- package/dist/consumer-check-contract/evaluate.d.ts +124 -0
- package/dist/consumer-check-contract/evaluate.js +699 -0
- package/dist/consumer-check-contract/index.d.ts +5 -0
- package/dist/consumer-check-contract/index.js +5 -0
- package/dist/delivery-attempt/disk-begin.d.ts +51 -0
- package/dist/delivery-attempt/disk-begin.js +68 -0
- package/dist/delivery-attempt/evaluate.d.ts +26 -0
- package/dist/delivery-attempt/evaluate.js +443 -0
- package/dist/delivery-attempt/fingerprint.d.ts +32 -0
- package/dist/delivery-attempt/fingerprint.js +100 -0
- package/dist/delivery-attempt/handoff.d.ts +25 -0
- package/dist/delivery-attempt/handoff.js +102 -0
- package/dist/delivery-attempt/index.d.ts +17 -0
- package/dist/delivery-attempt/index.js +17 -0
- package/dist/delivery-attempt/ledger.d.ts +169 -0
- package/dist/delivery-attempt/ledger.js +758 -0
- package/dist/delivery-attempt/material-delta.d.ts +38 -0
- package/dist/delivery-attempt/material-delta.js +126 -0
- package/dist/delivery-attempt/types.d.ts +210 -0
- package/dist/delivery-attempt/types.js +77 -0
- package/dist/doctor/checks.d.ts +7 -0
- package/dist/doctor/checks.js +83 -0
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.js +12 -0
- package/dist/doctor/openclaw-soft-rebind.d.ts +26 -0
- package/dist/doctor/openclaw-soft-rebind.js +164 -0
- package/dist/hooks/dispatcher.d.ts +7 -1
- package/dist/hooks/dispatcher.js +117 -13
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/init-deposit/gitignore.js +7 -0
- package/dist/init-deposit/hygiene.d.ts +70 -1
- package/dist/init-deposit/hygiene.js +582 -8
- package/dist/init-deposit/init-deposit.js +5 -0
- package/dist/init-deposit/refresh.js +3 -0
- package/dist/init-deposit/scaffold.js +277 -4
- package/dist/init-deposit/skill-discovery-deposit.js +15 -0
- package/dist/policy/check-resume.d.ts +72 -0
- package/dist/policy/check-resume.js +253 -0
- package/dist/policy/coverage-check-resume-presets.d.ts +46 -0
- package/dist/policy/coverage-check-resume-presets.js +228 -0
- package/dist/policy/coverage-debt.d.ts +76 -0
- package/dist/policy/coverage-debt.js +262 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +50 -21
- package/dist/pr-merge-readiness/ci-gate.d.ts +29 -1
- package/dist/pr-merge-readiness/ci-gate.js +191 -24
- package/dist/pr-merge-readiness/compute.js +10 -1
- package/dist/pr-merge-readiness/index.d.ts +2 -1
- package/dist/pr-merge-readiness/index.js +2 -1
- package/dist/pr-merge-readiness/output.js +14 -0
- package/dist/pr-merge-readiness/platform-status.d.ts +29 -0
- package/dist/pr-merge-readiness/platform-status.js +49 -0
- package/dist/pr-watch/constants.d.ts +10 -0
- package/dist/pr-watch/constants.js +12 -1
- package/dist/pr-watch/main.js +16 -1
- package/dist/pr-watch/probe.js +13 -1
- package/dist/pr-watch/types.d.ts +3 -2
- package/dist/pr-watch/watch.js +14 -7
- package/dist/release/auto-hatch.d.ts +114 -0
- package/dist/release/auto-hatch.js +301 -0
- package/dist/release/coverage-debt-ledger.d.ts +22 -0
- package/dist/release/coverage-debt-ledger.js +157 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/pipeline.js +164 -12
- package/dist/release/suite-stamp.d.ts +44 -0
- package/dist/release/suite-stamp.js +133 -0
- package/dist/release/types.d.ts +19 -0
- package/dist/scope-provenance/digest.d.ts +67 -0
- package/dist/scope-provenance/digest.js +188 -0
- package/dist/scope-provenance/evaluate.d.ts +82 -0
- package/dist/scope-provenance/evaluate.js +528 -0
- package/dist/scope-provenance/index.d.ts +6 -0
- package/dist/scope-provenance/index.js +6 -0
- package/dist/session/compact-ritual.d.ts +96 -0
- package/dist/session/compact-ritual.js +237 -0
- package/dist/session/compact-ritual.spec.d.ts +2 -0
- package/dist/session/compact-ritual.spec.js +21 -0
- package/dist/session/coverage-check-resume-nudge.d.ts +34 -0
- package/dist/session/coverage-check-resume-nudge.js +66 -0
- package/dist/session/index.d.ts +3 -0
- package/dist/session/index.js +3 -0
- package/dist/session/openclaw-soft-rebind-deposit.d.ts +46 -0
- package/dist/session/openclaw-soft-rebind-deposit.js +165 -0
- package/dist/session/session-start.js +21 -0
- package/dist/test-boundary/evaluate.d.ts +54 -0
- package/dist/test-boundary/evaluate.js +368 -0
- package/dist/test-boundary/index.d.ts +6 -0
- package/dist/test-boundary/index.js +6 -0
- package/dist/test-boundary/policy.d.ts +52 -0
- package/dist/test-boundary/policy.js +182 -0
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +15 -1
- package/dist/triage/classify/label-mirror.d.ts +31 -1
- package/dist/triage/classify/label-mirror.js +78 -6
- package/dist/triage/help/registry-data.d.ts +6 -6
- package/dist/triage/help/registry-data.js +12 -3
- package/dist/vbrief-activate/activate.js +22 -6
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +54 -0
- package/dist/xbrief/styles.js +33 -17
- package/package.json +7 -3
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify:test-boundary evaluation (#3145).
|
|
3
|
+
*
|
|
4
|
+
* Rejects recognized test artifacts under production roots and production
|
|
5
|
+
* references to test/fixture roots unless allowlisted or classified as
|
|
6
|
+
* production-liveness. Three-state exit: 0 clean / 1 violation / 2 config.
|
|
7
|
+
*/
|
|
8
|
+
import { type TestBoundaryPolicy } from "./policy.js";
|
|
9
|
+
export type TestBoundaryViolationKind = "test-under-source-root" | "production-references-test-root";
|
|
10
|
+
export interface TestBoundaryFinding {
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly kind: TestBoundaryViolationKind;
|
|
13
|
+
readonly detail: string;
|
|
14
|
+
readonly remediation: string;
|
|
15
|
+
}
|
|
16
|
+
export interface TestBoundaryResult {
|
|
17
|
+
readonly exitCode: 0 | 1 | 2;
|
|
18
|
+
readonly findings: readonly TestBoundaryFinding[];
|
|
19
|
+
readonly message: string;
|
|
20
|
+
readonly policy: TestBoundaryPolicy | null;
|
|
21
|
+
}
|
|
22
|
+
export interface TestBoundaryOptions {
|
|
23
|
+
readonly policyPath?: string | null;
|
|
24
|
+
/** Inject policy for pure unit tests (skips disk load). */
|
|
25
|
+
readonly policy?: TestBoundaryPolicy;
|
|
26
|
+
/** Inject file list for pure unit tests. */
|
|
27
|
+
readonly files?: readonly string[];
|
|
28
|
+
/** Inject file contents for reference scanning (posix path -> text). */
|
|
29
|
+
readonly fileContents?: ReadonlyMap<string, string>;
|
|
30
|
+
/** Force enforcement mode override. */
|
|
31
|
+
readonly enforce?: boolean;
|
|
32
|
+
readonly quiet?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Glob match with double-star as zero-or-more path segments.
|
|
36
|
+
* Plain fnmatchCase treats double-star as two greedy stars and fails nested
|
|
37
|
+
* colocated paths (e.g. packages/cli/src/foo/bar.test.ts vs packages star globs).
|
|
38
|
+
*/
|
|
39
|
+
export declare function matchPolicyGlob(relPath: string, pattern: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Match repo-relative path against a policy root glob.
|
|
42
|
+
* Supports foo/**-style prefixes, packages star-src globs, and basename patterns.
|
|
43
|
+
*/
|
|
44
|
+
export declare function matchesRootGlob(relPath: string, rootGlob: string): boolean;
|
|
45
|
+
/** True when basename/path matches a test-file pattern (fnmatch / ** globs). */
|
|
46
|
+
export declare function matchesTestFilePattern(relPath: string, patterns: readonly string[]): boolean;
|
|
47
|
+
/** Recognise conventional test basenames without full glob (fast path). */
|
|
48
|
+
export declare function isRecognizedTestBasename(relPath: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Evaluate test/source boundary for a project.
|
|
51
|
+
* Pure when `policy` + `files` (+ optional `fileContents`) are injected.
|
|
52
|
+
*/
|
|
53
|
+
export declare function evaluateTestBoundary(projectRoot: string, options?: TestBoundaryOptions): TestBoundaryResult;
|
|
54
|
+
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify:test-boundary evaluation (#3145).
|
|
3
|
+
*
|
|
4
|
+
* Rejects recognized test artifacts under production roots and production
|
|
5
|
+
* references to test/fixture roots unless allowlisted or classified as
|
|
6
|
+
* production-liveness. Three-state exit: 0 clean / 1 violation / 2 config.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { basename, resolve } from "node:path";
|
|
11
|
+
import { GitCommandError, GitNotFoundError } from "../encoding/git.js";
|
|
12
|
+
import { fnmatchCase } from "../encoding/text.js";
|
|
13
|
+
import { loadTestBoundaryPolicy, } from "./policy.js";
|
|
14
|
+
function gitTrackedFiles(projectRoot) {
|
|
15
|
+
const result = spawnSync("git", ["ls-files"], {
|
|
16
|
+
cwd: projectRoot,
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
19
|
+
});
|
|
20
|
+
if (result.error !== undefined) {
|
|
21
|
+
const e = result.error;
|
|
22
|
+
if (e.code === "ENOENT") {
|
|
23
|
+
throw new GitNotFoundError("'git' executable not found on PATH");
|
|
24
|
+
}
|
|
25
|
+
throw new GitCommandError(`git ls-files failed: ${String(e.message)}`);
|
|
26
|
+
}
|
|
27
|
+
if ((result.status ?? 1) !== 0) {
|
|
28
|
+
const stderr = String(result.stderr ?? "").trim();
|
|
29
|
+
throw new GitCommandError(`git ls-files exited ${result.status ?? 1}${stderr ? `: ${stderr}` : ""}`);
|
|
30
|
+
}
|
|
31
|
+
return (result.stdout ?? "")
|
|
32
|
+
.split("\n")
|
|
33
|
+
.map((l) => l.replace(/\r$/, "").replace(/\\/g, "/"))
|
|
34
|
+
.filter((l) => l.trim().length > 0);
|
|
35
|
+
}
|
|
36
|
+
/** Normalize a root glob like `src/**` or `infra/**` to a path prefix matcher. */
|
|
37
|
+
function rootPrefix(rootGlob) {
|
|
38
|
+
return rootGlob.replace(/\/\*\*$/, "/").replace(/\*\*$/, "");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Glob match with double-star as zero-or-more path segments.
|
|
42
|
+
* Plain fnmatchCase treats double-star as two greedy stars and fails nested
|
|
43
|
+
* colocated paths (e.g. packages/cli/src/foo/bar.test.ts vs packages star globs).
|
|
44
|
+
*/
|
|
45
|
+
export function matchPolicyGlob(relPath, pattern) {
|
|
46
|
+
const posix = relPath.replace(/\\/g, "/");
|
|
47
|
+
const g = pattern.replace(/\\/g, "/");
|
|
48
|
+
if (fnmatchCase(posix, g))
|
|
49
|
+
return true;
|
|
50
|
+
// Translate ** → «any path including empty», * → «any segment chars»
|
|
51
|
+
let reSrc = "";
|
|
52
|
+
for (let i = 0; i < g.length;) {
|
|
53
|
+
if (g.startsWith("**/", i)) {
|
|
54
|
+
reSrc += "(?:.*/)?";
|
|
55
|
+
i += 3;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (g.startsWith("**", i)) {
|
|
59
|
+
reSrc += ".*";
|
|
60
|
+
i += 2;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const c = g.charAt(i);
|
|
64
|
+
i += 1;
|
|
65
|
+
if (c === "*") {
|
|
66
|
+
reSrc += "[^/]*";
|
|
67
|
+
}
|
|
68
|
+
else if (c === "?") {
|
|
69
|
+
reSrc += "[^/]";
|
|
70
|
+
}
|
|
71
|
+
else if ("\\.[]{}()+-^$|".includes(c)) {
|
|
72
|
+
reSrc += `\\${c}`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
reSrc += c;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return new RegExp(`^${reSrc}$`).test(posix);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Match repo-relative path against a policy root glob.
|
|
87
|
+
* Supports foo/**-style prefixes, packages star-src globs, and basename patterns.
|
|
88
|
+
*/
|
|
89
|
+
export function matchesRootGlob(relPath, rootGlob) {
|
|
90
|
+
const posix = relPath.replace(/\\/g, "/");
|
|
91
|
+
const g = rootGlob.replace(/\\/g, "/");
|
|
92
|
+
if (matchPolicyGlob(posix, g))
|
|
93
|
+
return true;
|
|
94
|
+
// Prefix form: src/** matches src/foo.py
|
|
95
|
+
if (g.endsWith("/**")) {
|
|
96
|
+
const prefix = g.slice(0, -3);
|
|
97
|
+
if (!prefix.includes("*")) {
|
|
98
|
+
return posix === prefix || posix.startsWith(`${prefix}/`);
|
|
99
|
+
}
|
|
100
|
+
// packages/*/src/** — match prefix with matchPolicyGlob against path or ancestors
|
|
101
|
+
if (matchPolicyGlob(posix, `${prefix}/**`) || matchPolicyGlob(posix, prefix)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
// Any file under a matching prefix directory
|
|
105
|
+
const parts = posix.split("/");
|
|
106
|
+
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
107
|
+
const candidate = parts.slice(0, depth).join("/");
|
|
108
|
+
if (matchPolicyGlob(candidate, prefix)) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (g.includes("*")) {
|
|
114
|
+
return matchPolicyGlob(posix, g) || matchPolicyGlob(posix, g.endsWith("/**") ? g : `${g}/**`);
|
|
115
|
+
}
|
|
116
|
+
const prefix = rootPrefix(g);
|
|
117
|
+
return posix === prefix.replace(/\/$/, "") || posix.startsWith(prefix);
|
|
118
|
+
}
|
|
119
|
+
/** True when basename/path matches a test-file pattern (fnmatch / ** globs). */
|
|
120
|
+
export function matchesTestFilePattern(relPath, patterns) {
|
|
121
|
+
const posix = relPath.replace(/\\/g, "/");
|
|
122
|
+
const base = basename(posix);
|
|
123
|
+
for (const pat of patterns) {
|
|
124
|
+
const p = pat.replace(/\\/g, "/");
|
|
125
|
+
if (matchPolicyGlob(posix, p) ||
|
|
126
|
+
matchPolicyGlob(base, p) ||
|
|
127
|
+
fnmatchCase(base, basename(p)) ||
|
|
128
|
+
fnmatchCase(posix, p)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
function isUnderAnyRoot(relPath, roots) {
|
|
135
|
+
return roots.some((r) => matchesRootGlob(relPath, r));
|
|
136
|
+
}
|
|
137
|
+
function isAllowListed(relPath, allow) {
|
|
138
|
+
const posix = relPath.replace(/\\/g, "/");
|
|
139
|
+
for (const entry of allow) {
|
|
140
|
+
if (matchPolicyGlob(posix, entry.path) || matchesRootGlob(posix, entry.path)) {
|
|
141
|
+
return entry;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
/** Recognise conventional test basenames without full glob (fast path). */
|
|
147
|
+
export function isRecognizedTestBasename(relPath) {
|
|
148
|
+
const b = basename(relPath);
|
|
149
|
+
if (/^test_.+\.py$/i.test(b) || /.+_test\.py$/i.test(b))
|
|
150
|
+
return true;
|
|
151
|
+
if (/.+Tests?\.cs$/i.test(b))
|
|
152
|
+
return true;
|
|
153
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/i.test(b))
|
|
154
|
+
return true;
|
|
155
|
+
if (/.+_test\.go$/i.test(b))
|
|
156
|
+
return true;
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
function productionReferenceRemediation() {
|
|
160
|
+
return ("Move the reference under a declared test root, add a narrow allow entry " +
|
|
161
|
+
"with kind production-liveness (health/canary only), or set " +
|
|
162
|
+
"productionMayReferenceTestRoots=true after review. See content/docs/test-boundary.md.");
|
|
163
|
+
}
|
|
164
|
+
function testUnderSourceRemediation() {
|
|
165
|
+
return ("Move the test artifact under a declared test root (e.g. tests/**), " +
|
|
166
|
+
"or add a reviewed allow entry in plan.policy.testBoundary.allow / " +
|
|
167
|
+
".deft/test-boundary.policy.json. See content/docs/test-boundary.md (#3145).");
|
|
168
|
+
}
|
|
169
|
+
function scanProductionReferences(relPath, content, policy) {
|
|
170
|
+
// Only scan non-test production-ish paths
|
|
171
|
+
if (matchesTestFilePattern(relPath, policy.testFilePatterns) ||
|
|
172
|
+
isRecognizedTestBasename(relPath)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
if (isUnderAnyRoot(relPath, policy.testRoots) || isUnderAnyRoot(relPath, policy.fixtureRoots)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
// Prefer production roots; also scan infra/deploy scripts (path segments only —
|
|
179
|
+
// do not match free-text tokens inside xbrief story slugs).
|
|
180
|
+
const underSource = isUnderAnyRoot(relPath, policy.sourceRoots);
|
|
181
|
+
const looksLikeDeploy = /(^|\/)(infra|deploy|deployment|terraform|bicep|cloudformation)(\/|$)/i.test(relPath) ||
|
|
182
|
+
/(^|\/)\.github\/workflows\//i.test(relPath) ||
|
|
183
|
+
/(^|\/)Dockerfile(\.|$)/i.test(relPath) ||
|
|
184
|
+
/(^|\/).*pipeline.*\.(ya?ml|json|sh|ps1)$/i.test(relPath);
|
|
185
|
+
if (!underSource && !looksLikeDeploy) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const rootsToForbid = [...policy.testRoots, ...policy.fixtureRoots];
|
|
189
|
+
for (const root of rootsToForbid) {
|
|
190
|
+
// Only path-shaped roots (e.g. tests/**, tests/fixtures/**) — skip basename globs
|
|
191
|
+
// like **/*_test.go which are not importable path prefixes.
|
|
192
|
+
if (!root.includes("/") && !root.endsWith("/**")) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const needle = rootPrefix(root).replace(/\/$/, "");
|
|
196
|
+
// Require at least "tests" length and a path separator in references so bare
|
|
197
|
+
// English "test" / CI job names do not false-positive.
|
|
198
|
+
if (needle.length < 4)
|
|
199
|
+
continue;
|
|
200
|
+
if (needle === "test" || needle.endsWith("/test")) {
|
|
201
|
+
// Bare test/ is too common in prose; only match test/fixtures-style
|
|
202
|
+
if (!/fixture/i.test(root))
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Path-like only: tests/…, "tests/foo", tests\foo — not the word "tests" alone
|
|
206
|
+
const patterns = [
|
|
207
|
+
new RegExp(`(?:^|["'\`\\s=,:(])${escapeRegExp(needle)}/`),
|
|
208
|
+
new RegExp(`(?:^|["'\`\\s=,:(])${escapeRegExp(needle.replace(/\//g, "\\\\"))}\\\\`),
|
|
209
|
+
];
|
|
210
|
+
for (const re of patterns) {
|
|
211
|
+
if (re.test(content)) {
|
|
212
|
+
return {
|
|
213
|
+
path: relPath,
|
|
214
|
+
kind: "production-references-test-root",
|
|
215
|
+
detail: `references test/fixture root '${needle}/' while productionMayReferenceTestRoots is false`,
|
|
216
|
+
remediation: productionReferenceRemediation(),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
function escapeRegExp(s) {
|
|
224
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
225
|
+
}
|
|
226
|
+
function configError(message) {
|
|
227
|
+
return { exitCode: 2, findings: [], message, policy: null };
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Evaluate test/source boundary for a project.
|
|
231
|
+
* Pure when `policy` + `files` (+ optional `fileContents`) are injected.
|
|
232
|
+
*/
|
|
233
|
+
export function evaluateTestBoundary(projectRoot, options = {}) {
|
|
234
|
+
const root = resolve(projectRoot);
|
|
235
|
+
let policy;
|
|
236
|
+
try {
|
|
237
|
+
policy = options.policy ?? loadTestBoundaryPolicy(root, { policyPath: options.policyPath });
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
return configError(`verify_test_boundary: policy load failed -- ${String(err.message)}\n` +
|
|
241
|
+
" Recovery: fix .deft/test-boundary.policy.json or plan.policy.testBoundary, or omit for defaults.");
|
|
242
|
+
}
|
|
243
|
+
if (options.enforce === true) {
|
|
244
|
+
policy = { ...policy, enforcementMode: "enforce" };
|
|
245
|
+
}
|
|
246
|
+
else if (options.enforce === false) {
|
|
247
|
+
policy = { ...policy, enforcementMode: "warn" };
|
|
248
|
+
}
|
|
249
|
+
let files;
|
|
250
|
+
try {
|
|
251
|
+
files = options.files
|
|
252
|
+
? [...options.files].map((f) => f.replace(/\\/g, "/"))
|
|
253
|
+
: gitTrackedFiles(root);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
if (err instanceof GitNotFoundError) {
|
|
257
|
+
return configError("verify_test_boundary: 'git' executable not found on PATH.\n" +
|
|
258
|
+
" Recovery: install git or run inside a git working tree.");
|
|
259
|
+
}
|
|
260
|
+
if (err instanceof GitCommandError) {
|
|
261
|
+
const msg = err.message.toLowerCase();
|
|
262
|
+
// Only skip clean when the project is not a git repo (greenfield smoke).
|
|
263
|
+
// Other git operational failures fail closed (exit 2) — Greptile conf=0.
|
|
264
|
+
if (msg.includes("not a git repository") ||
|
|
265
|
+
msg.includes("outside repository") ||
|
|
266
|
+
msg.includes("not a git working tree")) {
|
|
267
|
+
return {
|
|
268
|
+
exitCode: 0,
|
|
269
|
+
findings: [],
|
|
270
|
+
message: `verify_test_boundary: skipped -- not a git working tree (${err.message}). ` +
|
|
271
|
+
"Initialize git, or inject files for offline evaluation (#3145).",
|
|
272
|
+
policy,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return configError(`verify_test_boundary: git failed -- ${err.message}\n` +
|
|
276
|
+
" Recovery: ensure --project-root points at a healthy git working tree.");
|
|
277
|
+
}
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
const findings = [];
|
|
281
|
+
for (const rel of files) {
|
|
282
|
+
const allow = isAllowListed(rel, policy.allow);
|
|
283
|
+
if (allow !== null) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const isTest = matchesTestFilePattern(rel, policy.testFilePatterns) || isRecognizedTestBasename(rel);
|
|
287
|
+
if (!isTest) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// Test files under declared test roots are fine
|
|
291
|
+
if (isUnderAnyRoot(rel, policy.testRoots)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
// Colocated under source root without test-root classification = violation
|
|
295
|
+
if (isUnderAnyRoot(rel, policy.sourceRoots)) {
|
|
296
|
+
findings.push({
|
|
297
|
+
path: rel,
|
|
298
|
+
kind: "test-under-source-root",
|
|
299
|
+
detail: `test file pattern under production source root`,
|
|
300
|
+
remediation: testUnderSourceRemediation(),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (!policy.productionMayReferenceTestRoots) {
|
|
305
|
+
for (const rel of files) {
|
|
306
|
+
if (isAllowListed(rel, policy.allow) !== null)
|
|
307
|
+
continue;
|
|
308
|
+
// Skip binary-ish extensions
|
|
309
|
+
if (/\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|gz|woff2?|ttf|eot|bin|exe|dll)$/i.test(rel)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
let content;
|
|
313
|
+
if (options.fileContents !== undefined) {
|
|
314
|
+
content = options.fileContents.get(rel);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
const full = resolve(root, rel);
|
|
318
|
+
if (!existsSync(full))
|
|
319
|
+
continue;
|
|
320
|
+
try {
|
|
321
|
+
content = readFileSync(full, "utf8");
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (content === undefined)
|
|
328
|
+
continue;
|
|
329
|
+
// Cap scan size
|
|
330
|
+
if (content.length > 512_000) {
|
|
331
|
+
content = content.slice(0, 512_000);
|
|
332
|
+
}
|
|
333
|
+
const finding = scanProductionReferences(rel, content, policy);
|
|
334
|
+
if (finding !== null) {
|
|
335
|
+
findings.push(finding);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (findings.length === 0) {
|
|
340
|
+
return {
|
|
341
|
+
exitCode: 0,
|
|
342
|
+
findings,
|
|
343
|
+
policy,
|
|
344
|
+
message: `verify_test_boundary: clean (${files.length} file(s), policy source=${policy.source}, ` +
|
|
345
|
+
`mode=${policy.enforcementMode}) (#3145).`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const header = policy.enforcementMode === "warn"
|
|
349
|
+
? `verify_test_boundary: WARN ${findings.length} boundary finding(s) (migration/discovery mode; not failing) (#3145).`
|
|
350
|
+
: `verify_test_boundary: ${findings.length} boundary violation(s) (#3145).`;
|
|
351
|
+
// Cap diagnostic body so migration discovery does not flood task check logs.
|
|
352
|
+
const maxShown = policy.enforcementMode === "warn" ? 15 : 50;
|
|
353
|
+
const shown = findings.slice(0, maxShown);
|
|
354
|
+
const body = shown
|
|
355
|
+
.map((f) => ` ${f.path}\n kind: ${f.kind}\n detail: ${f.detail}\n remediation: ${f.remediation}`)
|
|
356
|
+
.join("\n");
|
|
357
|
+
const truncated = findings.length > maxShown
|
|
358
|
+
? `\n … and ${findings.length - maxShown} more (re-run with authored policy to triage).`
|
|
359
|
+
: "";
|
|
360
|
+
const exitCode = policy.enforcementMode === "warn" ? 0 : 1;
|
|
361
|
+
return {
|
|
362
|
+
exitCode,
|
|
363
|
+
findings,
|
|
364
|
+
policy,
|
|
365
|
+
message: `${header}\n${body}${truncated}`,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
//# sourceMappingURL=evaluate.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test-boundary package surface (#3145).
|
|
3
|
+
*/
|
|
4
|
+
export { evaluateTestBoundary, isRecognizedTestBasename, matchesRootGlob, matchesTestFilePattern, matchPolicyGlob, type TestBoundaryFinding, type TestBoundaryOptions, type TestBoundaryResult, type TestBoundaryViolationKind, } from "./evaluate.js";
|
|
5
|
+
export { DEFAULT_FIXTURE_ROOTS, DEFAULT_SOURCE_ROOTS, DEFAULT_TEST_FILE_PATTERNS, DEFAULT_TEST_ROOTS, defaultTestBoundaryPolicy, FRAMEWORK_SELF_ALLOW, loadTestBoundaryPolicy, type TestBoundaryAllowEntry, type TestBoundaryPolicy, } from "./policy.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test-boundary package surface (#3145).
|
|
3
|
+
*/
|
|
4
|
+
export { evaluateTestBoundary, isRecognizedTestBasename, matchesRootGlob, matchesTestFilePattern, matchPolicyGlob, } from "./evaluate.js";
|
|
5
|
+
export { DEFAULT_FIXTURE_ROOTS, DEFAULT_SOURCE_ROOTS, DEFAULT_TEST_FILE_PATTERNS, DEFAULT_TEST_ROOTS, defaultTestBoundaryPolicy, FRAMEWORK_SELF_ALLOW, loadTestBoundaryPolicy, } from "./policy.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed test-boundary policy (#3145).
|
|
3
|
+
*
|
|
4
|
+
* Declares production vs test placement roots and conventional test-file
|
|
5
|
+
* patterns so verify:test-boundary can fail closed with concrete remediation.
|
|
6
|
+
*/
|
|
7
|
+
/** Explicit exception or production-liveness classification. */
|
|
8
|
+
export interface TestBoundaryAllowEntry {
|
|
9
|
+
/** Glob or path prefix (POSIX, repo-relative). */
|
|
10
|
+
readonly path: string;
|
|
11
|
+
/** Human-readable reason recorded for review. */
|
|
12
|
+
readonly reason?: string;
|
|
13
|
+
/**
|
|
14
|
+
* `exception` = narrow reviewed carve-out;
|
|
15
|
+
* `production-liveness` = health probe / canary / operational evidence.
|
|
16
|
+
*/
|
|
17
|
+
readonly kind?: "exception" | "production-liveness";
|
|
18
|
+
}
|
|
19
|
+
/** Machine-checked test/source boundary contract. */
|
|
20
|
+
export interface TestBoundaryPolicy {
|
|
21
|
+
readonly sourceRoots: readonly string[];
|
|
22
|
+
readonly testRoots: readonly string[];
|
|
23
|
+
readonly fixtureRoots: readonly string[];
|
|
24
|
+
readonly testFilePatterns: readonly string[];
|
|
25
|
+
/** When false (default), production content must not reference test/fixture roots. */
|
|
26
|
+
readonly productionMayReferenceTestRoots: boolean;
|
|
27
|
+
readonly allow: readonly TestBoundaryAllowEntry[];
|
|
28
|
+
/**
|
|
29
|
+
* `warn` = discovery / migration (exit 0 with findings);
|
|
30
|
+
* `enforce` = fail closed (exit 1). Default `enforce` when policy is present,
|
|
31
|
+
* `warn` when only defaults are inferred (migration path).
|
|
32
|
+
*/
|
|
33
|
+
readonly enforcementMode: "warn" | "enforce";
|
|
34
|
+
/** Where the policy was loaded from (for diagnostics). */
|
|
35
|
+
readonly source: "file" | "project-definition" | "defaults";
|
|
36
|
+
}
|
|
37
|
+
export declare const DEFAULT_TEST_FILE_PATTERNS: readonly string[];
|
|
38
|
+
export declare const DEFAULT_SOURCE_ROOTS: readonly string[];
|
|
39
|
+
export declare const DEFAULT_TEST_ROOTS: readonly string[];
|
|
40
|
+
export declare const DEFAULT_FIXTURE_ROOTS: readonly string[];
|
|
41
|
+
/** Built-in allow entries for framework self-check (colocated language tests). */
|
|
42
|
+
export declare const FRAMEWORK_SELF_ALLOW: readonly TestBoundaryAllowEntry[];
|
|
43
|
+
/** Default inferred policy (migration / discovery). */
|
|
44
|
+
export declare function defaultTestBoundaryPolicy(enforcementMode?: "warn" | "enforce"): TestBoundaryPolicy;
|
|
45
|
+
/**
|
|
46
|
+
* Load policy from explicit path, then `.deft/test-boundary.policy.json`,
|
|
47
|
+
* then `plan.policy.testBoundary` in PROJECT-DEFINITION, else defaults (warn).
|
|
48
|
+
*/
|
|
49
|
+
export declare function loadTestBoundaryPolicy(projectRoot: string, options?: {
|
|
50
|
+
readonly policyPath?: string | null;
|
|
51
|
+
}): TestBoundaryPolicy;
|
|
52
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed test-boundary policy (#3145).
|
|
3
|
+
*
|
|
4
|
+
* Declares production vs test placement roots and conventional test-file
|
|
5
|
+
* patterns so verify:test-boundary can fail closed with concrete remediation.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
export const DEFAULT_TEST_FILE_PATTERNS = [
|
|
10
|
+
"**/test_*.py",
|
|
11
|
+
"**/*_test.py",
|
|
12
|
+
"**/*Tests.cs",
|
|
13
|
+
"**/*Test.cs",
|
|
14
|
+
"**/*.test.ts",
|
|
15
|
+
"**/*.test.tsx",
|
|
16
|
+
"**/*.test.js",
|
|
17
|
+
"**/*.test.jsx",
|
|
18
|
+
"**/*.spec.ts",
|
|
19
|
+
"**/*.spec.tsx",
|
|
20
|
+
"**/*.spec.js",
|
|
21
|
+
"**/*.spec.jsx",
|
|
22
|
+
"**/*_test.go",
|
|
23
|
+
];
|
|
24
|
+
export const DEFAULT_SOURCE_ROOTS = [
|
|
25
|
+
"src/**",
|
|
26
|
+
"infra/**",
|
|
27
|
+
"packages/*/src/**",
|
|
28
|
+
"cmd/**",
|
|
29
|
+
"Tools/**",
|
|
30
|
+
];
|
|
31
|
+
export const DEFAULT_TEST_ROOTS = [
|
|
32
|
+
"tests/**",
|
|
33
|
+
"test/**",
|
|
34
|
+
"**/__tests__/**",
|
|
35
|
+
"packages/*/test/**",
|
|
36
|
+
// Language-idiomatic colocated tests (not production pollution)
|
|
37
|
+
"packages/*/src/**/*.test.*",
|
|
38
|
+
"packages/*/src/**/*.spec.*",
|
|
39
|
+
"cmd/**/*_test.go",
|
|
40
|
+
"**/*_test.go",
|
|
41
|
+
];
|
|
42
|
+
export const DEFAULT_FIXTURE_ROOTS = [
|
|
43
|
+
"tests/fixtures/**",
|
|
44
|
+
"test/fixtures/**",
|
|
45
|
+
"**/fixtures/**",
|
|
46
|
+
];
|
|
47
|
+
/** Built-in allow entries for framework self-check (colocated language tests). */
|
|
48
|
+
export const FRAMEWORK_SELF_ALLOW = [
|
|
49
|
+
{
|
|
50
|
+
path: "packages/*/src/**/*.test.ts",
|
|
51
|
+
reason: "Directive colocated unit tests under packages/*/src",
|
|
52
|
+
kind: "exception",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: "packages/*/src/**/*.test.tsx",
|
|
56
|
+
reason: "Directive colocated unit tests under packages/*/src",
|
|
57
|
+
kind: "exception",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
path: "packages/*/src/**/*.spec.ts",
|
|
61
|
+
reason: "Directive colocated unit tests under packages/*/src",
|
|
62
|
+
kind: "exception",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
path: "cmd/**/*_test.go",
|
|
66
|
+
reason: "Go colocated package tests (language convention)",
|
|
67
|
+
kind: "exception",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
path: "**/*_test.go",
|
|
71
|
+
reason: "Go colocated package tests (language convention)",
|
|
72
|
+
kind: "exception",
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
function asStringArray(raw) {
|
|
76
|
+
if (!Array.isArray(raw))
|
|
77
|
+
return [];
|
|
78
|
+
return raw
|
|
79
|
+
.filter((x) => typeof x === "string" && x.trim().length > 0)
|
|
80
|
+
.map((s) => s.trim());
|
|
81
|
+
}
|
|
82
|
+
function asAllowEntries(raw) {
|
|
83
|
+
if (!Array.isArray(raw))
|
|
84
|
+
return [];
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const item of raw) {
|
|
87
|
+
if (typeof item === "string" && item.trim().length > 0) {
|
|
88
|
+
out.push({ path: item.trim(), kind: "exception" });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
92
|
+
const rec = item;
|
|
93
|
+
const path = typeof rec.path === "string" ? rec.path.trim() : "";
|
|
94
|
+
if (path.length === 0)
|
|
95
|
+
continue;
|
|
96
|
+
const kind = rec.kind === "production-liveness" || rec.kind === "exception" ? rec.kind : "exception";
|
|
97
|
+
const reason = typeof rec.reason === "string" ? rec.reason : undefined;
|
|
98
|
+
out.push({ path, kind, reason });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function parsePolicyObject(raw, source, defaultMode) {
|
|
104
|
+
const sourceRoots = asStringArray(raw.sourceRoots);
|
|
105
|
+
const testRoots = asStringArray(raw.testRoots);
|
|
106
|
+
const fixtureRoots = asStringArray(raw.fixtureRoots);
|
|
107
|
+
const testFilePatterns = asStringArray(raw.testFilePatterns);
|
|
108
|
+
const productionMayReferenceTestRoots = typeof raw.productionMayReferenceTestRoots === "boolean"
|
|
109
|
+
? raw.productionMayReferenceTestRoots
|
|
110
|
+
: false;
|
|
111
|
+
const allow = asAllowEntries(raw.allow);
|
|
112
|
+
let enforcementMode = defaultMode;
|
|
113
|
+
if (raw.enforcementMode === "warn" || raw.enforcementMode === "enforce") {
|
|
114
|
+
enforcementMode = raw.enforcementMode;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
sourceRoots: sourceRoots.length > 0 ? sourceRoots : DEFAULT_SOURCE_ROOTS,
|
|
118
|
+
testRoots: testRoots.length > 0 ? testRoots : DEFAULT_TEST_ROOTS,
|
|
119
|
+
fixtureRoots: fixtureRoots.length > 0 ? fixtureRoots : DEFAULT_FIXTURE_ROOTS,
|
|
120
|
+
testFilePatterns: testFilePatterns.length > 0 ? testFilePatterns : DEFAULT_TEST_FILE_PATTERNS,
|
|
121
|
+
productionMayReferenceTestRoots,
|
|
122
|
+
allow,
|
|
123
|
+
enforcementMode,
|
|
124
|
+
source,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Default inferred policy (migration / discovery). */
|
|
128
|
+
export function defaultTestBoundaryPolicy(enforcementMode = "warn") {
|
|
129
|
+
return {
|
|
130
|
+
sourceRoots: DEFAULT_SOURCE_ROOTS,
|
|
131
|
+
testRoots: DEFAULT_TEST_ROOTS,
|
|
132
|
+
fixtureRoots: DEFAULT_FIXTURE_ROOTS,
|
|
133
|
+
testFilePatterns: DEFAULT_TEST_FILE_PATTERNS,
|
|
134
|
+
productionMayReferenceTestRoots: false,
|
|
135
|
+
allow: [...FRAMEWORK_SELF_ALLOW],
|
|
136
|
+
enforcementMode,
|
|
137
|
+
source: "defaults",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Load policy from explicit path, then `.deft/test-boundary.policy.json`,
|
|
142
|
+
* then `plan.policy.testBoundary` in PROJECT-DEFINITION, else defaults (warn).
|
|
143
|
+
*/
|
|
144
|
+
export function loadTestBoundaryPolicy(projectRoot, options = {}) {
|
|
145
|
+
const root = resolve(projectRoot);
|
|
146
|
+
if (options.policyPath !== null && options.policyPath !== undefined) {
|
|
147
|
+
const p = resolve(options.policyPath);
|
|
148
|
+
if (!existsSync(p)) {
|
|
149
|
+
throw new Error(`test-boundary policy file not found: ${p}`);
|
|
150
|
+
}
|
|
151
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
152
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
153
|
+
throw new Error(`test-boundary policy must be a JSON object: ${p}`);
|
|
154
|
+
}
|
|
155
|
+
return parsePolicyObject(raw, "file", "enforce");
|
|
156
|
+
}
|
|
157
|
+
const deftPolicy = join(root, ".deft", "test-boundary.policy.json");
|
|
158
|
+
if (existsSync(deftPolicy)) {
|
|
159
|
+
const raw = JSON.parse(readFileSync(deftPolicy, "utf8"));
|
|
160
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
161
|
+
throw new Error(`test-boundary policy must be a JSON object: ${deftPolicy}`);
|
|
162
|
+
}
|
|
163
|
+
return parsePolicyObject(raw, "file", "enforce");
|
|
164
|
+
}
|
|
165
|
+
const pdPath = join(root, "xbrief", "PROJECT-DEFINITION.xbrief.json");
|
|
166
|
+
if (existsSync(pdPath)) {
|
|
167
|
+
try {
|
|
168
|
+
const pd = JSON.parse(readFileSync(pdPath, "utf8"));
|
|
169
|
+
const plan = pd.plan;
|
|
170
|
+
const policy = plan?.policy;
|
|
171
|
+
const tb = policy?.testBoundary;
|
|
172
|
+
if (tb !== undefined && tb !== null && typeof tb === "object" && !Array.isArray(tb)) {
|
|
173
|
+
return parsePolicyObject(tb, "project-definition", "enforce");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// fall through to defaults
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return defaultTestBoundaryPolicy("warn");
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=policy.js.map
|