@deftai/directive-core 0.96.0 → 0.97.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 (73) hide show
  1. package/dist/cache/archive.js +10 -4
  2. package/dist/check/gate-lists.js +8 -0
  3. package/dist/consumer-check-contract/evaluate.d.ts +124 -0
  4. package/dist/consumer-check-contract/evaluate.js +699 -0
  5. package/dist/consumer-check-contract/index.d.ts +5 -0
  6. package/dist/consumer-check-contract/index.js +5 -0
  7. package/dist/delivery-attempt/disk-begin.d.ts +51 -0
  8. package/dist/delivery-attempt/disk-begin.js +68 -0
  9. package/dist/delivery-attempt/evaluate.d.ts +26 -0
  10. package/dist/delivery-attempt/evaluate.js +443 -0
  11. package/dist/delivery-attempt/fingerprint.d.ts +32 -0
  12. package/dist/delivery-attempt/fingerprint.js +100 -0
  13. package/dist/delivery-attempt/handoff.d.ts +25 -0
  14. package/dist/delivery-attempt/handoff.js +102 -0
  15. package/dist/delivery-attempt/index.d.ts +17 -0
  16. package/dist/delivery-attempt/index.js +17 -0
  17. package/dist/delivery-attempt/ledger.d.ts +169 -0
  18. package/dist/delivery-attempt/ledger.js +758 -0
  19. package/dist/delivery-attempt/material-delta.d.ts +38 -0
  20. package/dist/delivery-attempt/material-delta.js +126 -0
  21. package/dist/delivery-attempt/types.d.ts +210 -0
  22. package/dist/delivery-attempt/types.js +77 -0
  23. package/dist/doctor/index.d.ts +1 -0
  24. package/dist/doctor/index.js +1 -0
  25. package/dist/doctor/main.js +12 -0
  26. package/dist/doctor/openclaw-soft-rebind.d.ts +26 -0
  27. package/dist/doctor/openclaw-soft-rebind.js +164 -0
  28. package/dist/hooks/dispatcher.d.ts +2 -1
  29. package/dist/hooks/dispatcher.js +63 -9
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.js +4 -0
  32. package/dist/init-deposit/gitignore.js +7 -0
  33. package/dist/init-deposit/init-deposit.js +5 -0
  34. package/dist/init-deposit/refresh.js +3 -0
  35. package/dist/pr-merge-readiness/ci-gate.d.ts +29 -1
  36. package/dist/pr-merge-readiness/ci-gate.js +191 -24
  37. package/dist/pr-merge-readiness/compute.js +10 -1
  38. package/dist/pr-merge-readiness/index.d.ts +2 -1
  39. package/dist/pr-merge-readiness/index.js +2 -1
  40. package/dist/pr-merge-readiness/output.js +14 -0
  41. package/dist/pr-merge-readiness/platform-status.d.ts +29 -0
  42. package/dist/pr-merge-readiness/platform-status.js +49 -0
  43. package/dist/pr-watch/constants.d.ts +10 -0
  44. package/dist/pr-watch/constants.js +12 -1
  45. package/dist/pr-watch/main.js +16 -1
  46. package/dist/pr-watch/probe.js +13 -1
  47. package/dist/pr-watch/types.d.ts +3 -2
  48. package/dist/pr-watch/watch.js +14 -7
  49. package/dist/scope-provenance/digest.d.ts +67 -0
  50. package/dist/scope-provenance/digest.js +188 -0
  51. package/dist/scope-provenance/evaluate.d.ts +82 -0
  52. package/dist/scope-provenance/evaluate.js +528 -0
  53. package/dist/scope-provenance/index.d.ts +6 -0
  54. package/dist/scope-provenance/index.js +6 -0
  55. package/dist/session/compact-ritual.d.ts +96 -0
  56. package/dist/session/compact-ritual.js +237 -0
  57. package/dist/session/compact-ritual.spec.d.ts +2 -0
  58. package/dist/session/compact-ritual.spec.js +21 -0
  59. package/dist/session/index.d.ts +2 -0
  60. package/dist/session/index.js +2 -0
  61. package/dist/session/openclaw-soft-rebind-deposit.d.ts +46 -0
  62. package/dist/session/openclaw-soft-rebind-deposit.js +165 -0
  63. package/dist/test-boundary/evaluate.d.ts +54 -0
  64. package/dist/test-boundary/evaluate.js +368 -0
  65. package/dist/test-boundary/index.d.ts +6 -0
  66. package/dist/test-boundary/index.js +6 -0
  67. package/dist/test-boundary/policy.d.ts +52 -0
  68. package/dist/test-boundary/policy.js +182 -0
  69. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  70. package/dist/triage/bootstrap/gitignore.js +15 -1
  71. package/dist/vbrief-activate/activate.js +22 -6
  72. package/dist/xbrief/styles.js +33 -17
  73. package/package.json +7 -3
@@ -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
@@ -8,7 +8,7 @@ export declare function gitignoreTriageCacheEntries(projectRoot: string): readon
8
8
  export declare function gitattributesTriageCacheGlob(projectRoot: string): string;
9
9
  export declare const GITATTRIBUTES_EVAL_RULE = "vbrief/.triage-cache/*.jsonl merge=union";
10
10
  export declare const FORBIDDEN_BLANKET_EVAL_LINES: readonly string[];
11
- export declare const EVAL_README_BODY = "# `vbrief/.triage-cache/` \u2014 triage working-set files\n\nThis directory holds JSON-lines logs and scratch files that Deft triage and\nslicing workflows emit. Deft configures your repo's `.gitignore` and\n`.gitattributes` so some files stay local while team-shared records can be\ncommitted.\n\n## What lives here\n\n| File | Committed? | Notes |\n| --- | --- | --- |\n| `slices.jsonl` | Yes | Team-shared cohort records from slicing skills. New teammates use prior cohort outputs to spot orphans and avoid re-slicing the same scope. |\n| `candidates.jsonl` | No | Your local triage accept / defer / reject stream. Re-create on a fresh clone with `deft triage:bootstrap`. |\n| `summary-history.jsonl` | No | Local history of `deft triage:summary` output; not required for day-to-day work. |\n| `scope-lifecycle.jsonl` | No | Local audit trail for scope demotions (`deft scope:demote`). Each operator's stream stays on their machine. |\n| `decompositions/` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via `deft scope:decompose`. |\n| `doctor-state.json` | No | Per-clone throttle state for `deft doctor` re-probe timing. |\n\nPaths listed as \"No\" above are added to `.gitignore` during bootstrap; anything\nnot listed remains committable by default. The selective ignore entries live in\nthe repo-root `.gitignore` (`vbrief/.triage-cache/candidates.jsonl`,\n`vbrief/.triage-cache/summary-history.jsonl`, `vbrief/.triage-cache/scope-lifecycle.jsonl`,\n`vbrief/.triage-cache/decompositions/`, and `vbrief/.triage-cache/doctor-state.json`).\n\n## Fresh clone\n\nIf `candidates.jsonl` is missing, run:\n\n```\ndeft triage:bootstrap\n```\n\nBootstrap rebuilds the local candidates log without altering committed\n`slices.jsonl`.\n\n## Merge behavior for `*.jsonl`\n\nThe repo-root `.gitattributes` may declare:\n\n```\nvbrief/.triage-cache/*.jsonl merge=union\n```\n\nThe `union` merge driver concatenates both sides' appended lines on auto-merge,\nso parallel append-only edits to the same JSON-lines file rebase without manual\nconflict surgery. It does not dedupe semantically similar records \u2014 downstream\nreaders should tolerate duplicate-looking entries.\n\n## See also\n\n- `.gitignore` \u2014 selective ignore rules for operator-private files\n- `.gitattributes` \u2014 merge driver for committed JSON-lines logs\n";
11
+ export declare const EVAL_README_BODY = "# `vbrief/.triage-cache/` \u2014 triage working-set files\n\nThis directory holds JSON-lines logs and scratch files that Deft triage and\nslicing workflows emit. Deft configures your repo's `.gitignore` and\n`.gitattributes` so some files stay local while team-shared records can be\ncommitted.\n\n## What lives here\n\n| File | Committed? | Notes |\n| --- | --- | --- |\n| `slices.jsonl` | Yes | Team-shared cohort records from slicing skills. New teammates use prior cohort outputs to spot orphans and avoid re-slicing the same scope. |\n| `candidates.jsonl` | No | Your local triage accept / defer / reject stream. Re-create on a fresh clone with `deft triage:bootstrap`. |\n| `summary-history.jsonl` | No | Local history of `deft triage:summary` output; not required for day-to-day work. |\n| `scope-lifecycle.jsonl` | No | Local audit trail for scope demotions (`deft scope:demote`). Each operator's stream stays on their machine. |\n| `decompositions/` | No | Draft story-decomposition scratch. Produced child story xBRIEFs live in lifecycle folders via `deft scope:decompose`. |\n| `doctor-state.json` | No | Per-clone throttle state for `deft doctor` re-probe timing. |\n| `staleness-tickler-state.json` | No | Per-clone upgrade-tickler throttle state. |\n| `release-availability-state.json` | No | Per-clone release-availability probe throttle state. |\n\nPaths listed as \"No\" above are added to `.gitignore` during bootstrap; anything\nnot listed remains committable by default. The selective ignore entries live in\nthe repo-root `.gitignore` (`vbrief/.triage-cache/candidates.jsonl`,\n`vbrief/.triage-cache/summary-history.jsonl`, `vbrief/.triage-cache/scope-lifecycle.jsonl`,\n`vbrief/.triage-cache/decompositions/`, `vbrief/.triage-cache/doctor-state.json`,\n`vbrief/.triage-cache/staleness-tickler-state.json`, and\n`vbrief/.triage-cache/release-availability-state.json`).\n\n## Fresh clone\n\nIf `candidates.jsonl` is missing, run:\n\n```\ndeft triage:bootstrap\n```\n\nBootstrap rebuilds the local candidates log without altering committed\n`slices.jsonl`.\n\n## Merge behavior for `*.jsonl`\n\nThe repo-root `.gitattributes` may declare:\n\n```\nvbrief/.triage-cache/*.jsonl merge=union\n```\n\nThe `union` merge driver concatenates both sides' appended lines on auto-merge,\nso parallel append-only edits to the same JSON-lines file rebase without manual\nconflict surgery. It does not dedupe semantically similar records \u2014 downstream\nreaders should tolerate duplicate-looking entries.\n\n## See also\n\n- `.gitignore` \u2014 selective ignore rules for operator-private files\n- `.gitattributes` \u2014 merge driver for committed JSON-lines logs\n";
12
12
  /** Layout-aware triage-cache README body for the active lifecycle tree (#2344 / #2349). */
13
13
  export declare function generateTriageCacheReadmeBody(projectRoot: string): string;
14
14
  /** Strip an inline `# ...` comment from a gitignore line. */