@deftai/directive-core 0.77.0 → 0.79.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 (126) hide show
  1. package/dist/cache/empty-populate.d.ts +44 -0
  2. package/dist/cache/empty-populate.js +117 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/check-updates/index.d.ts +10 -0
  6. package/dist/check-updates/index.js +81 -1
  7. package/dist/doctor/main.d.ts +1 -0
  8. package/dist/doctor/main.js +48 -0
  9. package/dist/doctor/payload-staleness.js +7 -1
  10. package/dist/doctor/types.d.ts +3 -0
  11. package/dist/eval/triggers.d.ts +76 -0
  12. package/dist/eval/triggers.js +259 -0
  13. package/dist/eval-triggers-relocation/evaluate.d.ts +36 -0
  14. package/dist/eval-triggers-relocation/evaluate.js +115 -0
  15. package/dist/eval-triggers-relocation/index.d.ts +2 -0
  16. package/dist/eval-triggers-relocation/index.js +2 -0
  17. package/dist/hooks/dispatcher.d.ts +43 -0
  18. package/dist/hooks/dispatcher.js +173 -0
  19. package/dist/hooks/index.d.ts +3 -0
  20. package/dist/hooks/index.js +3 -0
  21. package/dist/hooks/scope.d.ts +12 -0
  22. package/dist/hooks/scope.js +46 -0
  23. package/dist/hooks/tools.d.ts +4 -0
  24. package/dist/hooks/tools.js +23 -0
  25. package/dist/init-deposit/agent-hooks.d.ts +22 -0
  26. package/dist/init-deposit/agent-hooks.js +238 -0
  27. package/dist/init-deposit/hygiene.js +4 -0
  28. package/dist/init-deposit/index.d.ts +1 -0
  29. package/dist/init-deposit/index.js +1 -0
  30. package/dist/init-deposit/init-deposit.js +4 -0
  31. package/dist/init-deposit/prettierignore.d.ts +21 -0
  32. package/dist/init-deposit/prettierignore.js +83 -0
  33. package/dist/init-deposit/refresh.d.ts +3 -1
  34. package/dist/init-deposit/refresh.js +31 -40
  35. package/dist/init-deposit/scaffold.js +6 -12
  36. package/dist/init-deposit/xbrief-projections.d.ts +18 -0
  37. package/dist/init-deposit/xbrief-projections.js +117 -0
  38. package/dist/intake/reconcile-issues.d.ts +11 -0
  39. package/dist/intake/reconcile-issues.js +166 -51
  40. package/dist/issue-sync/index.d.ts +2 -0
  41. package/dist/issue-sync/index.js +2 -0
  42. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
  43. package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
  44. package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
  45. package/dist/issue-sync/sync-from-xbrief.js +208 -0
  46. package/dist/platform/index.d.ts +1 -0
  47. package/dist/platform/index.js +1 -0
  48. package/dist/platform/platform-capabilities.d.ts +5 -0
  49. package/dist/platform/platform-capabilities.js +9 -0
  50. package/dist/platform/shell-context.d.ts +30 -0
  51. package/dist/platform/shell-context.js +111 -0
  52. package/dist/pr-monitor/cadence.d.ts +6 -0
  53. package/dist/pr-monitor/cadence.js +13 -0
  54. package/dist/pr-monitor/index.d.ts +1 -1
  55. package/dist/pr-monitor/index.js +1 -1
  56. package/dist/pr-monitor/monitor.d.ts +7 -0
  57. package/dist/pr-monitor/monitor.js +59 -13
  58. package/dist/preflight-cache/evaluate.d.ts +3 -8
  59. package/dist/preflight-cache/evaluate.js +19 -19
  60. package/dist/release/build-dist.js +1 -0
  61. package/dist/release/constants.d.ts +2 -0
  62. package/dist/release/constants.js +8 -1
  63. package/dist/release/flags.js +26 -0
  64. package/dist/release/issue-state-fetch.d.ts +27 -0
  65. package/dist/release/issue-state-fetch.js +146 -0
  66. package/dist/release/main.js +1 -0
  67. package/dist/release/native-steps.js +6 -4
  68. package/dist/release/pipeline.js +15 -5
  69. package/dist/release/preflight.d.ts +6 -2
  70. package/dist/release/preflight.js +19 -5
  71. package/dist/release/types.d.ts +3 -1
  72. package/dist/release-e2e/constants.js +6 -6
  73. package/dist/release-e2e/flags.d.ts +2 -0
  74. package/dist/release-e2e/flags.js +8 -4
  75. package/dist/release-e2e/greenfield-python-free-smoke-cli.js +40 -1
  76. package/dist/release-e2e/greenfield-python-free-smoke.d.ts +2 -0
  77. package/dist/release-e2e/greenfield-python-free-smoke.js +28 -8
  78. package/dist/release-e2e/main.js +15 -9
  79. package/dist/release-e2e/types.d.ts +4 -2
  80. package/dist/resolution/classify.d.ts +1 -1
  81. package/dist/resolution/classify.js +36 -11
  82. package/dist/scope/project-definition-sync.js +2 -150
  83. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  84. package/dist/scope/registry-artifact-sync.js +159 -0
  85. package/dist/scope/specification-sync.d.ts +3 -0
  86. package/dist/scope/specification-sync.js +16 -0
  87. package/dist/scope/transition.js +40 -12
  88. package/dist/session/cache-recovery.d.ts +19 -0
  89. package/dist/session/cache-recovery.js +26 -0
  90. package/dist/session/session-start.d.ts +2 -0
  91. package/dist/session/session-start.js +14 -3
  92. package/dist/session/verify-session-ritual.d.ts +17 -0
  93. package/dist/session/verify-session-ritual.js +64 -0
  94. package/dist/triage/help/registry-data.d.ts +12 -1
  95. package/dist/triage/help/registry-data.js +22 -1
  96. package/dist/triage/welcome/default-mode.js +2 -0
  97. package/dist/triage/welcome/onboard.js +2 -0
  98. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  99. package/dist/ts-check-lane/run-lane.js +9 -0
  100. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  101. package/dist/vbrief-reconcile/labels.js +14 -0
  102. package/dist/vbrief-reconcile/main.d.ts +1 -0
  103. package/dist/vbrief-reconcile/main.js +13 -3
  104. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  105. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  106. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  107. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  108. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  109. package/dist/verify-env/agent-hooks.d.ts +11 -0
  110. package/dist/verify-env/agent-hooks.js +46 -0
  111. package/dist/verify-env/command-spawn.d.ts +5 -0
  112. package/dist/verify-env/command-spawn.js +28 -9
  113. package/dist/verify-env/index.d.ts +1 -0
  114. package/dist/verify-env/index.js +1 -0
  115. package/dist/verify-env/verify-hooks-installed.js +5 -5
  116. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  117. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  118. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  119. package/dist/vitest-runner/coverage-debt.js +155 -0
  120. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  121. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  122. package/dist/xbrief-migrate/detect.js +46 -1
  123. package/dist/xbrief-migrate/migrate-project.js +19 -4
  124. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  125. package/dist/xbrief-migrate/migration-containment.js +37 -0
  126. package/package.json +19 -3
@@ -0,0 +1,33 @@
1
+ import { resolveRepo } from "../triage/queue/repo.js";
2
+ /** Normalize owner/name slugs for case-insensitive comparison. */
3
+ export function normalizeRepoSlug(repo) {
4
+ return repo.trim().toLowerCase();
5
+ }
6
+ /**
7
+ * Fail-closed gate before SCM label/comment mutations (#2601).
8
+ * Cross-repo targets are refused unless explicitly opted in or allowlisted.
9
+ */
10
+ export function isRepoMutationAllowed(targetRepo, projectRoot, options = {}) {
11
+ const projectRepo = resolveRepo(options.explicitRepo ?? null, projectRoot);
12
+ if (options.allowCrossRepo) {
13
+ return { allowed: true, projectRepo };
14
+ }
15
+ const normalizedTarget = normalizeRepoSlug(targetRepo);
16
+ for (const entry of options.allowlist ?? []) {
17
+ if (normalizeRepoSlug(entry) === normalizedTarget) {
18
+ return { allowed: true, projectRepo };
19
+ }
20
+ }
21
+ if (projectRepo !== null && normalizeRepoSlug(projectRepo) === normalizedTarget) {
22
+ return { allowed: true, projectRepo };
23
+ }
24
+ const hint = projectRepo !== null
25
+ ? `(project repo: ${projectRepo})`
26
+ : "(project repo unknown — set --repo or git remote origin)";
27
+ return {
28
+ allowed: false,
29
+ projectRepo,
30
+ reason: `refusing cross-repo mutation on ${targetRepo} ${hint}`,
31
+ };
32
+ }
33
+ //# sourceMappingURL=repo-guard.js.map
@@ -41,6 +41,9 @@ export interface ReconcileUmbrellasOptions {
41
41
  readonly dryRun?: boolean;
42
42
  readonly client?: UmbrellaClient;
43
43
  readonly now?: string;
44
+ /** Opt in to mutating comments on issues outside the resolved project repo (#2601). */
45
+ readonly allowCrossRepo?: boolean;
46
+ readonly repoAllowlist?: readonly string[];
44
47
  }
45
48
  export declare function reconcileUmbrellas(projectRoot: string, options?: ReconcileUmbrellasOptions): [number, ReconcileUmbrellasOutcome];
46
49
  export declare function renderUmbrellasReport(outcome: ReconcileUmbrellasOutcome): string;
@@ -5,6 +5,7 @@ import { createIssueComment, editIssueCommentBody, GitHubBodyError, } from "../i
5
5
  import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
6
6
  import { call } from "../scm/call.js";
7
7
  import { extractIssueRef } from "../triage/reconcile/parse-uri.js";
8
+ import { isRepoMutationAllowed } from "./repo-guard.js";
8
9
  export const OPEN_FOLDERS = ["proposed", "pending", "active"];
9
10
  export const CLOSED_FOLDERS = ["completed", "cancelled"];
10
11
  export const LIFECYCLE_FOLDERS = [...OPEN_FOLDERS, ...CLOSED_FOLDERS];
@@ -261,6 +262,14 @@ function planShape(epicData, index) {
261
262
  return [openChildren, closedChildren, waves];
262
263
  }
263
264
  function reconcileOneEpic(epicData, index, options) {
265
+ const mutateGate = isRepoMutationAllowed(options.repo, options.projectRoot, {
266
+ allowCrossRepo: options.allowCrossRepo,
267
+ allowlist: options.repoAllowlist,
268
+ explicitRepo: options.explicitRepo ?? null,
269
+ });
270
+ if (!mutateGate.allowed) {
271
+ throw new Error(mutateGate.reason ?? `refusing cross-repo mutation on ${options.repo}`);
272
+ }
264
273
  const [openChildren, closedChildren, waves] = planShape(epicData, index);
265
274
  const total = openChildren.length + closedChildren.length;
266
275
  const comments = options.client.fetchComments(options.repo, options.number);
@@ -412,6 +421,10 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
412
421
  client,
413
422
  dryRun: options.dryRun ?? false,
414
423
  now,
424
+ projectRoot: root,
425
+ allowCrossRepo: options.allowCrossRepo ?? false,
426
+ repoAllowlist: options.repoAllowlist,
427
+ explicitRepo: options.repo ?? null,
415
428
  });
416
429
  if (change.action === "unchanged")
417
430
  outcome.unchanged.push(change);
@@ -419,7 +432,10 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
419
432
  outcome.changed.push(change);
420
433
  }
421
434
  catch (exc) {
422
- outcome.errors.push({ story_id: storyId, message: String(exc) });
435
+ outcome.errors.push({
436
+ story_id: storyId,
437
+ message: exc instanceof Error ? exc.message : String(exc),
438
+ });
423
439
  }
424
440
  }
425
441
  }
@@ -0,0 +1,11 @@
1
+ import { type AgentHookInspection } from "../init-deposit/agent-hooks.js";
2
+ import type { OutputStream } from "./verify-hooks-installed.js";
3
+ export interface AgentHookHealthResult {
4
+ readonly code: 0 | 1 | 2;
5
+ readonly message: string;
6
+ readonly stream: OutputStream;
7
+ readonly registrations: readonly AgentHookInspection[];
8
+ }
9
+ /** Read-only P0 agent-host registration health, independent of git hooks. */
10
+ export declare function evaluateAgentHooks(projectRoot: string): AgentHookHealthResult;
11
+ //# sourceMappingURL=agent-hooks.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { inspectAgentHookDeposit } from "../init-deposit/agent-hooks.js";
4
+ function isDirectory(path) {
5
+ try {
6
+ return statSync(path).isDirectory();
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ /** Read-only P0 agent-host registration health, independent of git hooks. */
13
+ export function evaluateAgentHooks(projectRoot) {
14
+ const root = resolve(projectRoot);
15
+ if (!isDirectory(root)) {
16
+ return {
17
+ code: 2,
18
+ message: `❌ deft agent hooks: project root ${root} does not exist (config error).`,
19
+ stream: "stderr",
20
+ registrations: [],
21
+ };
22
+ }
23
+ const registrations = inspectAgentHookDeposit(root);
24
+ const unhealthy = registrations.filter((entry) => entry.status !== "healthy");
25
+ if (unhealthy.length > 0) {
26
+ return {
27
+ code: 1,
28
+ message: "❌ deft agent hook registration INCOMPLETE:\n" +
29
+ unhealthy
30
+ .map((entry) => ` - ${entry.host}: ${entry.status} at ${entry.path} — ${entry.detail}`)
31
+ .join("\n") +
32
+ "\n Recovery: run `deft update` (or `directive init`) to refresh project hooks.",
33
+ stream: "stderr",
34
+ registrations,
35
+ };
36
+ }
37
+ return {
38
+ code: 0,
39
+ message: "✓ deft agent hooks registered and structurally valid for Claude, Grok, Cursor, Codex " +
40
+ "(SessionStart + PreToolUse direct-write tools only; Codex runtime trust is " +
41
+ "user-controlled and must be reviewed with `/hooks`; shell/MCP policy is deferred).",
42
+ stream: "stdout",
43
+ registrations,
44
+ };
45
+ }
46
+ //# sourceMappingURL=agent-hooks.js.map
@@ -6,6 +6,11 @@ export interface ResolveCommandOnPathOptions {
6
6
  }
7
7
  /** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
8
8
  export declare function shouldUseShellForCommand(command: string, platform?: NodeJS.Platform): boolean;
9
+ /**
10
+ * Quote a win32 executable path for `shell: true` spawns when it contains spaces.
11
+ * Without quoting, cmd.exe treats `C:\Program` as the command (#2555).
12
+ */
13
+ export declare function quoteWin32CommandForShell(command: string, platform?: NodeJS.Platform): string;
9
14
  /**
10
15
  * Resolve an executable on PATH with PATHEXT / Path awareness (#2467 / #2548).
11
16
  * Mirrors ts-check-lane `resolvePnpm` and verify-tools `defaultProbe`.
@@ -6,6 +6,20 @@ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
6
6
  export function shouldUseShellForCommand(command, platform = process.platform) {
7
7
  return platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
8
8
  }
9
+ /**
10
+ * Quote a win32 executable path for `shell: true` spawns when it contains spaces.
11
+ * Without quoting, cmd.exe treats `C:\Program` as the command (#2555).
12
+ */
13
+ export function quoteWin32CommandForShell(command, platform = process.platform) {
14
+ if (platform !== "win32" || !command.includes(" ")) {
15
+ return command;
16
+ }
17
+ if ((command.startsWith('"') && command.endsWith('"')) ||
18
+ (command.startsWith("'") && command.endsWith("'"))) {
19
+ return command;
20
+ }
21
+ return `"${command}"`;
22
+ }
9
23
  /**
10
24
  * Resolve an executable on PATH with PATHEXT / Path awareness (#2467 / #2548).
11
25
  * Mirrors ts-check-lane `resolvePnpm` and verify-tools `defaultProbe`.
@@ -39,15 +53,20 @@ export function resolveCommandOnPath(command, options = {}) {
39
53
  * Retries with `shell: true` on win32 ENOENT (npm global `.cmd` shims).
40
54
  */
41
55
  export function spawnCommandText(cmd, args, options = {}) {
42
- const trySpawn = (shell) => spawnSync(cmd, [...args], {
43
- cwd: options.cwd,
44
- env: options.env ?? process.env,
45
- encoding: "utf8",
46
- timeout: options.timeoutMs,
47
- maxBuffer: SUBPROCESS_MAX_BUFFER,
48
- stdio: ["ignore", "pipe", "pipe"],
49
- shell,
50
- });
56
+ const trySpawn = (shell) => {
57
+ const spawnCmd = shell && process.platform === "win32" ? quoteWin32CommandForShell(cmd) : cmd;
58
+ return spawnSync(spawnCmd, [...args], {
59
+ cwd: options.cwd,
60
+ env: options.env ?? process.env,
61
+ encoding: "utf8",
62
+ timeout: options.timeoutMs,
63
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
64
+ stdio: ["ignore", "pipe", "pipe"],
65
+ shell,
66
+ // CREATE_NO_WINDOW on win32; harmless elsewhere (#2563).
67
+ windowsHide: true,
68
+ });
69
+ };
51
70
  let result = trySpawn(shouldUseShellForCommand(cmd));
52
71
  const spawnErr = result.error;
53
72
  if (spawnErr?.code === "ENOENT" && process.platform === "win32") {
@@ -1,3 +1,4 @@
1
+ export * from "./agent-hooks.js";
1
2
  export * from "./command-spawn.js";
2
3
  export * from "./node-runtime.js";
3
4
  export * from "./toolchain-check.js";
@@ -1,3 +1,4 @@
1
+ export * from "./agent-hooks.js";
1
2
  export * from "./command-spawn.js";
2
3
  export * from "./node-runtime.js";
3
4
  export * from "./toolchain-check.js";
@@ -134,7 +134,7 @@ export function evaluate(projectRoot, options = {}) {
134
134
  return {
135
135
  code: 1,
136
136
  message: "❌ deft hooks not installed: core.hooksPath is unset.\n" +
137
- " Recovery: run `task setup` (or re-run the deft installer).",
137
+ " Recovery: run `deft update` to deposit project-root `.githooks/`, then `task setup` to wire core.hooksPath.",
138
138
  stream: "stderr",
139
139
  };
140
140
  }
@@ -147,7 +147,7 @@ export function evaluate(projectRoot, options = {}) {
147
147
  code: 1,
148
148
  message: `❌ deft hooks wired but NON-FUNCTIONAL: core.hooksPath=${hooksPath} ` +
149
149
  `but the directory ${hooksDir} does not exist (#1463 false-green).\n` +
150
- " Recovery: re-run the deft installer / `task setup` to deposit the hooks.",
150
+ " Recovery: run `deft update` to deposit project-root `.githooks/`, then `task setup` to wire core.hooksPath.",
151
151
  stream: "stderr",
152
152
  };
153
153
  }
@@ -157,7 +157,7 @@ export function evaluate(projectRoot, options = {}) {
157
157
  code: 1,
158
158
  message: `❌ deft hooks wired but NON-FUNCTIONAL: ${hooksDir} is missing ` +
159
159
  `${missingHooks.join(", ")} (#1463 false-green).\n` +
160
- " Recovery: re-run the deft installer / `task setup`.",
160
+ " Recovery: run `deft update` to deposit project-root `.githooks/`, then `task setup` to wire core.hooksPath.",
161
161
  stream: "stderr",
162
162
  };
163
163
  }
@@ -169,7 +169,7 @@ export function evaluate(projectRoot, options = {}) {
169
169
  message: `❌ deft hooks wired but NON-FUNCTIONAL: ${hooksDir} hook(s) ` +
170
170
  `${nonExec.join(", ")} are not executable (git mode is not ` +
171
171
  "100755); git silently skips non-executable hooks on Unix (#1477).\n" +
172
- " Recovery: re-run the deft installer / `task setup`, or " +
172
+ " Recovery: run `deft update` to refresh `.githooks/`, or " +
173
173
  "`chmod +x .githooks/pre-commit .githooks/pre-push`.",
174
174
  stream: "stderr",
175
175
  };
@@ -198,7 +198,7 @@ export function evaluate(projectRoot, options = {}) {
198
198
  message: "❌ deft hooks wired but NON-FUNCTIONAL:\n" +
199
199
  contentIssues.map((issue) => ` - ${issue.replace(/\r?\n/g, " ")}`).join("\n") +
200
200
  "\n" +
201
- " Recovery: re-run the deft installer / `task setup` to refresh .githooks/.",
201
+ " Recovery: run `deft update` to refresh project-root `.githooks/`.",
202
202
  stream: "stderr",
203
203
  };
204
204
  }
@@ -0,0 +1,3 @@
1
+ /** Vitest globalTeardown: enforce coverage goal or debt soft-pass (#2573). */
2
+ export default function coverageDebtTeardown(): Promise<void>;
3
+ //# sourceMappingURL=coverage-debt-teardown.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { COVERAGE_GOAL, countRecentCoverageDebtMentions, formatCoverageAttribution, formatOveruseWarning, metricsBelowGoal, readCoverageTotalsFromReport, resolveCoverageDebtIssue, } from "./coverage-debt.js";
4
+ /** Vitest globalTeardown: enforce coverage goal or debt soft-pass (#2573). */
5
+ export default async function coverageDebtTeardown() {
6
+ const resolution = resolveCoverageDebtIssue(process.argv, process.env);
7
+ if (resolution.kind === "none")
8
+ return;
9
+ if (resolution.kind === "invalid") {
10
+ throw new Error(`coverage-debt: ${resolution.reason}`);
11
+ }
12
+ const repoRoot = join(import.meta.dirname, "..", "..", "..", "..");
13
+ const totals = readCoverageTotalsFromReport(join(repoRoot, "coverage"));
14
+ if (totals === null) {
15
+ throw new Error("coverage-debt: could not read coverage/coverage-final.json");
16
+ }
17
+ const missed = metricsBelowGoal(totals);
18
+ if (missed.length === 0) {
19
+ process.stderr.write(`coverage-debt: note — --allow-coverage-debt=#${resolution.issue} set but all metrics meet the ${COVERAGE_GOAL.branches}% goal\n`);
20
+ return;
21
+ }
22
+ process.stderr.write(`${formatCoverageAttribution(resolution.issue, totals)}\n`);
23
+ try {
24
+ const changelog = readFileSync(join(repoRoot, "CHANGELOG.md"), "utf8");
25
+ const overuse = formatOveruseWarning(countRecentCoverageDebtMentions(changelog));
26
+ if (overuse)
27
+ process.stderr.write(`${overuse}\n`);
28
+ }
29
+ catch {
30
+ // CHANGELOG unreadable — debt attribution still stands.
31
+ }
32
+ }
33
+ //# sourceMappingURL=coverage-debt-teardown.js.map
@@ -0,0 +1,50 @@
1
+ /** Shared coverage goal (vitest thresholds). */
2
+ export declare const COVERAGE_GOAL: {
3
+ readonly lines: 85;
4
+ readonly functions: 85;
5
+ readonly branches: 85;
6
+ readonly statements: 85;
7
+ };
8
+ export type CoverageMetric = keyof typeof COVERAGE_GOAL;
9
+ export interface CoverageTotals {
10
+ readonly lines: number;
11
+ readonly functions: number;
12
+ readonly branches: number;
13
+ readonly statements: number;
14
+ }
15
+ export type CoverageDebtResolution = {
16
+ readonly kind: "none";
17
+ } | {
18
+ readonly kind: "valid";
19
+ readonly issue: number;
20
+ } | {
21
+ readonly kind: "invalid";
22
+ readonly reason: string;
23
+ };
24
+ /** Parse `#2573`, `2573`, or bare numeric strings. */
25
+ export declare function parseCoverageDebtIssueNumber(raw: string): number | null;
26
+ /** Parse `--allow-coverage-debt=#N` tokens from argv. */
27
+ export declare function parseCoverageDebtArgv(argv: readonly string[]): CoverageDebtResolution;
28
+ /**
29
+ * Resolve coverage-debt issue from argv or release-scoped env (#2573 / #1553).
30
+ * Raw env bypass is accepted only during release Step-5 preflight.
31
+ */
32
+ export declare function resolveCoverageDebtIssue(argv: readonly string[], env?: NodeJS.ProcessEnv): CoverageDebtResolution;
33
+ interface IstanbulBranchHits {
34
+ readonly [branchId: string]: readonly number[];
35
+ }
36
+ interface IstanbulFileCoverage {
37
+ readonly s?: Readonly<Record<string, number>>;
38
+ readonly f?: Readonly<Record<string, number>>;
39
+ readonly b?: IstanbulBranchHits;
40
+ }
41
+ /** Aggregate global coverage percentages from istanbul coverage-final.json. */
42
+ export declare function summarizeCoverageFinal(coverageFinal: Readonly<Record<string, IstanbulFileCoverage>>): CoverageTotals;
43
+ export declare function readCoverageTotalsFromReport(coverageDir: string): CoverageTotals | null;
44
+ export declare function metricsBelowGoal(totals: CoverageTotals): CoverageMetric[];
45
+ export declare function formatCoverageAttribution(issue: number, totals: CoverageTotals): string;
46
+ /** Count recent release sections citing coverage-debt (CHANGELOG scan). */
47
+ export declare function countRecentCoverageDebtMentions(changelog: string, maxSections?: number): number;
48
+ export declare function formatOveruseWarning(recentCount: number): string | null;
49
+ export {};
50
+ //# sourceMappingURL=coverage-debt.d.ts.map
@@ -0,0 +1,155 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /** Shared coverage goal (vitest thresholds). */
4
+ export const COVERAGE_GOAL = {
5
+ lines: 85,
6
+ functions: 85,
7
+ branches: 85,
8
+ statements: 85,
9
+ };
10
+ const DEBT_FLAG = "--allow-coverage-debt";
11
+ const DEBT_ENV = "DEFT_ALLOW_COVERAGE_DEBT";
12
+ const RELEASE_PREFLIGHT_ENV = "DEFT_RELEASE_PREFLIGHT";
13
+ /** Parse `#2573`, `2573`, or bare numeric strings. */
14
+ export function parseCoverageDebtIssueNumber(raw) {
15
+ const trimmed = raw.trim();
16
+ if (!trimmed)
17
+ return null;
18
+ const normalized = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
19
+ if (!/^\d+$/.test(normalized))
20
+ return null;
21
+ const issue = Number.parseInt(normalized, 10);
22
+ return Number.isFinite(issue) && issue > 0 ? issue : null;
23
+ }
24
+ /** Parse `--allow-coverage-debt=#N` tokens from argv. */
25
+ export function parseCoverageDebtArgv(argv) {
26
+ for (let i = 0; i < argv.length; i += 1) {
27
+ const token = argv[i] ?? "";
28
+ if (token === DEBT_FLAG) {
29
+ const next = argv[i + 1];
30
+ if (next === undefined || next.startsWith("-")) {
31
+ return { kind: "invalid", reason: `${DEBT_FLAG} requires an issue number (#N)` };
32
+ }
33
+ const issue = parseCoverageDebtIssueNumber(next);
34
+ return issue === null
35
+ ? { kind: "invalid", reason: `${DEBT_FLAG} value must be #N or N` }
36
+ : { kind: "valid", issue };
37
+ }
38
+ if (token.startsWith(`${DEBT_FLAG}=`)) {
39
+ const value = token.slice(DEBT_FLAG.length + 1);
40
+ const issue = parseCoverageDebtIssueNumber(value);
41
+ return issue === null
42
+ ? { kind: "invalid", reason: `${DEBT_FLAG}= value must be #N or N` }
43
+ : { kind: "valid", issue };
44
+ }
45
+ }
46
+ return { kind: "none" };
47
+ }
48
+ /**
49
+ * Resolve coverage-debt issue from argv or release-scoped env (#2573 / #1553).
50
+ * Raw env bypass is accepted only during release Step-5 preflight.
51
+ */
52
+ export function resolveCoverageDebtIssue(argv, env = process.env) {
53
+ const fromArgv = parseCoverageDebtArgv(argv);
54
+ if (fromArgv.kind !== "none")
55
+ return fromArgv;
56
+ const rawEnv = env[DEBT_ENV];
57
+ if (rawEnv && env[RELEASE_PREFLIGHT_ENV] === "1") {
58
+ const issue = parseCoverageDebtIssueNumber(rawEnv);
59
+ return issue === null
60
+ ? { kind: "invalid", reason: `${DEBT_ENV} must be a positive integer` }
61
+ : { kind: "valid", issue };
62
+ }
63
+ return { kind: "none" };
64
+ }
65
+ /** Aggregate global coverage percentages from istanbul coverage-final.json. */
66
+ export function summarizeCoverageFinal(coverageFinal) {
67
+ let stmtTotal = 0;
68
+ let stmtCovered = 0;
69
+ let fnTotal = 0;
70
+ let fnCovered = 0;
71
+ let branchTotal = 0;
72
+ let branchCovered = 0;
73
+ for (const file of Object.values(coverageFinal)) {
74
+ if (file.s) {
75
+ for (const hits of Object.values(file.s)) {
76
+ stmtTotal += 1;
77
+ if (hits > 0)
78
+ stmtCovered += 1;
79
+ }
80
+ }
81
+ if (file.f) {
82
+ for (const hits of Object.values(file.f)) {
83
+ fnTotal += 1;
84
+ if (hits > 0)
85
+ fnCovered += 1;
86
+ }
87
+ }
88
+ if (file.b) {
89
+ for (const paths of Object.values(file.b)) {
90
+ for (const hits of paths) {
91
+ branchTotal += 1;
92
+ if (hits > 0)
93
+ branchCovered += 1;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ const pct = (covered, total) => total === 0 ? 100 : (covered / total) * 100;
99
+ const statements = pct(stmtCovered, stmtTotal);
100
+ const functions = pct(fnCovered, fnTotal);
101
+ const branches = pct(branchCovered, branchTotal);
102
+ // Istanbul coverage-final.json does not carry pre-aggregated line hit counts —
103
+ // `l` (line map) is optional and its format differs from `s`/`f`/`b`. Using
104
+ // statements as a proxy may overstate line coverage when multiple statements
105
+ // share a single uncovered line.
106
+ return {
107
+ statements,
108
+ functions,
109
+ branches,
110
+ lines: statements, // approximation: actual line coverage may differ
111
+ };
112
+ }
113
+ export function readCoverageTotalsFromReport(coverageDir) {
114
+ const finalPath = join(coverageDir, "coverage-final.json");
115
+ try {
116
+ const raw = readFileSync(finalPath, "utf8");
117
+ const parsed = JSON.parse(raw);
118
+ return summarizeCoverageFinal(parsed);
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ export function metricsBelowGoal(totals) {
125
+ const missed = [];
126
+ for (const metric of Object.keys(COVERAGE_GOAL)) {
127
+ if (totals[metric] + 1e-9 < COVERAGE_GOAL[metric]) {
128
+ missed.push(metric);
129
+ }
130
+ }
131
+ return missed;
132
+ }
133
+ const sanitizeMetric = (value) => String(value).replace(/\r?\n/g, " ");
134
+ export function formatCoverageAttribution(issue, totals) {
135
+ const lines = [
136
+ `coverage-debt: soft-pass acknowledged for issue #${issue}`,
137
+ ` measured: branches ${sanitizeMetric(totals.branches.toFixed(2))}% | lines ${sanitizeMetric(totals.lines.toFixed(2))}% | functions ${sanitizeMetric(totals.functions.toFixed(2))}% | statements ${sanitizeMetric(totals.statements.toFixed(2))}%`,
138
+ ` goal: branches ${sanitizeMetric(COVERAGE_GOAL.branches)}% | lines ${sanitizeMetric(COVERAGE_GOAL.lines)}% | functions ${sanitizeMetric(COVERAGE_GOAL.functions)}% | statements ${sanitizeMetric(COVERAGE_GOAL.statements)}%`,
139
+ ];
140
+ return lines.join("\n");
141
+ }
142
+ /** Count recent release sections citing coverage-debt (CHANGELOG scan). */
143
+ export function countRecentCoverageDebtMentions(changelog, maxSections = 5) {
144
+ const versionHeader = /^## \[(?!Unreleased)/m;
145
+ const sections = changelog.split(versionHeader).slice(1, maxSections + 1);
146
+ const pattern = /coverage[- ]debt soft[- ]pass|allow-coverage-debt=#?\d/i;
147
+ return sections.filter((section) => pattern.test(section)).length;
148
+ }
149
+ export function formatOveruseWarning(recentCount) {
150
+ if (recentCount < 2)
151
+ return null;
152
+ return (`coverage-debt WARN: ${recentCount} of the last release sections already cite coverage debt — ` +
153
+ "consider restoring real coverage instead of reusing the escape hatch.");
154
+ }
155
+ //# sourceMappingURL=coverage-debt.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Win32 globalSetup for coverage runs (#2580).
3
+ *
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.
10
+ */
11
+ export default function setup(): void;
12
+ //# sourceMappingURL=win32-coverage-tmp-setup.d.ts.map
@@ -0,0 +1,24 @@
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 });
6
+ }
7
+ /**
8
+ * Win32 globalSetup for coverage runs (#2580).
9
+ *
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.
16
+ */
17
+ export default function setup() {
18
+ if (process.platform !== "win32")
19
+ return;
20
+ ensureCoverageTmpDir();
21
+ const keepalive = setInterval(ensureCoverageTmpDir, 100);
22
+ keepalive.unref?.();
23
+ }
24
+ //# sourceMappingURL=win32-coverage-tmp-setup.js.map
@@ -33,6 +33,51 @@ function walkJsonFiles(root, acc = []) {
33
33
  }
34
34
  return acc;
35
35
  }
36
+ /** Cache support state may contain only ordinary files/directories, never links or special files. */
37
+ function containsUnsupportedCacheEntry(root) {
38
+ if (!isDirectory(root))
39
+ return false;
40
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
41
+ if (entry.isSymbolicLink())
42
+ return true;
43
+ if (entry.isDirectory()) {
44
+ if (containsUnsupportedCacheEntry(join(root, entry.name)))
45
+ return true;
46
+ continue;
47
+ }
48
+ if (!entry.isFile())
49
+ return true;
50
+ }
51
+ return false;
52
+ }
53
+ /**
54
+ * A top-level `.triage-cache/` is operational support state, not evidence that
55
+ * project artifacts have already migrated. Everything else keeps the generic
56
+ * empty-tree semantics; unknown files and symlinks remain real content.
57
+ */
58
+ function xbriefHasProjectContent(root) {
59
+ if (!isDirectory(root))
60
+ return false;
61
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
62
+ const full = join(root, entry.name);
63
+ if (entry.name === ".triage-cache" && entry.isDirectory()) {
64
+ if (containsUnsupportedCacheEntry(full))
65
+ return true;
66
+ continue;
67
+ }
68
+ if (entry.isSymbolicLink())
69
+ return true;
70
+ if (entry.isDirectory()) {
71
+ if (!isEffectivelyEmptyDir(full))
72
+ return true;
73
+ continue;
74
+ }
75
+ if (entry.isFile() && (entry.name === ".gitkeep" || entry.name === ".keep"))
76
+ continue;
77
+ return true;
78
+ }
79
+ return false;
80
+ }
36
81
  function scanFileContent(path, content, reasons) {
37
82
  if (path.endsWith(LEGACY_ARTIFACT_SUFFIX)) {
38
83
  reasons.add(`legacy artifact filename: ${path}`);
@@ -94,7 +139,7 @@ export function detectXbriefConvergence(projectRoot) {
94
139
  const vbriefHasMarker = vbriefPresent && hasVbriefDeprecationMarker(legacyDir);
95
140
  const vbriefEmpty = vbriefPresent && !vbriefHasMarker && isEffectivelyEmptyDir(legacyDir);
96
141
  const xbriefPresent = isDirectory(migratedDir);
97
- const xbriefHasContent = xbriefPresent && !isEffectivelyEmptyDir(migratedDir);
142
+ const xbriefHasContent = xbriefPresent && xbriefHasProjectContent(migratedDir);
98
143
  let state;
99
144
  if (vbriefHasMarker) {
100
145
  state = "xbrief-marker";