@deftai/directive-core 0.79.0 → 0.79.2

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 (43) hide show
  1. package/dist/cache/fetch.js +14 -4
  2. package/dist/capacity/backfill.js +4 -2
  3. package/dist/category-b-namespace/index.js +31 -6
  4. package/dist/content-contracts/skills/skill-frontmatter.js +6 -2
  5. package/dist/eval/health.js +8 -1
  6. package/dist/eval/run.js +2 -0
  7. package/dist/fs/projection-containment.d.ts +11 -0
  8. package/dist/fs/projection-containment.js +62 -1
  9. package/dist/hooks/dispatcher.d.ts +14 -2
  10. package/dist/hooks/dispatcher.js +121 -7
  11. package/dist/init-deposit/hygiene.js +3 -0
  12. package/dist/intake/reconcile-issues.d.ts +1 -1
  13. package/dist/intake/reconcile-issues.js +31 -3
  14. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +1 -0
  15. package/dist/issue-sync/sync-from-xbrief-cli.js +3 -0
  16. package/dist/issue-sync/sync-from-xbrief.d.ts +4 -0
  17. package/dist/issue-sync/sync-from-xbrief.js +12 -0
  18. package/dist/lifecycle/event.d.ts +15 -0
  19. package/dist/lifecycle/event.js +163 -0
  20. package/dist/lifecycle/index.d.ts +1 -0
  21. package/dist/lifecycle/index.js +1 -0
  22. package/dist/orchestration/verify-judgment-gates.js +18 -2
  23. package/dist/pr-merge-readiness/compute.d.ts +2 -1
  24. package/dist/pr-merge-readiness/compute.js +23 -6
  25. package/dist/pr-merge-readiness/evaluate.d.ts +2 -1
  26. package/dist/pr-merge-readiness/evaluate.js +46 -31
  27. package/dist/pr-merge-readiness/greptile-inline.d.ts +27 -0
  28. package/dist/pr-merge-readiness/greptile-inline.js +239 -0
  29. package/dist/pr-merge-readiness/index.d.ts +1 -0
  30. package/dist/pr-merge-readiness/index.js +1 -0
  31. package/dist/pr-merge-readiness/mergeability.d.ts +2 -1
  32. package/dist/pr-merge-readiness/mergeability.js +7 -1
  33. package/dist/pr-wait-mergeable/wrappers.d.ts +9 -0
  34. package/dist/pr-wait-mergeable/wrappers.js +31 -2
  35. package/dist/release/constants.js +2 -0
  36. package/dist/scope/demote.js +20 -0
  37. package/dist/swarm/worktrees.d.ts +3 -0
  38. package/dist/swarm/worktrees.js +18 -2
  39. package/dist/task-surface/index.js +3 -2
  40. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +16 -8
  41. package/dist/vitest-runner/win32-coverage-tmp-setup.js +41 -14
  42. package/dist/xbrief-migrate/migration-containment.js +9 -25
  43. package/package.json +3 -3
@@ -0,0 +1,239 @@
1
+ import { detect } from "../content-contracts/skills/greptile-detector.js";
2
+ import { GREPTILE_LOGIN } from "./constants.js";
3
+ const REVIEW_THREADS_QUERY = `
4
+ query($owner: String!, $repo: String!, $pr: Int!, $after: String) {
5
+ repository(owner: $owner, name: $repo) {
6
+ pullRequest(number: $pr) {
7
+ reviewThreads(first: 100, after: $after) {
8
+ pageInfo { hasNextPage endCursor }
9
+ nodes {
10
+ isResolved
11
+ isOutdated
12
+ comments(first: 50) {
13
+ nodes {
14
+ author { login }
15
+ body
16
+ path
17
+ commit { oid }
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }`;
25
+ const EMPTY_INLINE = {
26
+ p0Count: 0,
27
+ p1Count: 0,
28
+ unresolvedThreadCount: 0,
29
+ error: null,
30
+ };
31
+ /** True when a review comment commit SHA matches the current PR head (#2620 AC-2). */
32
+ export function headShaMatches(commentSha, headSha) {
33
+ return headSha.startsWith(commentSha) || commentSha.startsWith(headSha);
34
+ }
35
+ export function inlineFindingsToDict(findings) {
36
+ return {
37
+ p0_count: findings.p0Count,
38
+ p1_count: findings.p1Count,
39
+ unresolved_thread_count: findings.unresolvedThreadCount,
40
+ error: findings.error,
41
+ };
42
+ }
43
+ function parseOwnerRepo(ownerRepo) {
44
+ const slash = ownerRepo.indexOf("/");
45
+ if (slash <= 0 || slash >= ownerRepo.length - 1) {
46
+ return null;
47
+ }
48
+ return { owner: ownerRepo.slice(0, slash), repo: ownerRepo.slice(slash + 1) };
49
+ }
50
+ function asRecord(value) {
51
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
52
+ return null;
53
+ }
54
+ return value;
55
+ }
56
+ function parseReviewComment(node) {
57
+ const record = asRecord(node);
58
+ if (record === null) {
59
+ return null;
60
+ }
61
+ const author = asRecord(record.author);
62
+ const login = author?.login;
63
+ if (typeof login !== "string" || login.length === 0) {
64
+ return null;
65
+ }
66
+ const body = record.body;
67
+ if (typeof body !== "string") {
68
+ return null;
69
+ }
70
+ const commit = asRecord(record.commit);
71
+ const oid = commit?.oid;
72
+ const path = record.path;
73
+ return {
74
+ authorLogin: login,
75
+ body,
76
+ path: typeof path === "string" ? path : null,
77
+ commitOid: typeof oid === "string" && oid.length > 0 ? oid : null,
78
+ };
79
+ }
80
+ function parseReviewThread(node) {
81
+ const record = asRecord(node);
82
+ if (record === null) {
83
+ return null;
84
+ }
85
+ const commentsBlock = asRecord(record.comments);
86
+ const commentNodes = commentsBlock?.nodes;
87
+ const comments = [];
88
+ if (Array.isArray(commentNodes)) {
89
+ for (const commentNode of commentNodes) {
90
+ const parsed = parseReviewComment(commentNode);
91
+ if (parsed !== null) {
92
+ comments.push(parsed);
93
+ }
94
+ }
95
+ }
96
+ return {
97
+ isResolved: record.isResolved === true,
98
+ isOutdated: record.isOutdated === true,
99
+ comments,
100
+ };
101
+ }
102
+ function parseGraphqlReviewThreadsPage(stdout) {
103
+ if (!stdout.trim()) {
104
+ return { threads: [], hasNextPage: false, endCursor: null, error: "empty GraphQL body" };
105
+ }
106
+ let payload;
107
+ try {
108
+ payload = JSON.parse(stdout);
109
+ }
110
+ catch (exc) {
111
+ const message = exc instanceof Error ? exc.message : String(exc);
112
+ return {
113
+ threads: [],
114
+ hasNextPage: false,
115
+ endCursor: null,
116
+ error: `could not parse GraphQL JSON: ${message}`,
117
+ };
118
+ }
119
+ const root = asRecord(payload);
120
+ const errors = root?.errors;
121
+ if (Array.isArray(errors) && errors.length > 0) {
122
+ const first = errors[0];
123
+ const message = typeof first === "object" && first !== null && "message" in first
124
+ ? String(first.message)
125
+ : "GraphQL errors present";
126
+ return { threads: [], hasNextPage: false, endCursor: null, error: message };
127
+ }
128
+ const data = asRecord(root?.data);
129
+ const repository = asRecord(data?.repository);
130
+ const pullRequest = asRecord(repository?.pullRequest);
131
+ const reviewThreads = asRecord(pullRequest?.reviewThreads);
132
+ const nodes = reviewThreads?.nodes;
133
+ const pageInfo = asRecord(reviewThreads?.pageInfo);
134
+ const threads = [];
135
+ if (Array.isArray(nodes)) {
136
+ for (const node of nodes) {
137
+ const parsed = parseReviewThread(node);
138
+ if (parsed !== null) {
139
+ threads.push(parsed);
140
+ }
141
+ }
142
+ }
143
+ return {
144
+ threads,
145
+ hasNextPage: pageInfo?.hasNextPage === true,
146
+ endCursor: typeof pageInfo?.endCursor === "string" ? pageInfo.endCursor : null,
147
+ error: null,
148
+ };
149
+ }
150
+ /** Score unresolved Greptile inline P0/P1 threads pinned to the current HEAD (#2620). */
151
+ export function evaluateInlineReviewThreads(threads, headSha) {
152
+ let p0Count = 0;
153
+ let p1Count = 0;
154
+ let unresolvedThreadCount = 0;
155
+ for (const thread of threads) {
156
+ if (thread.isResolved || thread.isOutdated) {
157
+ continue;
158
+ }
159
+ let threadP0 = 0;
160
+ let threadP1 = 0;
161
+ for (const comment of thread.comments) {
162
+ if (comment.authorLogin !== GREPTILE_LOGIN) {
163
+ continue;
164
+ }
165
+ if (comment.commitOid === null || !headShaMatches(comment.commitOid, headSha)) {
166
+ continue;
167
+ }
168
+ const findings = detect(comment.body);
169
+ threadP0 += findings.p0_count;
170
+ threadP1 += findings.p1_count;
171
+ }
172
+ if (threadP0 + threadP1 > 0) {
173
+ p0Count += threadP0;
174
+ p1Count += threadP1;
175
+ unresolvedThreadCount += 1;
176
+ }
177
+ }
178
+ return { p0Count, p1Count, unresolvedThreadCount, error: null };
179
+ }
180
+ /** Fetch unresolved Greptile inline P0/P1 on the current HEAD via reviewThreads GraphQL (#2620). */
181
+ export function fetchUnresolvedGreptileInlineFindings(prNumber, repo, headSha, runGh) {
182
+ const parsed = parseOwnerRepo(repo);
183
+ if (parsed === null) {
184
+ return { ...EMPTY_INLINE, error: `invalid repo: ${repo}` };
185
+ }
186
+ const allThreads = [];
187
+ let after = null;
188
+ const maxPages = 10;
189
+ let hasNextPage = true;
190
+ for (let page = 0; hasNextPage && page < maxPages; page += 1) {
191
+ const cmd = [
192
+ "gh",
193
+ "api",
194
+ "graphql",
195
+ "-f",
196
+ `query=${REVIEW_THREADS_QUERY}`,
197
+ "-f",
198
+ `owner=${parsed.owner}`,
199
+ "-f",
200
+ `repo=${parsed.repo}`,
201
+ "-F",
202
+ `pr=${String(prNumber)}`,
203
+ ];
204
+ if (after !== null) {
205
+ cmd.push("-f", `after=${after}`);
206
+ }
207
+ const rc = runGh(cmd);
208
+ if (rc.returncode !== 0) {
209
+ return {
210
+ ...EMPTY_INLINE,
211
+ error: `graphql reviewThreads failed: ${rc.stderr.trim() || rc.stdout.trim()}`,
212
+ };
213
+ }
214
+ const pageResult = parseGraphqlReviewThreadsPage(rc.stdout);
215
+ if (pageResult.error !== null) {
216
+ return { ...EMPTY_INLINE, error: pageResult.error };
217
+ }
218
+ allThreads.push(...pageResult.threads);
219
+ hasNextPage = pageResult.hasNextPage;
220
+ if (!hasNextPage) {
221
+ break;
222
+ }
223
+ if (pageResult.endCursor === null) {
224
+ return {
225
+ ...EMPTY_INLINE,
226
+ error: "graphql reviewThreads pagination missing endCursor while hasNextPage=true",
227
+ };
228
+ }
229
+ after = pageResult.endCursor;
230
+ }
231
+ if (hasNextPage) {
232
+ return {
233
+ ...EMPTY_INLINE,
234
+ error: `graphql reviewThreads pagination exceeded ${maxPages} pages`,
235
+ };
236
+ }
237
+ return evaluateInlineReviewThreads(allThreads, headSha);
238
+ }
239
+ //# sourceMappingURL=greptile-inline.js.map
@@ -3,6 +3,7 @@ export { type ComputeGateOptions, computeGateResult, type FetchMergeabilityFn }
3
3
  export * from "./constants.js";
4
4
  export { evaluateGates, isMergeReady } from "./evaluate.js";
5
5
  export { defaultRunGh } from "./gh.js";
6
+ export { evaluateInlineReviewThreads, fetchUnresolvedGreptileInlineFindings, headShaMatches, type InlineGreptileFindings, type InlineReviewComment, type InlineReviewThread, inlineFindingsToDict, } from "./greptile-inline.js";
6
7
  export { cmdPrMergeReadiness, parseArgs, run } from "./main.js";
7
8
  export { fetchMergeability, isGithubMergeableClean, MERGE_STATE_CLEAN, type MergeabilitySignal, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
8
9
  export { emitJson, exitCodeFor, gateResultToDict, printHuman } from "./output.js";
@@ -3,6 +3,7 @@ export { computeGateResult } from "./compute.js";
3
3
  export * from "./constants.js";
4
4
  export { evaluateGates, isMergeReady } from "./evaluate.js";
5
5
  export { defaultRunGh } from "./gh.js";
6
+ export { evaluateInlineReviewThreads, fetchUnresolvedGreptileInlineFindings, headShaMatches, inlineFindingsToDict, } from "./greptile-inline.js";
6
7
  export { cmdPrMergeReadiness, parseArgs, run } from "./main.js";
7
8
  export { fetchMergeability, isGithubMergeableClean, MERGE_STATE_CLEAN, mergeabilityToDict, verdictBlockIsSoftOnly, verdictShaIsStale, } from "./mergeability.js";
8
9
  export { emitJson, exitCodeFor, gateResultToDict, printHuman } from "./output.js";
@@ -1,3 +1,4 @@
1
+ import type { InlineGreptileFindings } from "./greptile-inline.js";
1
2
  import type { GreptileVerdict, RunGhFn } from "./types.js";
2
3
  /**
3
4
  * GitHub's authoritative mergeability signal, read over REST (#2260).
@@ -41,5 +42,5 @@ export declare function verdictShaIsStale(verdict: GreptileVerdict, headSha: str
41
42
  * regardless of GitHub mergeability (guardrail: do not merge a PR with a real
42
43
  * P0/P1 review finding).
43
44
  */
44
- export declare function verdictBlockIsSoftOnly(verdict: GreptileVerdict, headSha: string | null): boolean;
45
+ export declare function verdictBlockIsSoftOnly(verdict: GreptileVerdict, headSha: string | null, inline?: InlineGreptileFindings | null): boolean;
45
46
  //# sourceMappingURL=mergeability.d.ts.map
@@ -75,7 +75,13 @@ export function verdictShaIsStale(verdict, headSha) {
75
75
  * regardless of GitHub mergeability (guardrail: do not merge a PR with a real
76
76
  * P0/P1 review finding).
77
77
  */
78
- export function verdictBlockIsSoftOnly(verdict, headSha) {
78
+ export function verdictBlockIsSoftOnly(verdict, headSha, inline = null) {
79
+ if (inline !== null && inline.error !== null) {
80
+ return false;
81
+ }
82
+ if (inline !== null && inline.error === null && (inline.p0Count > 0 || inline.p1Count > 0)) {
83
+ return false;
84
+ }
79
85
  // Absent: no Greptile rolling-summary comment at all.
80
86
  if (!verdict.found) {
81
87
  return true;
@@ -17,6 +17,15 @@ export interface CaptureExecOptions {
17
17
  }
18
18
  /** UTF-8-safe subprocess capture via spawnSync (no shell) — mirrors #1366. */
19
19
  export declare function captureExec(executable: string, args: readonly string[], timeoutMs: number, options?: CaptureExecOptions): CaptureExecResult;
20
+ /**
21
+ * Resolve a CLI entry script for subprocess spawn (#2615).
22
+ *
23
+ * Published npm layout nests `@deftai/directive-core` under `@deftai/directive`,
24
+ * so the old `../../../cli/dist` relative path resolved to a non-existent
25
+ * `@deftai/cli` sibling. Prefer the published package root, then monorepo path,
26
+ * then nested `@deftai/directive/dist`.
27
+ */
28
+ export declare function cliScriptPath(name: string): string;
20
29
  export interface RunProtectedCheckOptions {
21
30
  readonly nodeExecutable?: string;
22
31
  readonly timeout?: number;
@@ -1,4 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
2
4
  import { dirname, resolve } from "node:path";
3
5
  import { fileURLToPath } from "node:url";
4
6
  import { resolveBinary } from "../scm/binary.js";
@@ -40,9 +42,36 @@ export function captureExec(executable, args, timeoutMs, options = {}) {
40
42
  stderr: typeof result.stderr === "string" ? result.stderr : "",
41
43
  };
42
44
  }
43
- function cliScriptPath(name) {
45
+ /**
46
+ * Resolve a CLI entry script for subprocess spawn (#2615).
47
+ *
48
+ * Published npm layout nests `@deftai/directive-core` under `@deftai/directive`,
49
+ * so the old `../../../cli/dist` relative path resolved to a non-existent
50
+ * `@deftai/cli` sibling. Prefer the published package root, then monorepo path,
51
+ * then nested `@deftai/directive/dist`.
52
+ */
53
+ export function cliScriptPath(name) {
54
+ const script = `${name}.js`;
44
55
  const here = dirname(fileURLToPath(import.meta.url));
45
- return resolve(here, "../../../cli/dist", `${name}.js`);
56
+ const candidates = [];
57
+ try {
58
+ const require = createRequire(import.meta.url);
59
+ const pkgJson = require.resolve("@deftai/directive/package.json");
60
+ candidates.push(resolve(dirname(pkgJson), "dist", script));
61
+ }
62
+ catch {
63
+ // Core does not declare a runtime dependency on the CLI package; ignore.
64
+ }
65
+ // Monorepo: packages/core/dist/pr-wait-mergeable -> packages/cli/dist
66
+ candidates.push(resolve(here, "../../../cli/dist", script));
67
+ // npm nested: .../directive/node_modules/@deftai/directive-core/dist/... -> .../directive/dist
68
+ candidates.push(resolve(here, "../../../../dist", script));
69
+ for (const candidate of candidates) {
70
+ if (existsSync(candidate))
71
+ return candidate;
72
+ }
73
+ // ENOENT path for actionable errors — monorepo layout is the stable fallback.
74
+ return resolve(here, "../../../cli/dist", script);
46
75
  }
47
76
  /** Invoke pr-protected-issues CLI and return (returncode, stdout, stderr). */
48
77
  export function runProtectedCheck(prNumber, repo, protectedIssues, options = {}) {
@@ -64,6 +64,8 @@ export const RELEASE_HELP = "usage: release [-h] [--dry-run] [--skip-tag] [--ski
64
64
  " Acknowledge a hairline coverage miss: Step 5 passes\n" +
65
65
  " only when metrics sit below the 85% goal and this\n" +
66
66
  " flag cites an operator-owned issue number (#2573).\n" +
67
+ " PowerShell: use --allow-coverage-debt=N (no bare #)\n" +
68
+ ' or quote "#N"; unquoted # starts a comment (#2621).\n' +
67
69
  " --skip-ci Skip Step 3 (task ci:local / task check fallback).\n" +
68
70
  " Used by `task release:e2e` to keep wall-clock\n" +
69
71
  " manageable inside the auto-created temp repo (CI\n" +
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
2
2
  import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
3
4
  import { hasArtifactSuffix, resolveLifecycleFolder } from "../layout/resolve.js";
4
5
  import { stripTrailingPathSeparators } from "../text/redos-safe.js";
5
6
  import { append, canonicalLogPath, latestForPath, newDecisionId } from "./audit-log.js";
@@ -51,6 +52,15 @@ export function demoteOne(filePath, projectRoot, reason, options = {}) {
51
52
  auditEntry: null,
52
53
  };
53
54
  }
55
+ try {
56
+ assertWriteTargetSafe(projectRoot, resolved);
57
+ }
58
+ catch (err) {
59
+ if (err instanceof ProjectionContainmentError) {
60
+ return { ok: false, message: err.message, auditEntry: null };
61
+ }
62
+ throw err;
63
+ }
54
64
  let data;
55
65
  try {
56
66
  data = JSON.parse(readFileSync(resolved, "utf8"));
@@ -129,6 +139,16 @@ export function batchDemote(projectRoot, olderThanDays, options = {}) {
129
139
  .sort();
130
140
  for (const name of files) {
131
141
  const candidate = join(pendingDir, name);
142
+ try {
143
+ assertWriteTargetSafe(projectRoot, candidate);
144
+ }
145
+ catch (err) {
146
+ if (err instanceof ProjectionContainmentError) {
147
+ skipped.push(`${name}: ${err.message}`);
148
+ continue;
149
+ }
150
+ throw err;
151
+ }
132
152
  let data;
133
153
  try {
134
154
  data = JSON.parse(readFileSync(candidate, "utf8"));
@@ -18,6 +18,9 @@ export declare class DuplicateStoryError extends WorktreeMapError {
18
18
  export declare class WorktreeMapConfigError extends Error {
19
19
  name: string;
20
20
  }
21
+ export declare class WorktreePathEscapeError extends WorktreeMapError {
22
+ name: string;
23
+ }
21
24
  export type GitRunner = (args: readonly string[], cwd: string) => TextCaptureResult;
22
25
  export declare const defaultGitRunner: GitRunner;
23
26
  /** Case-normalized comparison key for worktree-path equality. */
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, readFileSync } from "node:fs";
2
- import { resolve as pathResolve } from "node:path";
2
+ import { isAbsolute, resolve as pathResolve } from "node:path";
3
+ import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
3
4
  import { C3_FIELDS } from "./constants.js";
4
5
  import { runText } from "./subprocess.js";
5
6
  export class WorktreeMapError extends Error {
@@ -20,11 +21,25 @@ export class DuplicateStoryError extends WorktreeMapError {
20
21
  export class WorktreeMapConfigError extends Error {
21
22
  name = "WorktreeMapConfigError";
22
23
  }
24
+ export class WorktreePathEscapeError extends WorktreeMapError {
25
+ name = "WorktreePathEscapeError";
26
+ }
23
27
  export const defaultGitRunner = (args, cwd) => runText(["git", ...args], { cwd });
24
28
  function resolvePath(raw, repoRoot) {
25
- const candidate = raw.startsWith("/") ? raw : pathResolve(repoRoot, raw);
29
+ const candidate = isAbsolute(raw) ? raw : pathResolve(repoRoot, raw);
26
30
  return pathResolve(candidate);
27
31
  }
32
+ function assertWorktreePathContained(repoRoot, worktreePath) {
33
+ try {
34
+ assertProjectionContained(repoRoot, worktreePath);
35
+ }
36
+ catch (err) {
37
+ if (err instanceof ProjectionContainmentError) {
38
+ throw new WorktreePathEscapeError(`worktree_path must stay under the repository root: ${err.message}`);
39
+ }
40
+ throw err;
41
+ }
42
+ }
28
43
  /** Case-normalized comparison key for worktree-path equality. */
29
44
  export function compareKey(pathStr) {
30
45
  return pathStr.replace(/\\/g, "/").toLowerCase();
@@ -119,6 +134,7 @@ export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, op
119
134
  }
120
135
  }
121
136
  const worktreePath = resolvePath(rawPath.trim(), root);
137
+ assertWorktreePathContained(root, worktreePath);
122
138
  const key = compareKey(worktreePath);
123
139
  const posixPath = worktreePath.replace(/\\/g, "/");
124
140
  if (seenPaths.has(key)) {
@@ -1,13 +1,14 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
+ import { LEGACY_VBRIEF_VERSION } from "@deftai/directive-types";
4
5
  const UNRELEASED_RE = /## \[Unreleased\][ \t]*\n([\s\S]*?)(?=\n## \[|$)/;
5
6
  const CHANGE_NAME_RE = /^[\w][\w-]*$/;
6
7
  const COMMIT_TYPES = "feat|fix|docs|chore|refactor|test|style|perf|ci|build|revert";
7
8
  const COMMIT_SUBJECT_RE = new RegExp(`^(${COMMIT_TYPES})(\\(.+\\))?!?: .+`);
8
9
  function proposalTemplate(name) {
9
10
  return {
10
- vBRIEFInfo: { version: "0.5" },
11
+ vBRIEFInfo: { version: LEGACY_VBRIEF_VERSION },
11
12
  plan: {
12
13
  title: name,
13
14
  status: "draft",
@@ -26,7 +27,7 @@ function proposalTemplate(name) {
26
27
  }
27
28
  function tasksTemplate(name) {
28
29
  return {
29
- vBRIEFInfo: { version: "0.5" },
30
+ vBRIEFInfo: { version: LEGACY_VBRIEF_VERSION },
30
31
  plan: { title: name, status: "draft", items: [], edges: [] },
31
32
  };
32
33
  }
@@ -1,12 +1,20 @@
1
+ /** Matches Vitest v8 chunk paths such as coverage/.tmp/coverage-0.json (#2580 / #2634). */
2
+ export declare const COVERAGE_TMP_CHUNK_RE: RegExp;
3
+ export declare function isCoverageTmpChunkPath(filePath: string): boolean;
4
+ export declare function ensureCoverageTmpDir(coverageTmpDir?: string): void;
1
5
  /**
2
- * Win32 globalSetup for coverage runs (#2580).
6
+ * Vitest 3.2.x writes coverage chunks without mkdir'ing the parent directory
7
+ * (fixed upstream in vitest 4.x — vitest-dev/vitest#10117). On Windows the
8
+ * directory can disappear between clean() and writeFile under release-scale
9
+ * load; mkdir immediately before chunk writes closes that race without
10
+ * soft-failing real threshold failures (#2634).
11
+ */
12
+ export declare function installCoverageTmpWriteGuard(): () => void;
13
+ /**
14
+ * Win32 globalSetup for coverage runs (#2580, hardened #2634).
3
15
  *
4
- * Vitest's v8 provider writes per-suite JSON under coverage/.tmp without always
5
- * re-mkdir'ing before writeFile. Under parallel fork load the directory can
6
- * disappear mid-suite, surfacing as ENOENT after an otherwise green run.
7
- * Keep the directory present for the coordinator process; late ENOENT flakes
8
- * are tolerated via vitest dangerouslyIgnoreUnhandledErrors (#2546) without
9
- * soft-failing real coverage threshold failures.
16
+ * Keepalive mkdir plus a writeFile guard mirror vitest 4's defensive mkdir until
17
+ * directive upgrades past vitest@3 (see vitest.config.ts #2634 upgrade note).
10
18
  */
11
- export default function setup(): void;
19
+ export default function setup(): () => void;
12
20
  //# sourceMappingURL=win32-coverage-tmp-setup.d.ts.map
@@ -1,24 +1,51 @@
1
- import { mkdirSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- const COVERAGE_TMP = resolve(process.cwd(), "coverage", ".tmp");
4
- function ensureCoverageTmpDir() {
5
- mkdirSync(COVERAGE_TMP, { recursive: true });
1
+ import { promises as fsPromises, mkdirSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ /** Matches Vitest v8 chunk paths such as coverage/.tmp/coverage-0.json (#2580 / #2634). */
4
+ export const COVERAGE_TMP_CHUNK_RE = /[/\\]coverage[/\\]\.tmp[/\\]coverage-\d+\.json$/;
5
+ const defaultCoverageTmp = resolve(process.cwd(), "coverage", ".tmp");
6
+ export function isCoverageTmpChunkPath(filePath) {
7
+ return COVERAGE_TMP_CHUNK_RE.test(filePath.replace(/\\/g, "/"));
8
+ }
9
+ export function ensureCoverageTmpDir(coverageTmpDir = defaultCoverageTmp) {
10
+ mkdirSync(coverageTmpDir, { recursive: true });
11
+ }
12
+ /**
13
+ * Vitest 3.2.x writes coverage chunks without mkdir'ing the parent directory
14
+ * (fixed upstream in vitest 4.x — vitest-dev/vitest#10117). On Windows the
15
+ * directory can disappear between clean() and writeFile under release-scale
16
+ * load; mkdir immediately before chunk writes closes that race without
17
+ * soft-failing real threshold failures (#2634).
18
+ */
19
+ export function installCoverageTmpWriteGuard() {
20
+ const originalWriteFile = fsPromises.writeFile.bind(fsPromises);
21
+ const patchedWriteFile = async (path, ...args) => {
22
+ const target = typeof path === "string" ? path : String(path);
23
+ if (isCoverageTmpChunkPath(target)) {
24
+ mkdirSync(dirname(target), { recursive: true });
25
+ }
26
+ return originalWriteFile(path, ...args);
27
+ };
28
+ fsPromises.writeFile = patchedWriteFile;
29
+ return () => {
30
+ fsPromises.writeFile = originalWriteFile;
31
+ };
6
32
  }
7
33
  /**
8
- * Win32 globalSetup for coverage runs (#2580).
34
+ * Win32 globalSetup for coverage runs (#2580, hardened #2634).
9
35
  *
10
- * Vitest's v8 provider writes per-suite JSON under coverage/.tmp without always
11
- * re-mkdir'ing before writeFile. Under parallel fork load the directory can
12
- * disappear mid-suite, surfacing as ENOENT after an otherwise green run.
13
- * Keep the directory present for the coordinator process; late ENOENT flakes
14
- * are tolerated via vitest dangerouslyIgnoreUnhandledErrors (#2546) without
15
- * soft-failing real coverage threshold failures.
36
+ * Keepalive mkdir plus a writeFile guard mirror vitest 4's defensive mkdir until
37
+ * directive upgrades past vitest@3 (see vitest.config.ts #2634 upgrade note).
16
38
  */
17
39
  export default function setup() {
18
40
  if (process.platform !== "win32")
19
- return;
41
+ return () => { };
20
42
  ensureCoverageTmpDir();
21
- const keepalive = setInterval(ensureCoverageTmpDir, 100);
43
+ const uninstallWriteGuard = installCoverageTmpWriteGuard();
44
+ const keepalive = setInterval(() => ensureCoverageTmpDir(), 50);
22
45
  keepalive.unref?.();
46
+ return () => {
47
+ clearInterval(keepalive);
48
+ uninstallWriteGuard();
49
+ };
23
50
  }
24
51
  //# sourceMappingURL=win32-coverage-tmp-setup.js.map
@@ -1,37 +1,21 @@
1
- import { lstatSync, readdirSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { assertProjectionContained } from "../fs/projection-containment.js";
1
+ import { assertProjectionContained, ProjectionContainmentError, walkDirectoryRejectSymlinks, } from "../fs/projection-containment.js";
4
2
  /**
5
3
  * Refuse migrate:xbrief when legacy `vbrief/` (or any traversed entry) escapes
6
4
  * the project tree via symlinks (#2601).
7
5
  */
8
6
  export function assertMigrationSourceSafe(projectRoot, legacyDir) {
9
7
  assertProjectionContained(projectRoot, legacyDir);
10
- walkMigrationTreeRejectSymlinks(legacyDir);
11
- }
12
- function walkMigrationTreeRejectSymlinks(root) {
13
- let entries;
14
8
  try {
15
- entries = readdirSync(root, { withFileTypes: true });
16
- }
17
- catch {
18
- return;
9
+ walkDirectoryRejectSymlinks(legacyDir);
19
10
  }
20
- for (const entry of entries) {
21
- const full = join(root, entry.name);
22
- let info;
23
- try {
24
- info = lstatSync(full);
25
- }
26
- catch {
27
- continue;
28
- }
29
- if (info.isSymbolicLink()) {
30
- throw new Error(`refusing to migrate: symlink on migration path: ${full}`);
31
- }
32
- if (info.isDirectory()) {
33
- walkMigrationTreeRejectSymlinks(full);
11
+ catch (err) {
12
+ if (err instanceof ProjectionContainmentError) {
13
+ const nested = err.message.match(/symlink on traversal path: (.+)$/);
14
+ if (nested) {
15
+ throw new Error(`refusing to migrate: symlink on migration path: ${nested[1]}`);
16
+ }
34
17
  }
18
+ throw err;
35
19
  }
36
20
  }
37
21
  //# sourceMappingURL=migration-containment.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.79.0",
3
+ "version": "0.79.2",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -305,8 +305,8 @@
305
305
  "provenance": true
306
306
  },
307
307
  "dependencies": {
308
- "@deftai/directive-content": "^0.79.0",
309
- "@deftai/directive-types": "^0.79.0",
308
+ "@deftai/directive-content": "^0.79.2",
309
+ "@deftai/directive-types": "^0.79.2",
310
310
  "archiver": "^8.0.0"
311
311
  },
312
312
  "scripts": {