@juzser/blacksmith 0.1.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 (172) hide show
  1. package/.claude/agents/auditor.md +146 -0
  2. package/.claude/agents/coder.md +164 -0
  3. package/.claude/agents/grader.md +110 -0
  4. package/.claude/agents/merger.md +107 -0
  5. package/.claude/agents/planner.md +92 -0
  6. package/.claude/agents/researcher.md +105 -0
  7. package/.claude/agents/reviewer.md +171 -0
  8. package/.claude/agents/scribe.md +72 -0
  9. package/.claude/agents/security-reviewer.md +130 -0
  10. package/.claude/agents/spec-reviewer.md +215 -0
  11. package/.claude/agents/tester.md +121 -0
  12. package/.claude/agents/uiux.md +126 -0
  13. package/.claude/agents/verifier.md +103 -0
  14. package/.claude/agents/wave-runner.md +74 -0
  15. package/LICENSE +21 -0
  16. package/README.md +389 -0
  17. package/factory/orchestrator/dist/actors.js +46 -0
  18. package/factory/orchestrator/dist/agents-registry.js +308 -0
  19. package/factory/orchestrator/dist/args.js +84 -0
  20. package/factory/orchestrator/dist/artifacts.js +111 -0
  21. package/factory/orchestrator/dist/attribution.js +160 -0
  22. package/factory/orchestrator/dist/audit.js +812 -0
  23. package/factory/orchestrator/dist/autonomy.js +151 -0
  24. package/factory/orchestrator/dist/budgetAlarm.js +366 -0
  25. package/factory/orchestrator/dist/budgets.js +196 -0
  26. package/factory/orchestrator/dist/claims.js +633 -0
  27. package/factory/orchestrator/dist/cli.js +3185 -0
  28. package/factory/orchestrator/dist/commit.js +152 -0
  29. package/factory/orchestrator/dist/coverage.js +188 -0
  30. package/factory/orchestrator/dist/crossFinding.js +479 -0
  31. package/factory/orchestrator/dist/crosscheck.js +396 -0
  32. package/factory/orchestrator/dist/daemon.js +707 -0
  33. package/factory/orchestrator/dist/db/projector.js +1317 -0
  34. package/factory/orchestrator/dist/db/queries.js +1714 -0
  35. package/factory/orchestrator/dist/db/schema.js +305 -0
  36. package/factory/orchestrator/dist/delegation.js +400 -0
  37. package/factory/orchestrator/dist/diffstat.js +164 -0
  38. package/factory/orchestrator/dist/dispatchAudit.js +275 -0
  39. package/factory/orchestrator/dist/dotenv.js +72 -0
  40. package/factory/orchestrator/dist/effort.js +202 -0
  41. package/factory/orchestrator/dist/epic.js +1018 -0
  42. package/factory/orchestrator/dist/epicWidth.js +238 -0
  43. package/factory/orchestrator/dist/errorIssues.js +249 -0
  44. package/factory/orchestrator/dist/errors.js +11 -0
  45. package/factory/orchestrator/dist/escalation.js +500 -0
  46. package/factory/orchestrator/dist/events.js +1015 -0
  47. package/factory/orchestrator/dist/findingAge.js +91 -0
  48. package/factory/orchestrator/dist/findingContext.js +103 -0
  49. package/factory/orchestrator/dist/findings.js +1156 -0
  50. package/factory/orchestrator/dist/gate.js +911 -0
  51. package/factory/orchestrator/dist/gh.js +146 -0
  52. package/factory/orchestrator/dist/git.js +87 -0
  53. package/factory/orchestrator/dist/goalCheck.js +380 -0
  54. package/factory/orchestrator/dist/graph.js +61 -0
  55. package/factory/orchestrator/dist/hookDecision.js +106 -0
  56. package/factory/orchestrator/dist/immutability.js +171 -0
  57. package/factory/orchestrator/dist/impact.js +280 -0
  58. package/factory/orchestrator/dist/integration.js +141 -0
  59. package/factory/orchestrator/dist/judgePreflight.js +155 -0
  60. package/factory/orchestrator/dist/judges.js +310 -0
  61. package/factory/orchestrator/dist/lessonAudit.js +399 -0
  62. package/factory/orchestrator/dist/lessons.js +1329 -0
  63. package/factory/orchestrator/dist/mcp.js +555 -0
  64. package/factory/orchestrator/dist/paths.js +82 -0
  65. package/factory/orchestrator/dist/plan.js +502 -0
  66. package/factory/orchestrator/dist/planQuorum.js +421 -0
  67. package/factory/orchestrator/dist/policy.js +1308 -0
  68. package/factory/orchestrator/dist/policyHook.js +28 -0
  69. package/factory/orchestrator/dist/preconditions.js +47 -0
  70. package/factory/orchestrator/dist/projects.js +134 -0
  71. package/factory/orchestrator/dist/prompts.js +57 -0
  72. package/factory/orchestrator/dist/provenance.js +201 -0
  73. package/factory/orchestrator/dist/providers/api-transport.js +117 -0
  74. package/factory/orchestrator/dist/providers/cli-transport.js +208 -0
  75. package/factory/orchestrator/dist/providers/index.js +54 -0
  76. package/factory/orchestrator/dist/providers/schema-validate.js +152 -0
  77. package/factory/orchestrator/dist/providers/types.js +49 -0
  78. package/factory/orchestrator/dist/queue.js +263 -0
  79. package/factory/orchestrator/dist/quorum.js +347 -0
  80. package/factory/orchestrator/dist/quorumEscalations.js +132 -0
  81. package/factory/orchestrator/dist/results.js +65 -0
  82. package/factory/orchestrator/dist/roadmap.js +437 -0
  83. package/factory/orchestrator/dist/runtime.js +86 -0
  84. package/factory/orchestrator/dist/sameMistakeKpi.js +243 -0
  85. package/factory/orchestrator/dist/sandbox.js +165 -0
  86. package/factory/orchestrator/dist/scaffold.js +373 -0
  87. package/factory/orchestrator/dist/scheduler.js +408 -0
  88. package/factory/orchestrator/dist/schemas.js +195 -0
  89. package/factory/orchestrator/dist/security.js +222 -0
  90. package/factory/orchestrator/dist/severity.js +293 -0
  91. package/factory/orchestrator/dist/spec.js +376 -0
  92. package/factory/orchestrator/dist/specChange.js +497 -0
  93. package/factory/orchestrator/dist/stack.js +324 -0
  94. package/factory/orchestrator/dist/symbols.js +966 -0
  95. package/factory/orchestrator/dist/taskEvents.js +399 -0
  96. package/factory/orchestrator/dist/taskId.js +126 -0
  97. package/factory/orchestrator/dist/taxonomy.js +113 -0
  98. package/factory/orchestrator/dist/testSelect.js +129 -0
  99. package/factory/orchestrator/dist/testerAudit.js +264 -0
  100. package/factory/orchestrator/dist/testgate.js +171 -0
  101. package/factory/orchestrator/dist/usage.js +874 -0
  102. package/factory/orchestrator/dist/waivers.js +228 -0
  103. package/factory/orchestrator/dist/waveBudget.js +223 -0
  104. package/factory/orchestrator/dist/waveConcurrency.js +250 -0
  105. package/factory/orchestrator/dist/waveNext.js +210 -0
  106. package/factory/orchestrator/dist/waveSchedule.js +146 -0
  107. package/factory/orchestrator/dist/worktree.js +207 -0
  108. package/factory/orchestrator/drizzle/0000_broken_rockslide.sql +183 -0
  109. package/factory/orchestrator/drizzle/0001_luxuriant_zaladane.sql +1 -0
  110. package/factory/orchestrator/drizzle/0002_yellow_wild_pack.sql +8 -0
  111. package/factory/orchestrator/drizzle/0003_damp_riptide.sql +12 -0
  112. package/factory/orchestrator/drizzle/0004_broad_lady_vermin.sql +1 -0
  113. package/factory/orchestrator/drizzle/0005_romantic_jocasta.sql +2 -0
  114. package/factory/orchestrator/drizzle/0006_bent_madelyne_pryor.sql +16 -0
  115. package/factory/orchestrator/drizzle/0007_overjoyed_goliath.sql +1 -0
  116. package/factory/orchestrator/drizzle/0008_smart_iron_patriot.sql +2 -0
  117. package/factory/orchestrator/drizzle/0009_agents_epic_id.sql +2 -0
  118. package/factory/orchestrator/drizzle/0010_milestone_kind.sql +1 -0
  119. package/factory/orchestrator/drizzle/meta/_journal.json +83 -0
  120. package/factory/policies/budgets.yml +262 -0
  121. package/factory/policies/crosscheck.yml +308 -0
  122. package/factory/policies/delegation.yml +44 -0
  123. package/factory/policies/effort.yml +142 -0
  124. package/factory/policies/guardrails.yml +306 -0
  125. package/factory/policies/lessons.md +239 -0
  126. package/factory/policies/scheduler.yml +90 -0
  127. package/factory/policies/sensitive-paths.yml +148 -0
  128. package/factory/policies/severity.yml +83 -0
  129. package/factory/policies/stack.yml +60 -0
  130. package/factory/policies/taxonomy.yml +340 -0
  131. package/factory/policies/worktree.yml +95 -0
  132. package/factory/scaffold/base/.github/workflows/ci.yml +34 -0
  133. package/factory/scaffold/base/.gitignore.tmpl +11 -0
  134. package/factory/scaffold/base/AGENTS.md +33 -0
  135. package/factory/scaffold/base/README.md +25 -0
  136. package/factory/scaffold/base/biome.json.tmpl +30 -0
  137. package/factory/scaffold/base/package.json +25 -0
  138. package/factory/scaffold/base/pnpm-workspace.yaml.tmpl +16 -0
  139. package/factory/scaffold/base/src/index.ts +4 -0
  140. package/factory/scaffold/base/test/index.test.ts +8 -0
  141. package/factory/scaffold/base/tsconfig.json +20 -0
  142. package/factory/scaffold/base/tsconfig.test.json +9 -0
  143. package/factory/scaffold/base/vitest.config.ts +29 -0
  144. package/factory/scaffold/mcp/mcp.manifest.json +22 -0
  145. package/factory/scaffold/mcp/package.fragment.json +9 -0
  146. package/factory/scaffold/mcp/src/mcp/README.md +63 -0
  147. package/factory/scaffold/mcp/src/mcp/guard.ts +110 -0
  148. package/factory/scaffold/mcp/src/mcp/redact.ts +69 -0
  149. package/factory/scaffold/mcp/src/mcp/server.ts +55 -0
  150. package/factory/scaffold/mcp/src/mcp/tools/health.ts +60 -0
  151. package/factory/scaffold/mcp/test/mcp/guard.test.ts +108 -0
  152. package/factory/scaffold/mcp/test/mcp/redact.test.ts +61 -0
  153. package/factory/scaffold/mcp/test/mcp/surface.test.ts +100 -0
  154. package/factory/scaffold/ui/index.html +12 -0
  155. package/factory/scaffold/ui/package.fragment.json +13 -0
  156. package/factory/scaffold/ui/src/App.vue +10 -0
  157. package/factory/scaffold/ui/src/main.ts +5 -0
  158. package/factory/scaffold/ui/src/shims.d.ts +11 -0
  159. package/factory/scaffold/ui/vite.config.ts +20 -0
  160. package/factory/scaffold/ui-tailwind/package.fragment.json +6 -0
  161. package/factory/scaffold/ui-tailwind/vite.config.ts +26 -0
  162. package/factory/specs/schema/event.schema.json +82 -0
  163. package/factory/specs/schema/finding-evidence.schema.json +45 -0
  164. package/factory/specs/schema/finding.schema.json +115 -0
  165. package/factory/specs/schema/grader-verdict.schema.json +50 -0
  166. package/factory/specs/schema/judge-verdict.schema.json +19 -0
  167. package/factory/specs/schema/lesson.schema.json +85 -0
  168. package/factory/specs/schema/mcp-manifest.schema.json +230 -0
  169. package/factory/specs/schema/result.schema.json +81 -0
  170. package/factory/specs/schema/spec-change-request.schema.json +46 -0
  171. package/factory/specs/schema/task-spec.schema.json +134 -0
  172. package/package.json +98 -0
@@ -0,0 +1,152 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ /** The taxonomy class raised when a task reports done with work still in the working tree. */
3
+ export const UNCOMMITTED_WORK_CODE = 'contract.uncommitted-work';
4
+ const PORCELAIN_STATUS_WIDTH = 3; // "XY " — two status chars plus the separator
5
+ function git(cwd, args) {
6
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
7
+ .toString()
8
+ .trim();
9
+ }
10
+ function isGitWorktree(worktreeDir) {
11
+ try {
12
+ git(worktreeDir, ['rev-parse', '--is-inside-work-tree']);
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ function headSha(worktreeDir) {
20
+ try {
21
+ return git(worktreeDir, ['rev-parse', 'HEAD']);
22
+ }
23
+ catch {
24
+ return null; // unborn branch — nothing has ever been committed here
25
+ }
26
+ }
27
+ function currentBranch(worktreeDir) {
28
+ try {
29
+ return git(worktreeDir, ['branch', '--show-current']) || null; // empty when detached
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function resolveRef(worktreeDir, ref) {
36
+ try {
37
+ return git(worktreeDir, ['rev-parse', '--verify', `${ref}^{commit}`]);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Every path git currently reports as dirty, staged or untracked, `-z`-parsed
45
+ * so spaces and newlines in filenames survive. Renames emit destination then
46
+ * source; both count, since a rename left two paths uncommitted.
47
+ *
48
+ * Deliberately without `--ignored`: build output and node_modules are not work
49
+ * the coder forgot to commit, and a gate that blocked on `dist/` would be
50
+ * unusable in exactly the repos that need it most.
51
+ *
52
+ * `--untracked-files=all` for the two reasons its siblings in `claims.ts` and
53
+ * `immutability.ts` already pass it, and one more that only bites here (D-178):
54
+ * the default `normal` collapses a whole new directory into a single `src/`
55
+ * entry, so a certificate promising to name every dirty path named a directory
56
+ * instead; and the mode is *configurable*, so a repo, global, or system
57
+ * `status.showUntrackedFiles = no` made untracked work invisible to the read
58
+ * and this gate certified a worktree with uncommitted new files. Passing it
59
+ * explicitly makes the setting unable to weaken the check — which is the whole
60
+ * point of D-30. Untracked is not an edge case here: a task's first write is
61
+ * usually a new file.
62
+ */
63
+ function dirtyPaths(worktreeDir) {
64
+ const output = execFileSync('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all'], {
65
+ cwd: worktreeDir,
66
+ encoding: 'utf8',
67
+ });
68
+ const fields = output.split('\0');
69
+ const paths = [];
70
+ for (let i = 0; i < fields.length; i++) {
71
+ const field = fields[i];
72
+ if (field.length <= PORCELAIN_STATUS_WIDTH)
73
+ continue;
74
+ const status = field.slice(0, 2);
75
+ paths.push(field.slice(PORCELAIN_STATUS_WIDTH));
76
+ if (status.startsWith('R') || status.startsWith('C')) {
77
+ const source = fields[i + 1];
78
+ if (source)
79
+ paths.push(source);
80
+ i++;
81
+ }
82
+ }
83
+ return paths;
84
+ }
85
+ function blocked(reason, rest) {
86
+ return { certified: false, reason, ...rest };
87
+ }
88
+ /**
89
+ * Certify that a task worktree holds a commit worth merging (P9-8 / D-30).
90
+ *
91
+ * The gate scores a worktree; the merge queue merges a *branch*. When a task
92
+ * leaves its work staged-but-uncommitted, those are two different things: every
93
+ * check goes green against the working tree while the merge is a no-op that
94
+ * still reports success. This closes that gap from the cheap end — no checkout,
95
+ * two git reads — by refusing to score a worktree that is dirty, and refusing
96
+ * one whose branch head still equals the integration base it was cut from.
97
+ *
98
+ * `baseRef` is optional: an ad-hoc `gate run` against a repo with no
99
+ * integration branch is not forced to invent one, and says `commitsAhead: null`
100
+ * rather than pretending that half ran.
101
+ */
102
+ export function certifyCommit(worktreeDir, opts = {}) {
103
+ const baseRef = opts.baseRef ?? null;
104
+ const empty = { head: null, branch: null, dirty: [], baseRef, baseSha: null, commitsAhead: null };
105
+ if (!isGitWorktree(worktreeDir))
106
+ return blocked('not-a-git-worktree', empty);
107
+ const branch = currentBranch(worktreeDir);
108
+ const dirty = dirtyPaths(worktreeDir);
109
+ const head = headSha(worktreeDir);
110
+ // Dirt first: when a worktree is both dirty and unadvanced, the uncommitted
111
+ // work is the cause and "commit it" is the whole fix.
112
+ if (dirty.length > 0) {
113
+ return blocked('uncommitted-work', {
114
+ head,
115
+ branch,
116
+ dirty,
117
+ baseRef,
118
+ baseSha: null,
119
+ commitsAhead: null,
120
+ });
121
+ }
122
+ if (head === null)
123
+ return blocked('unborn-branch', { ...empty, branch });
124
+ if (baseRef === null) {
125
+ return {
126
+ certified: true,
127
+ reason: null,
128
+ head,
129
+ branch,
130
+ dirty,
131
+ baseRef,
132
+ baseSha: null,
133
+ commitsAhead: null,
134
+ };
135
+ }
136
+ const baseSha = resolveRef(worktreeDir, baseRef);
137
+ if (baseSha === null) {
138
+ return blocked('unknown-base', {
139
+ head,
140
+ branch,
141
+ dirty,
142
+ baseRef,
143
+ baseSha: null,
144
+ commitsAhead: null,
145
+ });
146
+ }
147
+ const commitsAhead = Number(git(worktreeDir, ['rev-list', '--count', `${baseSha}..HEAD`]));
148
+ if (commitsAhead === 0) {
149
+ return blocked('branch-not-advanced', { head, branch, dirty, baseRef, baseSha, commitsAhead });
150
+ }
151
+ return { certified: true, reason: null, head, branch, dirty, baseRef, baseSha, commitsAhead };
152
+ }
@@ -0,0 +1,188 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { SmithError } from './errors.js';
4
+ export class CoverageError extends SmithError {
5
+ }
6
+ /**
7
+ * Where the `json-summary` reporter writes, relative to the worktree:
8
+ * vitest's default `coverage.reportsDirectory` is `./coverage`, and the
9
+ * reporter's filename is fixed.
10
+ */
11
+ export const DEFAULT_SUMMARY_PATH = 'coverage/coverage-summary.json';
12
+ const METRIC_KEYS = ['lines', 'statements', 'functions', 'branches'];
13
+ function isRecord(value) {
14
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
15
+ }
16
+ function parseMetric(value, where) {
17
+ if (!isRecord(value)) {
18
+ throw new CoverageError('coverage.malformed-summary', `${where} is not an object`);
19
+ }
20
+ const nums = {};
21
+ for (const key of ['total', 'covered', 'skipped', 'pct']) {
22
+ const n = value[key];
23
+ if (typeof n !== 'number' || !Number.isFinite(n)) {
24
+ throw new CoverageError('coverage.malformed-summary', `${where}.${key} is not a finite number`);
25
+ }
26
+ nums[key] = n;
27
+ }
28
+ return nums;
29
+ }
30
+ function parseFileCoverage(value, where) {
31
+ if (!isRecord(value)) {
32
+ throw new CoverageError('coverage.malformed-summary', `${where} is not an object`);
33
+ }
34
+ const out = {};
35
+ for (const key of METRIC_KEYS) {
36
+ // Every metric is required. Defaulting a missing one to zero would invent
37
+ // a number, and defaulting it to 100 would invent a pass; both are worse
38
+ // than refusing to read the file.
39
+ out[key] = parseMetric(value[key], `${where}.${key}`);
40
+ }
41
+ return out;
42
+ }
43
+ /**
44
+ * Turn one file key into a worktree-relative POSIX path. The reporter emits
45
+ * absolute paths; a key that resolves outside the worktree is left exactly as
46
+ * it came, because a `../../..` path would be a worse label than the truth.
47
+ */
48
+ function relativizeKey(key, worktreeDir) {
49
+ if (!path.isAbsolute(key))
50
+ return key.split(path.sep).join('/');
51
+ const rel = path.relative(worktreeDir, key);
52
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel))
53
+ return key;
54
+ return rel.split(path.sep).join('/');
55
+ }
56
+ /**
57
+ * Parse a `coverage-summary.json` document. Strict on shape: a summary that
58
+ * cannot be read must raise, never come back as an empty-but-valid one — an
59
+ * empty summary reads as "nothing was instrumented", which is a claim, and the
60
+ * wrong one.
61
+ */
62
+ export function parseCoverageSummary(raw, worktreeDir) {
63
+ if (!isRecord(raw)) {
64
+ throw new CoverageError('coverage.malformed-summary', 'coverage summary is not an object');
65
+ }
66
+ if (!('total' in raw)) {
67
+ throw new CoverageError('coverage.malformed-summary', 'coverage summary has no "total" key — it is not a json-summary report');
68
+ }
69
+ const total = parseFileCoverage(raw.total, 'total');
70
+ const files = {};
71
+ for (const [key, value] of Object.entries(raw)) {
72
+ if (key === 'total')
73
+ continue;
74
+ files[relativizeKey(key, worktreeDir)] = parseFileCoverage(value, key);
75
+ }
76
+ return { total, files };
77
+ }
78
+ /**
79
+ * Read the summary a coverage run left behind. `null` means the file is not
80
+ * there; anything else that goes wrong throws, because "absent" and
81
+ * "unreadable" are different facts and only one of them is fixed by adding a
82
+ * reporter.
83
+ */
84
+ export async function readCoverageSummary(worktreeDir, summaryPath = DEFAULT_SUMMARY_PATH) {
85
+ const abs = path.resolve(worktreeDir, summaryPath);
86
+ let text;
87
+ try {
88
+ text = await readFile(abs, 'utf8');
89
+ }
90
+ catch (err) {
91
+ if (err.code === 'ENOENT')
92
+ return null;
93
+ throw new CoverageError('coverage.unreadable-summary', `could not read ${summaryPath}: ${err.message}`);
94
+ }
95
+ let raw;
96
+ try {
97
+ raw = JSON.parse(text);
98
+ }
99
+ catch (err) {
100
+ throw new CoverageError('coverage.malformed-summary', `${summaryPath} is not valid JSON: ${err.message}`);
101
+ }
102
+ return parseCoverageSummary(raw, worktreeDir);
103
+ }
104
+ const GLOB_CHARS = /[*?[\]{}!()]/;
105
+ /** A claim that names one file, not a region. */
106
+ function isLiteralClaim(claim) {
107
+ return !GLOB_CHARS.test(claim);
108
+ }
109
+ function dirOf(p) {
110
+ const i = p.lastIndexOf('/');
111
+ return i === -1 ? '' : p.slice(0, i);
112
+ }
113
+ /**
114
+ * Answer, for each file a task's criteria actually name, whether the coverage
115
+ * run produced a number for it. Glob claims are skipped: `src/**` names a
116
+ * region, and a region has no single number to report — a per-file criterion
117
+ * has to name a per-file path.
118
+ */
119
+ export function coverageSubjects(summary, claims) {
120
+ const instrumentedDirs = new Set(Object.keys(summary.files).map(dirOf));
121
+ const seen = new Set();
122
+ const subjects = [];
123
+ for (const claim of claims) {
124
+ if (!isLiteralClaim(claim) || seen.has(claim))
125
+ continue;
126
+ seen.add(claim);
127
+ const coverage = summary.files[claim];
128
+ if (coverage !== undefined) {
129
+ subjects.push({
130
+ path: claim,
131
+ status: 'measured',
132
+ coverage,
133
+ detail: `${claim}: ${coverage.lines.pct}% lines, ${coverage.statements.pct}% statements, ${coverage.functions.pct}% functions, ${coverage.branches.pct}% branches.`,
134
+ });
135
+ continue;
136
+ }
137
+ if (instrumentedDirs.has(dirOf(claim))) {
138
+ subjects.push({
139
+ path: claim,
140
+ status: 'unmeasured',
141
+ coverage: null,
142
+ detail: `${claim} has no row in the coverage summary, yet other files in ${dirOf(claim) || '.'} do — the run did not measure the file the criterion names.`,
143
+ });
144
+ continue;
145
+ }
146
+ subjects.push({
147
+ path: claim,
148
+ status: 'not-instrumented',
149
+ coverage: null,
150
+ detail: `${claim} is outside the coverage include glob — nothing in ${dirOf(claim) || '.'} was instrumented.`,
151
+ });
152
+ }
153
+ return subjects;
154
+ }
155
+ /**
156
+ * The record a gate attaches instead of a scraped table: the per-file numbers
157
+ * as the reporter wrote them, plus an explicit verdict on whether they cover
158
+ * the files this task's criteria are about.
159
+ */
160
+ export async function collectCoverageEvidence(input) {
161
+ const summaryPath = input.summaryPath ?? DEFAULT_SUMMARY_PATH;
162
+ const summary = await readCoverageSummary(input.worktreeDir, summaryPath);
163
+ if (summary === null) {
164
+ return {
165
+ summaryPath,
166
+ present: false,
167
+ total: null,
168
+ filesMeasured: 0,
169
+ subjects: [],
170
+ complete: false,
171
+ detail: `no ${summaryPath} after the coverage check — add "json-summary" to coverage.reporter in vitest.config.ts, because the text table hides every file at 100%.`,
172
+ };
173
+ }
174
+ const subjects = coverageSubjects(summary, input.claims);
175
+ const missing = subjects.filter((s) => s.status === 'unmeasured');
176
+ const filesMeasured = Object.keys(summary.files).length;
177
+ return {
178
+ summaryPath,
179
+ present: true,
180
+ total: summary.total,
181
+ filesMeasured,
182
+ subjects,
183
+ complete: missing.length === 0,
184
+ detail: missing.length === 0
185
+ ? `${filesMeasured} files measured; ${subjects.filter((s) => s.status === 'measured').length} of ${subjects.length} named files have a per-file number.`
186
+ : `no per-file number for ${missing.map((s) => s.path).join(', ')} — the criterion names a file the coverage run did not measure.`,
187
+ };
188
+ }