@deftai/directive-core 0.74.0 → 0.76.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/agents-md-budget/evaluate.d.ts +61 -0
- package/dist/agents-md-budget/evaluate.js +341 -19
- package/dist/agents-md-budget/index.d.ts +1 -0
- package/dist/agents-md-budget/index.js +1 -0
- package/dist/agents-md-budget/skill-frontmatter.d.ts +35 -0
- package/dist/agents-md-budget/skill-frontmatter.js +116 -0
- package/dist/cache/operations.d.ts +2 -0
- package/dist/cache/operations.js +16 -1
- package/dist/content-contracts/skills/helpers.d.ts +0 -1
- package/dist/content-contracts/skills/helpers.js +1 -11
- package/dist/content-contracts/skills/skill-frontmatter.js +8 -1
- package/dist/content-contracts/standards/_helpers.js +4 -2
- package/dist/content-contracts/standards/_taskfile-helpers.d.ts +2 -0
- package/dist/content-contracts/standards/_taskfile-helpers.js +11 -5
- package/dist/doctor/doctor-state.js +2 -1
- package/dist/doctor/paths.js +2 -1
- package/dist/eval-health-relocation/evaluate.d.ts +71 -0
- package/dist/eval-health-relocation/evaluate.js +252 -0
- package/dist/eval-health-relocation/index.d.ts +2 -0
- package/dist/eval-health-relocation/index.js +2 -0
- package/dist/init-deposit/scaffold.js +35 -20
- package/dist/intake/issue-emit.js +2 -2
- package/dist/intake/issue-ingest.js +23 -4
- package/dist/packaging/openpackage-tiers.d.ts +12 -0
- package/dist/packaging/openpackage-tiers.js +42 -0
- package/dist/policy/agents-md-budget.d.ts +23 -0
- package/dist/policy/agents-md-budget.js +75 -4
- package/dist/preflight/evaluate.d.ts +5 -2
- package/dist/preflight/evaluate.js +9 -4
- package/dist/preflight/index.d.ts +1 -1
- package/dist/preflight/index.js +1 -1
- package/dist/release/pipeline-fixture.d.ts +3 -0
- package/dist/release/pipeline-fixture.js +11 -0
- package/dist/release/pipeline.js +4 -0
- package/dist/release/spawn.js +5 -3
- package/dist/scope/decompose.js +4 -3
- package/dist/scope/transition.js +17 -2
- package/dist/scope/vbrief-ref.d.ts +6 -0
- package/dist/scope/vbrief-ref.js +23 -4
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/posture.d.ts +50 -0
- package/dist/session/posture.js +152 -0
- package/dist/session/ritual-sentinel.d.ts +2 -0
- package/dist/session/ritual-sentinel.js +3 -0
- package/dist/session/session-start.d.ts +8 -0
- package/dist/session/session-start.js +43 -0
- package/dist/session/verify-session-ritual.d.ts +6 -0
- package/dist/session/verify-session-ritual.js +67 -4
- package/dist/swarm/complete-cohort.js +6 -6
- package/dist/swarm/launch.js +2 -2
- package/dist/triage/bootstrap/gitignore.js +8 -4
- package/dist/triage/cache-path.js +16 -3
- package/dist/triage/queue/cache.d.ts +14 -0
- package/dist/triage/queue/cache.js +61 -1
- package/dist/triage/welcome/writers.js +2 -0
- package/dist/ts-check-lane/index.d.ts +1 -1
- package/dist/ts-check-lane/index.js +1 -1
- package/dist/ts-check-lane/run-lane.d.ts +5 -0
- package/dist/ts-check-lane/run-lane.js +15 -0
- package/dist/validate-content/validate-links.js +2 -2
- package/dist/value/readback.d.ts +1 -0
- package/dist/value/readback.js +5 -1
- package/dist/vbrief-build/constants.d.ts +3 -3
- package/dist/vbrief-build/constants.js +4 -3
- package/dist/vbrief-build/index.d.ts +1 -1
- package/dist/vbrief-build/index.js +1 -1
- package/dist/verify-env/toolchain-check.js +20 -5
- package/dist/verify-env/verify-hooks-installed.d.ts +2 -0
- package/dist/verify-env/verify-hooks-installed.js +17 -15
- package/package.json +7 -3
package/dist/cache/operations.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
3
4
|
import { pyRepr } from "../scm/py-format.js";
|
|
4
5
|
import { ALLOWED_SOURCES, REPO_RE, SOURCE_TTL_SECONDS } from "./constants.js";
|
|
5
6
|
import { CacheCapBreachedError, CacheError, CacheNotFoundError, CacheValidationError, } from "./errors.js";
|
|
@@ -10,6 +11,15 @@ import { enforceCaps, predictEvictionSet, resolveCaps, } from "./quota.js";
|
|
|
10
11
|
import { flagsForMeta, SCANNER_VERSION, scan } from "./scanner.js";
|
|
11
12
|
import { addSeconds, parseIso, systemClock, utcIso } from "./time.js";
|
|
12
13
|
import { validateMeta } from "./validate.js";
|
|
14
|
+
export { ProjectionContainmentError as CacheContainmentError };
|
|
15
|
+
/** Refuse symlink-escaping `.deft-cache` before mkdir/read/write (#2470). */
|
|
16
|
+
function assertWritableCachePath(cacheRoot, ...segments) {
|
|
17
|
+
const cacheAbs = resolve(cacheRoot);
|
|
18
|
+
const projectDir = dirname(cacheAbs);
|
|
19
|
+
const target = segments.length > 0 ? join(cacheAbs, ...segments) : cacheAbs;
|
|
20
|
+
assertProjectionContained(projectDir, target);
|
|
21
|
+
return target;
|
|
22
|
+
}
|
|
13
23
|
function renderContent(source, raw) {
|
|
14
24
|
if (source === "github-issue") {
|
|
15
25
|
const number = raw.number;
|
|
@@ -82,6 +92,7 @@ export function cachePut(source, key, raw, options = {}) {
|
|
|
82
92
|
}
|
|
83
93
|
const expires = addSeconds(fetched, ttl);
|
|
84
94
|
const cacheRoot = options.cacheRoot ?? ".deft-cache";
|
|
95
|
+
assertWritableCachePath(cacheRoot);
|
|
85
96
|
const edir = entryDir(source, key, cacheRoot);
|
|
86
97
|
const rawText = pythonJsonDump(raw);
|
|
87
98
|
const rawSize = Buffer.byteLength(rawText, "utf8");
|
|
@@ -182,6 +193,7 @@ export function cachePut(source, key, raw, options = {}) {
|
|
|
182
193
|
export function cacheGet(source, key, options = {}) {
|
|
183
194
|
const clock = options.clock ?? systemClock;
|
|
184
195
|
const cacheRoot = options.cacheRoot ?? ".deft-cache";
|
|
196
|
+
assertWritableCachePath(cacheRoot);
|
|
185
197
|
const allowStale = options.allowStale ?? true;
|
|
186
198
|
const edir = entryDir(source, key, cacheRoot);
|
|
187
199
|
const metaRelPath = `${source}/${key}/meta.json`;
|
|
@@ -220,6 +232,7 @@ export function cacheInvalidate(source, key, options = {}) {
|
|
|
220
232
|
const clock = options.clock ?? systemClock;
|
|
221
233
|
validateKey(source, key);
|
|
222
234
|
const cacheRoot = options.cacheRoot ?? ".deft-cache";
|
|
235
|
+
assertWritableCachePath(cacheRoot);
|
|
223
236
|
const edir = entryDir(source, key, cacheRoot);
|
|
224
237
|
const existed = existsSync(edir);
|
|
225
238
|
if (existed)
|
|
@@ -252,6 +265,7 @@ export function cachePrune(options = {}) {
|
|
|
252
265
|
throw new CacheError(`--older-than-days must be >= 0 (got ${JSON.stringify(olderThanDays)})`);
|
|
253
266
|
}
|
|
254
267
|
const cacheRoot = options.cacheRoot ?? ".deft-cache";
|
|
268
|
+
assertWritableCachePath(cacheRoot);
|
|
255
269
|
if (!existsSync(cacheRoot))
|
|
256
270
|
return [];
|
|
257
271
|
const cutoff = addSeconds(clock.now(), -olderThanDays * 24 * 60 * 60);
|
|
@@ -313,6 +327,7 @@ function collectMetaPathsUnder(srcRoot) {
|
|
|
313
327
|
export function cachePruneToCap(options = {}) {
|
|
314
328
|
const clock = options.clock ?? systemClock;
|
|
315
329
|
const cacheRoot = options.cacheRoot ?? ".deft-cache";
|
|
330
|
+
assertWritableCachePath(cacheRoot);
|
|
316
331
|
const resolved = options.caps ?? resolveCaps();
|
|
317
332
|
if (!resolved.maxBytes && !resolved.maxEntries)
|
|
318
333
|
return [];
|
|
@@ -19,6 +19,5 @@ export declare function listSkillMdFiles(): string[];
|
|
|
19
19
|
export declare const RFC2119_LEGEND = "!=MUST, ~=SHOULD";
|
|
20
20
|
export declare const PLATFORM_DETECTION_HEADING = "## Platform Detection";
|
|
21
21
|
export declare const USER_MD_GATE_HEADING = "## USER.md Gate";
|
|
22
|
-
export declare const DEPRECATED_SKILL_REDIRECT_STUBS: Set<string>;
|
|
23
22
|
export declare const REQUIRED_OSES: Set<string>;
|
|
24
23
|
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -21,7 +21,7 @@ export function resolveRepoPath(relPath) {
|
|
|
21
21
|
return repoPath(relPath);
|
|
22
22
|
}
|
|
23
23
|
export function readRepoFile(relPath) {
|
|
24
|
-
return readFileSync(resolveRepoPath(relPath), "utf8");
|
|
24
|
+
return readFileSync(resolveRepoPath(relPath), "utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
25
25
|
}
|
|
26
26
|
export function repoFileExists(relPath) {
|
|
27
27
|
return existsSync(resolveRepoPath(relPath));
|
|
@@ -59,15 +59,5 @@ export function listSkillMdFiles() {
|
|
|
59
59
|
export const RFC2119_LEGEND = "!=MUST, ~=SHOULD";
|
|
60
60
|
export const PLATFORM_DETECTION_HEADING = "## Platform Detection";
|
|
61
61
|
export const USER_MD_GATE_HEADING = "## USER.md Gate";
|
|
62
|
-
export const DEPRECATED_SKILL_REDIRECT_STUBS = new Set([
|
|
63
|
-
"deft-build",
|
|
64
|
-
"deft-interview",
|
|
65
|
-
"deft-pre-pr",
|
|
66
|
-
"deft-review-cycle",
|
|
67
|
-
"deft-roadmap-refresh",
|
|
68
|
-
"deft-setup",
|
|
69
|
-
"deft-swarm",
|
|
70
|
-
"deft-sync",
|
|
71
|
-
]);
|
|
72
62
|
export const REQUIRED_OSES = new Set(["darwin", "linux", "windows"]);
|
|
73
63
|
//# sourceMappingURL=helpers.js.map
|
|
@@ -4,7 +4,14 @@ import { REPO_ROOT, REQUIRED_OSES } from "./helpers.js";
|
|
|
4
4
|
export { REQUIRED_OSES };
|
|
5
5
|
const OS_LINE = /^os\s*:\s*\[(?<body>[^\]]*)\]\s*$/m;
|
|
6
6
|
const OS_TOKEN = /['"]([^'"]+)['"]/g;
|
|
7
|
-
const EXCLUDED_PARTS = new Set([
|
|
7
|
+
const EXCLUDED_PARTS = new Set([
|
|
8
|
+
".git",
|
|
9
|
+
".venv",
|
|
10
|
+
"venv",
|
|
11
|
+
"node_modules",
|
|
12
|
+
"__pycache__",
|
|
13
|
+
".pytest_cache",
|
|
14
|
+
]);
|
|
8
15
|
function frontmatter(text) {
|
|
9
16
|
if (!text.startsWith("---")) {
|
|
10
17
|
return null;
|
|
@@ -27,10 +27,12 @@ export function resolveContentPath(relPath) {
|
|
|
27
27
|
return join(root, relPath);
|
|
28
28
|
}
|
|
29
29
|
export function readText(relPath) {
|
|
30
|
-
return readFileSync(resolveContentPath(relPath), { encoding: "utf8" })
|
|
30
|
+
return readFileSync(resolveContentPath(relPath), { encoding: "utf8" })
|
|
31
|
+
.replace(/\r\n/g, "\n")
|
|
32
|
+
.replace(/\r/g, "\n");
|
|
31
33
|
}
|
|
32
34
|
export function readAbs(absPath) {
|
|
33
|
-
return readFileSync(absPath, { encoding: "utf8" });
|
|
35
|
+
return readFileSync(absPath, { encoding: "utf8" }).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
34
36
|
}
|
|
35
37
|
export function pathExists(relPath) {
|
|
36
38
|
return existsSync(resolveContentPath(relPath));
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare function taskYamlFiles(): string[];
|
|
2
|
+
/** Normalize CRLF so Taskfile parsers do not treat lone `\r` lines as section exits (#2467). */
|
|
3
|
+
export declare function normalizeYamlNewlines(text: string): string;
|
|
2
4
|
export declare function iterTaskBlocks(text: string): Array<{
|
|
3
5
|
name: string;
|
|
4
6
|
start: number;
|
|
@@ -10,8 +10,12 @@ export function taskYamlFiles() {
|
|
|
10
10
|
.map((f) => join(dir, f))
|
|
11
11
|
.sort();
|
|
12
12
|
}
|
|
13
|
+
/** Normalize CRLF so Taskfile parsers do not treat lone `\r` lines as section exits (#2467). */
|
|
14
|
+
export function normalizeYamlNewlines(text) {
|
|
15
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
16
|
+
}
|
|
13
17
|
export function iterTaskBlocks(text) {
|
|
14
|
-
const lines = text.split("\n");
|
|
18
|
+
const lines = normalizeYamlNewlines(text).split("\n");
|
|
15
19
|
const taskPositions = [];
|
|
16
20
|
let inTasksSection = false;
|
|
17
21
|
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
@@ -43,19 +47,21 @@ export function iterTaskBlocks(text) {
|
|
|
43
47
|
return blocks;
|
|
44
48
|
}
|
|
45
49
|
export function blockBody(text, start, end) {
|
|
46
|
-
return text.split("\n").slice(start, end).join("\n");
|
|
50
|
+
return normalizeYamlNewlines(text).split("\n").slice(start, end).join("\n");
|
|
47
51
|
}
|
|
48
52
|
export function nonCommentLines(block) {
|
|
49
|
-
return block
|
|
53
|
+
return normalizeYamlNewlines(block)
|
|
54
|
+
.split("\n")
|
|
55
|
+
.filter((ln) => !ln.trimStart().startsWith("#"));
|
|
50
56
|
}
|
|
51
57
|
export function cachingKeyOnLine(line) {
|
|
52
58
|
const m = line.match(CACHING_KEY);
|
|
53
59
|
return m?.[1] ?? null;
|
|
54
60
|
}
|
|
55
61
|
export function readTaskfile(name) {
|
|
56
|
-
return readFileSync(join(repoRoot(), "tasks", name), { encoding: "utf8" });
|
|
62
|
+
return normalizeYamlNewlines(readFileSync(join(repoRoot(), "tasks", name), { encoding: "utf8" }));
|
|
57
63
|
}
|
|
58
64
|
export function readRoot(name) {
|
|
59
|
-
return readFileSync(join(repoRoot(), "tasks", "..", name), { encoding: "utf8" });
|
|
65
|
+
return normalizeYamlNewlines(readFileSync(join(repoRoot(), "tasks", "..", name), { encoding: "utf8" }));
|
|
60
66
|
}
|
|
61
67
|
//# sourceMappingURL=_taskfile-helpers.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { resolveTriageCachePath } from "../triage/cache-path.js";
|
|
4
5
|
import { CLEAN_WINDOW_HOURS, DIRTY_WINDOW_HOURS, ENV_STATE_PATH } from "./constants.js";
|
|
@@ -7,7 +8,7 @@ export function statePath(projectRoot) {
|
|
|
7
8
|
const override = process.env[ENV_STATE_PATH]?.trim();
|
|
8
9
|
if (override) {
|
|
9
10
|
return override.startsWith("~")
|
|
10
|
-
? join(
|
|
11
|
+
? join(homedir(), override.slice(1).replace(/^[\\/]/, ""))
|
|
11
12
|
: override;
|
|
12
13
|
}
|
|
13
14
|
return resolveTriageCachePath(projectRoot, STATE_FILENAME);
|
package/dist/doctor/paths.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { dirname, join, resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { DEFT_REPO_POSITIVE_MARKERS } from "./constants.js";
|
|
@@ -8,7 +9,7 @@ export function resolvePath(pathStr, cwd = process.cwd()) {
|
|
|
8
9
|
return cwd;
|
|
9
10
|
}
|
|
10
11
|
const expanded = pathStr.startsWith("~")
|
|
11
|
-
? join(
|
|
12
|
+
? join(homedir(), pathStr.slice(1).replace(/^[\\/]/, ""))
|
|
12
13
|
: pathStr;
|
|
13
14
|
return resolve(cwd, expanded);
|
|
14
15
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type HealthReport } from "../eval/health.js";
|
|
2
|
+
export type OutputStream = "stdout" | "stderr" | "none";
|
|
3
|
+
/** Glob patterns that classify a change as epic #2369 rule-relocation (#2373). */
|
|
4
|
+
export declare const RULE_RELOCATION_PATH_PATTERNS: readonly ["AGENTS.md", "content/templates/agents-entry.md", "content/skills/**/SKILL.md", "content/packs/**"];
|
|
5
|
+
/** Committed no-regression baseline relative to `xbrief/.eval/`. */
|
|
6
|
+
export declare const HEALTH_BASELINE_REL = "results/eval-health-baseline.json";
|
|
7
|
+
/** Result of verify:eval-health-relocation; three-state exit contract. */
|
|
8
|
+
export interface EvaluateResult {
|
|
9
|
+
readonly code: 0 | 1 | 2;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly stream: OutputStream;
|
|
12
|
+
/** True when the diff did not touch rule-relocation paths (gate skipped). */
|
|
13
|
+
readonly skipped?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface EvaluateOptions {
|
|
16
|
+
readonly projectRoot?: string;
|
|
17
|
+
/** Git ref for `git diff --name-only` (CI / branch comparison). */
|
|
18
|
+
readonly baseRef?: string;
|
|
19
|
+
/** Use `git diff --cached --name-only` (pre-commit / staged). */
|
|
20
|
+
readonly staged?: boolean;
|
|
21
|
+
/** Explicit path list (tests / overrides). */
|
|
22
|
+
readonly paths?: readonly string[];
|
|
23
|
+
readonly quiet?: boolean;
|
|
24
|
+
/** Write the current eval:health report as the committed baseline. */
|
|
25
|
+
readonly seedBaseline?: boolean;
|
|
26
|
+
/** Test hook: override eval:health collection. */
|
|
27
|
+
readonly healthEvaluator?: (projectRoot: string) => HealthReport | null;
|
|
28
|
+
}
|
|
29
|
+
/** Validate a parsed baseline JSON object before use. */
|
|
30
|
+
export declare function parseHealthBaseline(parsed: unknown): HealthReport | null;
|
|
31
|
+
/** True when *path* matches a rule-relocation home. */
|
|
32
|
+
export declare function isRuleRelocationPath(path: string): boolean;
|
|
33
|
+
/** Classify a path list for rule-relocation coverage. */
|
|
34
|
+
export declare function classifyRuleRelocationPaths(paths: readonly string[]): {
|
|
35
|
+
readonly isRelocation: boolean;
|
|
36
|
+
readonly matchedPaths: readonly string[];
|
|
37
|
+
};
|
|
38
|
+
/** Absolute path to the committed eval-health baseline snapshot. */
|
|
39
|
+
export declare function healthBaselinePath(projectRoot: string): string;
|
|
40
|
+
/** Read the committed baseline snapshot, if present. */
|
|
41
|
+
export declare function readHealthBaseline(projectRoot: string): HealthReport | null;
|
|
42
|
+
/** Persist a health report as the committed baseline (#2373 Wave 2 seed). */
|
|
43
|
+
export declare function writeHealthBaseline(projectRoot: string, report: HealthReport): void;
|
|
44
|
+
/** Collect changed paths from git (base ref or staged index). */
|
|
45
|
+
export declare function collectChangedPaths(projectRoot: string, options: {
|
|
46
|
+
baseRef?: string;
|
|
47
|
+
staged?: boolean;
|
|
48
|
+
}): string[] | {
|
|
49
|
+
error: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Detect eval:health regression against a committed baseline.
|
|
53
|
+
*
|
|
54
|
+
* Fail-closed rules (#2373):
|
|
55
|
+
* - score must not drop below baseline
|
|
56
|
+
* - gates that passed in the baseline must still pass
|
|
57
|
+
* - no new contradictory-gate ids beyond the baseline set
|
|
58
|
+
*/
|
|
59
|
+
export declare function detectHealthRegression(current: HealthReport, baseline: HealthReport): {
|
|
60
|
+
pass: boolean;
|
|
61
|
+
reasons: readonly string[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Conditional fail-closed gate for epic #2369 rule-relocation PRs (#2373).
|
|
65
|
+
*
|
|
66
|
+
* Skips (exit 0) when the diff does not touch relocation homes. When relocation
|
|
67
|
+
* paths are present, requires eval:health no-regression against the committed
|
|
68
|
+
* baseline at `xbrief/.eval/results/eval-health-baseline.json`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function evaluate(options?: EvaluateOptions): EvaluateResult;
|
|
71
|
+
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { evaluateHealth } from "../eval/health.js";
|
|
5
|
+
import { resolveEvalPath } from "../layout/resolve.js";
|
|
6
|
+
import { matchAny } from "../orchestration/pathspec.js";
|
|
7
|
+
/** Glob patterns that classify a change as epic #2369 rule-relocation (#2373). */
|
|
8
|
+
export const RULE_RELOCATION_PATH_PATTERNS = [
|
|
9
|
+
"AGENTS.md",
|
|
10
|
+
"content/templates/agents-entry.md",
|
|
11
|
+
"content/skills/**/SKILL.md",
|
|
12
|
+
"content/packs/**",
|
|
13
|
+
];
|
|
14
|
+
/** Committed no-regression baseline relative to `xbrief/.eval/`. */
|
|
15
|
+
export const HEALTH_BASELINE_REL = "results/eval-health-baseline.json";
|
|
16
|
+
/** Validate a parsed baseline JSON object before use. */
|
|
17
|
+
export function parseHealthBaseline(parsed) {
|
|
18
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const record = parsed;
|
|
22
|
+
if (typeof record.score !== "number" || !Array.isArray(record.gates)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(record.contradictions)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
/** True when *path* matches a rule-relocation home. */
|
|
31
|
+
export function isRuleRelocationPath(path) {
|
|
32
|
+
return matchAny(RULE_RELOCATION_PATH_PATTERNS, path);
|
|
33
|
+
}
|
|
34
|
+
/** Classify a path list for rule-relocation coverage. */
|
|
35
|
+
export function classifyRuleRelocationPaths(paths) {
|
|
36
|
+
const matchedPaths = paths.filter(isRuleRelocationPath);
|
|
37
|
+
return { isRelocation: matchedPaths.length > 0, matchedPaths };
|
|
38
|
+
}
|
|
39
|
+
/** Absolute path to the committed eval-health baseline snapshot. */
|
|
40
|
+
export function healthBaselinePath(projectRoot) {
|
|
41
|
+
return resolveEvalPath(projectRoot, HEALTH_BASELINE_REL);
|
|
42
|
+
}
|
|
43
|
+
/** Read the committed baseline snapshot, if present. */
|
|
44
|
+
export function readHealthBaseline(projectRoot) {
|
|
45
|
+
const path = healthBaselinePath(projectRoot);
|
|
46
|
+
if (!existsSync(path)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
51
|
+
return parseHealthBaseline(parsed);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Persist a health report as the committed baseline (#2373 Wave 2 seed). */
|
|
58
|
+
export function writeHealthBaseline(projectRoot, report) {
|
|
59
|
+
const path = healthBaselinePath(projectRoot);
|
|
60
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
61
|
+
writeFileSync(path, `${JSON.stringify(report)}\n`, "utf8");
|
|
62
|
+
}
|
|
63
|
+
function gitNameOnlyDiff(projectRoot, args) {
|
|
64
|
+
try {
|
|
65
|
+
const stdout = execFileSync("git", ["-C", projectRoot, "diff", "--name-only", ...args], {
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
68
|
+
});
|
|
69
|
+
return stdout
|
|
70
|
+
.split("\n")
|
|
71
|
+
.map((line) => line.trim())
|
|
72
|
+
.filter((line) => line.length > 0);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return { error: String(err) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Collect changed paths from git (base ref or staged index). */
|
|
79
|
+
export function collectChangedPaths(projectRoot, options) {
|
|
80
|
+
if (options.staged) {
|
|
81
|
+
return gitNameOnlyDiff(projectRoot, ["--cached"]);
|
|
82
|
+
}
|
|
83
|
+
if (options.baseRef !== undefined && options.baseRef.length > 0) {
|
|
84
|
+
return gitNameOnlyDiff(projectRoot, [options.baseRef, "HEAD"]);
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Detect eval:health regression against a committed baseline.
|
|
90
|
+
*
|
|
91
|
+
* Fail-closed rules (#2373):
|
|
92
|
+
* - score must not drop below baseline
|
|
93
|
+
* - gates that passed in the baseline must still pass
|
|
94
|
+
* - no new contradictory-gate ids beyond the baseline set
|
|
95
|
+
*/
|
|
96
|
+
export function detectHealthRegression(current, baseline) {
|
|
97
|
+
const reasons = [];
|
|
98
|
+
if (current.score < baseline.score) {
|
|
99
|
+
reasons.push(`framework health score dropped ${baseline.score}->${current.score}`);
|
|
100
|
+
}
|
|
101
|
+
const baselineGates = new Map(baseline.gates.filter((gate) => !gate.skipped).map((gate) => [gate.id, gate]));
|
|
102
|
+
for (const [id, baseGate] of baselineGates) {
|
|
103
|
+
if (!baseGate.pass) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const currentGate = current.gates.find((gate) => gate.id === id && !gate.skipped);
|
|
107
|
+
if (currentGate === undefined) {
|
|
108
|
+
reasons.push(`gate '${id}' missing from current eval:health report`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!currentGate.pass) {
|
|
112
|
+
reasons.push(`gate '${id}' regressed (baseline pass -> current fail)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const baselineContradictionIds = new Set(baseline.contradictions.map((c) => c.id));
|
|
116
|
+
for (const contradiction of current.contradictions) {
|
|
117
|
+
if (!baselineContradictionIds.has(contradiction.id)) {
|
|
118
|
+
reasons.push(`new contradictory gate '${contradiction.id}'`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { pass: reasons.length === 0, reasons };
|
|
122
|
+
}
|
|
123
|
+
function formatSkipMessage(matchedPaths) {
|
|
124
|
+
return ("✓ verify:eval-health-relocation: no rule-relocation paths in diff " +
|
|
125
|
+
`(checked ${RULE_RELOCATION_PATH_PATTERNS.length} patterns; ` +
|
|
126
|
+
`diff had ${matchedPaths.length} relocation match${matchedPaths.length === 1 ? "" : "es"}).`);
|
|
127
|
+
}
|
|
128
|
+
function formatPassMessage(score, matchedPaths) {
|
|
129
|
+
return (`✓ verify:eval-health-relocation: eval:health no-regression OK ` +
|
|
130
|
+
`(score=${score}/100; relocation paths: ${matchedPaths.join(", ")}).`);
|
|
131
|
+
}
|
|
132
|
+
function formatRegressionMessage(reasons, matchedPaths, baseline, current) {
|
|
133
|
+
const detail = reasons.map((reason) => ` - ${reason}`).join("\n");
|
|
134
|
+
return ("❌ verify:eval-health-relocation: eval:health regression on rule-relocation PR " +
|
|
135
|
+
`(#2373 / epic #2369).\n` +
|
|
136
|
+
` Baseline score: ${baseline.score}/100; current: ${current.score}/100.\n` +
|
|
137
|
+
` Matched relocation paths: ${matchedPaths.join(", ")}\n` +
|
|
138
|
+
`${detail}\n` +
|
|
139
|
+
" Remediation: restore gate pass states / score, or bump the committed baseline\n" +
|
|
140
|
+
` at ${HEALTH_BASELINE_REL} only after a deliberate, reviewed health change.\n` +
|
|
141
|
+
" Run `task eval:health` for the full gate breakdown.");
|
|
142
|
+
}
|
|
143
|
+
function runHealthEvaluator(projectRoot, healthEvaluator) {
|
|
144
|
+
if (healthEvaluator !== undefined) {
|
|
145
|
+
return healthEvaluator(projectRoot);
|
|
146
|
+
}
|
|
147
|
+
const healthResult = evaluateHealth({
|
|
148
|
+
projectRoot,
|
|
149
|
+
persist: false,
|
|
150
|
+
frameworkSource: true,
|
|
151
|
+
});
|
|
152
|
+
return healthResult.report;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Conditional fail-closed gate for epic #2369 rule-relocation PRs (#2373).
|
|
156
|
+
*
|
|
157
|
+
* Skips (exit 0) when the diff does not touch relocation homes. When relocation
|
|
158
|
+
* paths are present, requires eval:health no-regression against the committed
|
|
159
|
+
* baseline at `xbrief/.eval/results/eval-health-baseline.json`.
|
|
160
|
+
*/
|
|
161
|
+
export function evaluate(options = {}) {
|
|
162
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
163
|
+
const quiet = options.quiet ?? false;
|
|
164
|
+
if (options.seedBaseline) {
|
|
165
|
+
const report = runHealthEvaluator(projectRoot, options.healthEvaluator);
|
|
166
|
+
if (report === null) {
|
|
167
|
+
return {
|
|
168
|
+
code: 2,
|
|
169
|
+
message: "❌ verify:eval-health-relocation: eval:health returned no report.",
|
|
170
|
+
stream: "stderr",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
writeHealthBaseline(projectRoot, report);
|
|
174
|
+
return {
|
|
175
|
+
code: 0,
|
|
176
|
+
message: `✓ verify:eval-health-relocation: seeded baseline at ${HEALTH_BASELINE_REL} ` +
|
|
177
|
+
`(score=${report.score}/100).`,
|
|
178
|
+
stream: "stdout",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
let paths;
|
|
182
|
+
if (options.paths !== undefined) {
|
|
183
|
+
paths = [...options.paths];
|
|
184
|
+
}
|
|
185
|
+
else if (options.baseRef !== undefined || options.staged) {
|
|
186
|
+
const collected = collectChangedPaths(projectRoot, {
|
|
187
|
+
baseRef: options.baseRef,
|
|
188
|
+
staged: options.staged,
|
|
189
|
+
});
|
|
190
|
+
if ("error" in collected) {
|
|
191
|
+
return {
|
|
192
|
+
code: 2,
|
|
193
|
+
message: "❌ verify:eval-health-relocation: could not read git diff " +
|
|
194
|
+
`(project_root=${projectRoot}): ${collected.error}`,
|
|
195
|
+
stream: "stderr",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
paths = collected;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
return { code: 0, message: "", stream: "none", skipped: true };
|
|
202
|
+
}
|
|
203
|
+
const { isRelocation, matchedPaths } = classifyRuleRelocationPaths(paths);
|
|
204
|
+
if (!isRelocation) {
|
|
205
|
+
if (quiet) {
|
|
206
|
+
return { code: 0, message: "", stream: "none", skipped: true };
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
code: 0,
|
|
210
|
+
message: formatSkipMessage(matchedPaths),
|
|
211
|
+
stream: "stdout",
|
|
212
|
+
skipped: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const current = runHealthEvaluator(projectRoot, options.healthEvaluator);
|
|
216
|
+
if (current === null) {
|
|
217
|
+
return {
|
|
218
|
+
code: 2,
|
|
219
|
+
message: "❌ verify:eval-health-relocation: eval:health returned no report.",
|
|
220
|
+
stream: "stderr",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const baseline = readHealthBaseline(projectRoot);
|
|
224
|
+
if (baseline === null) {
|
|
225
|
+
return {
|
|
226
|
+
code: 2,
|
|
227
|
+
message: "❌ verify:eval-health-relocation: missing committed baseline " +
|
|
228
|
+
`at ${HEALTH_BASELINE_REL}.\n` +
|
|
229
|
+
" Seed once with: task verify:eval-health-relocation -- --seed-baseline\n" +
|
|
230
|
+
" (after `task eval:health` is green on master). Baseline seeding is intentional\n" +
|
|
231
|
+
" and does not weaken verify:agents-md-budget.",
|
|
232
|
+
stream: "stderr",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const regression = detectHealthRegression(current, baseline);
|
|
236
|
+
if (!regression.pass) {
|
|
237
|
+
return {
|
|
238
|
+
code: 1,
|
|
239
|
+
message: formatRegressionMessage(regression.reasons, matchedPaths, baseline, current),
|
|
240
|
+
stream: "stderr",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (quiet) {
|
|
244
|
+
return { code: 0, message: "", stream: "none" };
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
code: 0,
|
|
248
|
+
message: formatPassMessage(current.score, matchedPaths),
|
|
249
|
+
stream: "stdout",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=evaluate.js.map
|