mikoshi-construct 0.1.3 → 0.2.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/cli.js CHANGED
@@ -1,22 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path14 from "path";
5
- import process4 from "process";
4
+ import path21 from "path";
5
+ import process6 from "process";
6
6
  import { isTTY } from "@clack/prompts";
7
7
  import { defineCommand, runMain } from "citty";
8
8
 
9
- // src/commands/cost.ts
10
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
9
+ // src/commands/cost/index.ts
10
+ import process2 from "process";
11
+
12
+ // src/commands/cost/claude-code.ts
13
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
11
14
  import { homedir } from "os";
12
15
  import path from "path";
16
+
17
+ // src/commands/cost/usage.ts
18
+ var PRICE_RELATIVE_TO_INPUT = { cacheWrite: 1.25, cacheRead: 0.1, output: 5 };
13
19
  function emptyUsage() {
14
20
  return { calls: 0, input: 0, cacheWrite: 0, cacheRead: 0, output: 0, models: [] };
15
21
  }
16
22
  function billable(usage) {
17
23
  return usage.input + usage.cacheWrite + usage.cacheRead + usage.output;
18
24
  }
19
- var PRICE_RELATIVE_TO_INPUT = { cacheWrite: 1.25, cacheRead: 0.1, output: 5 };
20
25
  function weighted(usage) {
21
26
  return Math.round(usage.input + usage.cacheWrite * PRICE_RELATIVE_TO_INPUT.cacheWrite + usage.cacheRead * PRICE_RELATIVE_TO_INPUT.cacheRead + usage.output * PRICE_RELATIVE_TO_INPUT.output);
22
27
  }
@@ -31,6 +36,8 @@ function add(total, part) {
31
36
  total.models.push(model);
32
37
  }
33
38
  }
39
+
40
+ // src/commands/cost/claude-code.ts
34
41
  function projectKey(cwd) {
35
42
  return cwd.replace(/[/.]/g, "-");
36
43
  }
@@ -80,48 +87,181 @@ function collectRuns(sessionDir) {
80
87
  });
81
88
  }
82
89
  function collectWorkflowRuns(cwd, projectsDir = claudeProjectsDir()) {
83
- const projectDir = path.join(projectsDir, projectKey(cwd));
84
- if (!existsSync(projectDir))
90
+ return directories(path.join(projectsDir, projectKey(cwd))).flatMap(collectRuns).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
91
+ }
92
+ function resolvedPath(cwd) {
93
+ try {
94
+ return realpathSync(cwd);
95
+ } catch {
85
96
  return null;
86
- return directories(projectDir).flatMap(collectRuns).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
97
+ }
87
98
  }
88
- function fmt(value) {
89
- return value.toLocaleString("en-US");
99
+ function mainWorktreePath(cwd) {
100
+ const dotGit = path.join(cwd, ".git");
101
+ if (!existsSync(dotGit) || !statSync(dotGit).isFile())
102
+ return null;
103
+ const gitdir = readFileSync(dotGit, "utf8").match(/^gitdir: *(\S.*)$/m)?.[1]?.trim();
104
+ return gitdir?.match(/^(.+)\/\.git\/worktrees\/[^/]+\/?$/)?.[1] ?? null;
90
105
  }
91
- function printCost(ui2, runs, last) {
92
- if (runs == null) {
93
- ui2.flatline("No Claude Code session data for this directory.");
94
- return 1;
106
+ function recordedElsewhere(cwd, key, projectsDir) {
107
+ return [resolvedPath(cwd), mainWorktreePath(cwd)].filter((candidate) => candidate != null).map(projectKey).filter((candidate) => candidate !== key && existsSync(path.join(projectsDir, candidate)));
108
+ }
109
+ function lookalikeKeys(cwd, key, projectsDir) {
110
+ const suffix = `-${path.basename(cwd)}`;
111
+ return directories(projectsDir).map((dir) => path.basename(dir)).filter((name) => name !== key && name.endsWith(suffix));
112
+ }
113
+ var ClaudeCodeCostSource = class {
114
+ constructor(projectsDir = claudeProjectsDir()) {
115
+ this.projectsDir = projectsDir;
95
116
  }
96
- const selected = last ? runs.slice(-1) : runs;
97
- if (selected.length === 0) {
98
- ui2.glitch("No /implement runs recorded here yet.");
99
- return 0;
117
+ projectsDir;
118
+ runtime = "claude-code";
119
+ readable() {
120
+ return existsSync(this.projectsDir);
100
121
  }
101
- const grand = emptyUsage();
102
- for (const run of selected) {
103
- ui2.line(`${ui2.theme.accent(run.run)} ${ui2.theme.dim(`${run.startedAt} \xB7 ${run.total.models.join(", ")}`)}`);
104
- for (const agent of run.agents) {
105
- ui2.line(` ${agent.type.padEnd(12)} ${agent.label.padEnd(28)} calls ${String(agent.usage.calls).padStart(3)} in ${fmt(agent.usage.input).padStart(8)} cache-w ${fmt(agent.usage.cacheWrite).padStart(9)} cache-r ${fmt(agent.usage.cacheRead).padStart(10)} out ${fmt(agent.usage.output).padStart(7)}`);
122
+ read(cwd) {
123
+ const key = projectKey(cwd);
124
+ if (existsSync(path.join(this.projectsDir, key))) {
125
+ const runs = collectWorkflowRuns(cwd, this.projectsDir);
126
+ return { status: runs.length > 0 ? "ok" : "empty", runs, key, candidates: [] };
106
127
  }
107
- ui2.line(` ${ui2.theme.bold(`total ${fmt(billable(run.total))} billable tokens in ${run.total.calls} calls`)} ${ui2.theme.dim(`\u2248 ${fmt(weighted(run.total))} input-equivalent`)}`);
108
- ui2.line();
109
- add(grand, run.total);
128
+ const recorded = recordedElsewhere(cwd, key, this.projectsDir);
129
+ if (recorded.length > 0)
130
+ return { status: "mismatch", runs: [], key, candidates: recorded };
131
+ const lookalikes = lookalikeKeys(cwd, key, this.projectsDir);
132
+ if (lookalikes.length > 0)
133
+ return { status: "unknown", runs: [], key, candidates: lookalikes };
134
+ return { status: "empty", runs: [], key, candidates: [] };
110
135
  }
111
- if (selected.length > 1)
112
- ui2.line(`${ui2.theme.bold(`${selected.length} runs: ${fmt(billable(grand))} billable tokens in ${grand.calls} calls`)} ${ui2.theme.dim(`\u2248 ${fmt(weighted(grand))} input-equivalent (cache-write \xD7${PRICE_RELATIVE_TO_INPUT.cacheWrite}, cache-read \xD7${PRICE_RELATIVE_TO_INPUT.cacheRead}, output \xD7${PRICE_RELATIVE_TO_INPUT.output})`)}`);
113
- return 0;
136
+ };
137
+
138
+ // src/commands/cost/ledger.ts
139
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
140
+ import path2 from "path";
141
+ var LEDGER_FILE = ".construct/runs.jsonl";
142
+ var TEXT_FIELDS = ["at", "task", "effort", "status", "rung"];
143
+ var COUNT_FIELDS = ["agents", "toolUses", "seconds"];
144
+ function isText(value) {
145
+ return typeof value === "string" && value !== "";
146
+ }
147
+ function isCount(value) {
148
+ return typeof value === "number" && Number.isFinite(value);
149
+ }
150
+ function isTokenCount(value) {
151
+ return value === "unknown" || isCount(value);
152
+ }
153
+ function attemptFaults(value, index) {
154
+ if (value == null || typeof value !== "object")
155
+ return [`attempts[${index}] is not a record`];
156
+ const attempt = value;
157
+ return [
158
+ isCount(attempt.rung) ? null : `attempts[${index}].rung`,
159
+ isText(attempt.effort) ? null : `attempts[${index}].effort`,
160
+ isText(attempt.outcome) ? null : `attempts[${index}].outcome`,
161
+ typeof attempt.reason === "string" ? null : `attempts[${index}].reason`
162
+ ].filter((fault) => fault != null);
163
+ }
164
+ function attemptListFaults(value) {
165
+ if (!Array.isArray(value))
166
+ return ["attempts"];
167
+ return value.flatMap(attemptFaults);
168
+ }
169
+ function undeclaredFields(record) {
170
+ const missing = [
171
+ ...TEXT_FIELDS.filter((field) => !isText(record[field])),
172
+ ...COUNT_FIELDS.filter((field) => !isCount(record[field]))
173
+ ];
174
+ if (!isTokenCount(record.tokens))
175
+ missing.push("tokens");
176
+ missing.push(...attemptListFaults(record.attempts));
177
+ return missing;
178
+ }
179
+ function toAttempt(value) {
180
+ const attempt = value;
181
+ return { rung: attempt.rung, effort: attempt.effort, outcome: attempt.outcome, reason: attempt.reason };
182
+ }
183
+ function toEntry(raw) {
184
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw))
185
+ return "not a run record";
186
+ const record = raw;
187
+ const missing = undeclaredFields(record);
188
+ if (missing.length > 0)
189
+ return `missing or invalid: ${missing.join(", ")}`;
190
+ return {
191
+ run: isText(record.run) ? record.run : null,
192
+ at: record.at,
193
+ task: record.task,
194
+ effort: record.effort,
195
+ status: record.status,
196
+ rung: record.rung,
197
+ attempts: record.attempts.map(toAttempt),
198
+ agents: record.agents,
199
+ tokens: record.tokens,
200
+ toolUses: record.toolUses,
201
+ seconds: record.seconds
202
+ };
203
+ }
204
+ function readLedger(root) {
205
+ const file = path2.join(root, LEDGER_FILE);
206
+ const reading = { entries: [], malformed: [] };
207
+ if (!existsSync2(file))
208
+ return reading;
209
+ readFileSync2(file, "utf8").split("\n").forEach((text2, index) => {
210
+ const line = index + 1;
211
+ if (text2.trim() === "")
212
+ return;
213
+ let raw;
214
+ try {
215
+ raw = JSON.parse(text2);
216
+ } catch {
217
+ reading.malformed.push({ line, reason: "not JSON" });
218
+ return;
219
+ }
220
+ const entry = toEntry(raw);
221
+ if (typeof entry === "string")
222
+ reading.malformed.push({ line, reason: entry });
223
+ else
224
+ reading.entries.push(entry);
225
+ });
226
+ return reading;
227
+ }
228
+ function summarizeLedger(reading) {
229
+ const anyTokenUnknown = reading.entries.some((entry) => entry.tokens === "unknown");
230
+ const counted = reading.entries.reduce((total, entry) => total + (entry.tokens === "unknown" ? 0 : entry.tokens), 0);
231
+ return {
232
+ runs: reading.entries.length,
233
+ agents: reading.entries.reduce((total, entry) => total + entry.agents, 0),
234
+ failures: reading.entries.filter((entry) => entry.status !== "done").length,
235
+ tokens: anyTokenUnknown ? "unknown" : counted,
236
+ malformed: reading.malformed
237
+ };
238
+ }
239
+ function withoutTokenTotals(summary) {
240
+ return { ...summary, tokens: "unknown" };
241
+ }
242
+ function hasLedgerFindings(summary) {
243
+ return summary.runs > 0 || summary.malformed.length > 0;
244
+ }
245
+ function reconcile(entries, runs) {
246
+ const sessionRuns = new Set(runs.map((run) => run.run));
247
+ const ledgerRuns = entries.map((entry) => entry.run).filter((run) => run != null);
248
+ const joinable = new Set(ledgerRuns);
249
+ return {
250
+ entriesWithoutSession: [...joinable].filter((run) => !sessionRuns.has(run)),
251
+ sessionsWithoutEntry: runs.map((run) => run.run).filter((run) => !joinable.has(run)),
252
+ unjoinable: entries.length - ledgerRuns.length
253
+ };
114
254
  }
115
255
 
116
- // src/commands/doctor.ts
117
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
118
- import path3 from "path";
256
+ // src/commands/cost/runtime.ts
257
+ import process from "process";
119
258
 
120
259
  // src/manifest.ts
121
260
  import { createHash } from "crypto";
122
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
123
- import path2 from "path";
261
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
262
+ import path3 from "path";
124
263
  var MANIFEST_FILE = "construct.json";
264
+ var MANIFEST_VERSION = 2;
125
265
  var DISCOVERY_MARKERS = [
126
266
  "product",
127
267
  "module-map",
@@ -151,8 +291,13 @@ function buildManifest(input) {
151
291
  const files = {};
152
292
  for (const op of input.written)
153
293
  files[op.target] = sha256(op.content);
154
- const discovery = Object.fromEntries(DISCOVERY_MARKERS.map((marker) => [marker, markerFile(marker, input.vars.compositionDir)]));
294
+ const markers = Object.fromEntries(DISCOVERY_MARKERS.map((marker) => [marker, {
295
+ file: markerFile(marker, input.vars.compositionDir),
296
+ authoredBy: "unknown",
297
+ sha: null
298
+ }]));
155
299
  return {
300
+ manifestVersion: MANIFEST_VERSION,
156
301
  construct: input.version,
157
302
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
158
303
  preset: input.preset,
@@ -163,21 +308,378 @@ function buildManifest(input) {
163
308
  contracts: input.contracts ? { path: input.vars.contractPath, types: input.vars.contractTypesOutput } : null,
164
309
  vars: input.vars,
165
310
  files,
166
- discovery
311
+ discovery: { baseSha: null, filledAt: null, markers }
312
+ };
313
+ }
314
+ function upgradeMarker(recorded, file) {
315
+ if (typeof recorded === "string")
316
+ return { file: recorded, authoredBy: "unknown", sha: null };
317
+ const value = recorded ?? {};
318
+ return {
319
+ file: typeof value.file === "string" ? value.file : file,
320
+ authoredBy: value.authoredBy === "construct" ? "construct" : "unknown",
321
+ sha: typeof value.sha === "string" ? value.sha : null
322
+ };
323
+ }
324
+ function upgradeManifest(raw) {
325
+ const manifest = raw;
326
+ const discovery = manifest.discovery ?? {};
327
+ const recorded = discovery.markers ?? discovery;
328
+ const markers = Object.fromEntries(DISCOVERY_MARKERS.map((marker) => [
329
+ marker,
330
+ upgradeMarker(recorded[marker], markerFile(marker, manifest.vars?.compositionDir))
331
+ ]));
332
+ return {
333
+ ...manifest,
334
+ manifestVersion: MANIFEST_VERSION,
335
+ discovery: {
336
+ baseSha: typeof discovery.baseSha === "string" ? discovery.baseSha : null,
337
+ filledAt: typeof discovery.filledAt === "string" ? discovery.filledAt : null,
338
+ markers
339
+ }
167
340
  };
168
341
  }
169
342
  function writeManifest(root, manifest) {
170
- writeFileSync(path2.join(root, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
343
+ writeFileSync(path3.join(root, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
171
344
  `);
172
345
  }
173
346
  function readManifest(root) {
174
- const file = path2.join(root, MANIFEST_FILE);
175
- if (!existsSync2(file))
347
+ const file = path3.join(root, MANIFEST_FILE);
348
+ if (!existsSync3(file))
176
349
  return null;
177
- return JSON.parse(readFileSync2(file, "utf8"));
350
+ return upgradeManifest(JSON.parse(readFileSync3(file, "utf8")));
351
+ }
352
+
353
+ // src/commands/cost/runtime.ts
354
+ var CLAUDE_CODE_ENV = ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"];
355
+ var CURSOR_ENV = ["CURSOR_AGENT", "CURSOR_TRACE_ID"];
356
+ function anySet(env, names) {
357
+ return names.some((name) => (env[name] ?? "") !== "");
358
+ }
359
+ function resolveRuntime(root, env = process.env) {
360
+ if (anySet(env, CLAUDE_CODE_ENV))
361
+ return "claude-code";
362
+ if (anySet(env, CURSOR_ENV))
363
+ return "cursor";
364
+ return readManifest(root)?.ai === "cursor" ? "cursor" : "claude-code";
365
+ }
366
+
367
+ // src/commands/cost/report.ts
368
+ var COST_EXIT = {
369
+ ok: 0,
370
+ empty: 0,
371
+ mismatch: 1,
372
+ unknown: 1,
373
+ unsupported: 3
374
+ };
375
+ function selectRuns(report, last) {
376
+ const runs = report.runs ?? [];
377
+ return last ? runs.slice(-1) : runs;
378
+ }
379
+ function costJson(report, last) {
380
+ const runs = selectRuns(report, last);
381
+ return {
382
+ status: report.status,
383
+ runtime: report.runtime,
384
+ ...report.key == null ? {} : { key: report.key },
385
+ ...report.candidates == null || report.candidates.length === 0 ? {} : { candidates: report.candidates },
386
+ ...runs.length === 0 ? {} : { runs },
387
+ ...report.ledger == null ? {} : { ledger: report.ledger },
388
+ ...report.reconciliation == null ? {} : { reconciliation: report.reconciliation }
389
+ };
390
+ }
391
+ function fmt(value) {
392
+ return value.toLocaleString("en-US");
393
+ }
394
+ function tokens(value) {
395
+ return value === "unknown" ? "unknown" : fmt(value);
396
+ }
397
+ function printLedger(ui2, ledger, reconciliation) {
398
+ if (ledger == null && reconciliation == null)
399
+ return;
400
+ if (ledger != null) {
401
+ ui2.line(ui2.theme.dim(ui2.lore.ledgerCounts(ledger.runs, ledger.agents, ledger.failures, tokens(ledger.tokens))));
402
+ if (ledger.malformed.length > 0)
403
+ ui2.glitch(ui2.lore.ledgerMalformed(ledger.malformed.length), ledger.malformed.map((entry) => `line ${entry.line}: ${entry.reason}`));
404
+ }
405
+ if (reconciliation == null)
406
+ return;
407
+ ui2.line(ui2.theme.dim(ui2.lore.ledgerDrift(reconciliation.entriesWithoutSession.length, reconciliation.sessionsWithoutEntry.length, reconciliation.unjoinable)));
408
+ for (const run of reconciliation.entriesWithoutSession)
409
+ ui2.line(ui2.theme.dim(` ${run}: ${ui2.lore.ledgerEntryWithoutSession}`));
410
+ for (const run of reconciliation.sessionsWithoutEntry)
411
+ ui2.line(ui2.theme.dim(` ${run}: ${ui2.lore.ledgerSessionWithoutEntry}`));
412
+ }
413
+ function printRuns(ui2, runs) {
414
+ const grand = emptyUsage();
415
+ for (const run of runs) {
416
+ ui2.line(`${ui2.theme.accent(run.run)} ${ui2.theme.dim(`${run.startedAt} \xB7 ${run.total.models.join(", ")}`)}`);
417
+ for (const agent of run.agents)
418
+ ui2.line(` ${agent.type.padEnd(12)} ${agent.label.padEnd(28)} calls ${String(agent.usage.calls).padStart(3)} in ${fmt(agent.usage.input).padStart(8)} cache-w ${fmt(agent.usage.cacheWrite).padStart(9)} cache-r ${fmt(agent.usage.cacheRead).padStart(10)} out ${fmt(agent.usage.output).padStart(7)}`);
419
+ ui2.line(` ${ui2.theme.bold(`total ${fmt(billable(run.total))} billable tokens in ${run.total.calls} calls`)} ${ui2.theme.dim(`\u2248 ${fmt(weighted(run.total))} input-equivalent`)}`);
420
+ ui2.line();
421
+ add(grand, run.total);
422
+ }
423
+ if (runs.length > 1)
424
+ ui2.line(`${ui2.theme.bold(`${runs.length} runs: ${fmt(billable(grand))} billable tokens in ${grand.calls} calls`)} ${ui2.theme.dim(`\u2248 ${fmt(weighted(grand))} input-equivalent (cache-write \xD7${PRICE_RELATIVE_TO_INPUT.cacheWrite}, cache-read \xD7${PRICE_RELATIVE_TO_INPUT.cacheRead}, output \xD7${PRICE_RELATIVE_TO_INPUT.output})`)}`);
425
+ }
426
+ function printCost(ui2, report, last) {
427
+ const key = report.key ?? "";
428
+ const candidates = report.candidates ?? [];
429
+ switch (report.status) {
430
+ case "unsupported":
431
+ ui2.glitch(ui2.lore.costUnsupported(report.runtime));
432
+ break;
433
+ case "mismatch":
434
+ ui2.glitch(ui2.lore.costKeyMismatch(key), candidates);
435
+ break;
436
+ case "unknown":
437
+ ui2.glitch(ui2.lore.costKeyUnknown(key), candidates);
438
+ break;
439
+ case "empty":
440
+ ui2.glitch(ui2.lore.costEmpty);
441
+ break;
442
+ case "ok":
443
+ printRuns(ui2, selectRuns(report, last));
444
+ break;
445
+ }
446
+ printLedger(ui2, report.ledger, report.reconciliation);
447
+ return COST_EXIT[report.status];
448
+ }
449
+
450
+ // src/commands/cost/index.ts
451
+ function costReport(cwd, options = {}) {
452
+ const runtime = resolveRuntime(cwd, options.env ?? process2.env);
453
+ const reading = readLedger(cwd);
454
+ const ledger = summarizeLedger(reading);
455
+ const reported = hasLedgerFindings(ledger);
456
+ const source = runtime === "claude-code" ? new ClaudeCodeCostSource(options.projectsDir) : null;
457
+ if (source == null || !source.readable())
458
+ return { status: "unsupported", runtime, ...reported ? { ledger: withoutTokenTotals(ledger) } : {} };
459
+ const result = source.read(cwd);
460
+ const joinable = result.status === "ok" || result.status === "empty";
461
+ return {
462
+ runtime,
463
+ ...result,
464
+ ...reported ? { ledger } : {},
465
+ ...joinable && (reported || result.runs.length > 0) ? { reconciliation: reconcile(reading.entries, result.runs) } : {}
466
+ };
467
+ }
468
+
469
+ // src/commands/doctor/baseline.ts
470
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
471
+ import path4 from "path";
472
+ function baselineVerdict(root, manifest) {
473
+ const missingFiles = [];
474
+ const modifiedFiles = [];
475
+ for (const [file, hash] of Object.entries(manifest.files)) {
476
+ const absolute = path4.join(root, file);
477
+ if (!existsSync4(absolute))
478
+ missingFiles.push(file);
479
+ else if (sha256(readFileSync4(absolute, "utf8")) !== hash)
480
+ modifiedFiles.push(file);
481
+ }
482
+ return { missingFiles, modifiedFiles };
483
+ }
484
+
485
+ // src/commands/doctor/enforcement.ts
486
+ function harnessReach(evidence) {
487
+ const command = `"${evidence.harness.command}"`;
488
+ if (evidence.workflows.harnessWorkflow != null)
489
+ return { level: "L3", evidence: `${evidence.workflows.harnessWorkflow} runs ${command}` };
490
+ if (evidence.hooks.runsHarness && evidence.hooks.manager != null)
491
+ return { level: "L2", evidence: `${evidence.hooks.manager} runs ${command}, and a local hook is bypassable with --no-verify` };
492
+ return { level: "L0", evidence: `no workflow and no hook configuration runs ${command}` };
493
+ }
494
+ function reached(id, state, evidence, detail) {
495
+ const reach = harnessReach(evidence);
496
+ return { id, level: reach.level, state, evidence: `${detail}; ${reach.evidence}` };
497
+ }
498
+ function absent(id, detail) {
499
+ return { id, level: "L0", state: "absent", evidence: detail };
500
+ }
501
+
502
+ // src/commands/doctor/checks/ci.ts
503
+ var ID = "ci";
504
+ var SCOPE = "branch protection and organisation rulesets live in the GitHub API, not in the repository, so doctor cannot see whether this blocks a merge";
505
+ function ciCheck(evidence) {
506
+ const workflows = evidence.workflows;
507
+ const reach = harnessReach(evidence);
508
+ if (workflows.harnessWorkflow != null)
509
+ return { id: ID, level: reach.level, state: "present", evidence: `${reach.evidence}; ${SCOPE}` };
510
+ const seen = workflows.files.length === 0 ? `${workflows.directory} holds no workflow file` : `none of ${workflows.files.map((file) => `${workflows.directory}/${file}`).join(", ")} runs "${evidence.harness.command}"`;
511
+ return { id: ID, level: reach.level, state: "unknown", evidence: `${seen}; ${SCOPE}` };
512
+ }
513
+
514
+ // src/commands/doctor/runner.ts
515
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
516
+ import path5 from "path";
517
+ var RUNNER_CONFIG_FILES = [
518
+ "vitest.config.ts",
519
+ "vitest.config.mts",
520
+ "vitest.config.js",
521
+ "vitest.config.mjs",
522
+ "vite.config.ts",
523
+ "vite.config.mts",
524
+ "vite.config.js",
525
+ "vite.config.mjs"
526
+ ];
527
+ var STRING_LITERAL = /^(['"])(.*)\1$/;
528
+ function literalEntries(body) {
529
+ const trimmed = body.trim();
530
+ if (trimmed === "")
531
+ return [];
532
+ const entries = trimmed.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
533
+ const values = entries.map((entry) => STRING_LITERAL.exec(entry)?.[2]);
534
+ return values.every((value) => value != null) ? values : null;
535
+ }
536
+ function includeGlobs(source) {
537
+ const found = [];
538
+ let literal = false;
539
+ for (const match of source.matchAll(/include\s*:\s*/g)) {
540
+ const rest = source.slice(match.index + match[0].length);
541
+ if (!rest.startsWith("["))
542
+ return null;
543
+ const close = rest.indexOf("]");
544
+ if (close === -1)
545
+ return null;
546
+ const entries = literalEntries(rest.slice(1, close));
547
+ if (entries == null)
548
+ return null;
549
+ literal = true;
550
+ found.push(...entries);
551
+ }
552
+ return literal ? found : null;
553
+ }
554
+ function escapeLiteral(character) {
555
+ return /[.+^${}()|[\]\\]/.test(character) ? `\\${character}` : character;
556
+ }
557
+ function globToRegExp(glob) {
558
+ let pattern = "";
559
+ let braces = 0;
560
+ for (let index = 0; index < glob.length; index += 1) {
561
+ const character = glob[index];
562
+ if (character === "*" && glob[index + 1] === "*" && glob[index + 2] === "/") {
563
+ pattern += "(?:[^/]+/)*";
564
+ index += 2;
565
+ continue;
566
+ }
567
+ if (character === "*" && glob[index + 1] === "*") {
568
+ pattern += ".*";
569
+ index += 1;
570
+ continue;
571
+ }
572
+ if (character === "*") {
573
+ pattern += "[^/]*";
574
+ continue;
575
+ }
576
+ if (character === "?") {
577
+ pattern += "[^/]";
578
+ continue;
579
+ }
580
+ if (character === "{") {
581
+ braces += 1;
582
+ pattern += "(?:";
583
+ continue;
584
+ }
585
+ if (character === "}" && braces > 0) {
586
+ braces -= 1;
587
+ pattern += ")";
588
+ continue;
589
+ }
590
+ if (character === "," && braces > 0) {
591
+ pattern += "|";
592
+ continue;
593
+ }
594
+ pattern += escapeLiteral(character);
595
+ }
596
+ return new RegExp(`^${pattern}$`);
597
+ }
598
+ function matchesAnyGlob(file, globs) {
599
+ const normalized = file.replace(/^\.\//, "");
600
+ return globs.some((glob) => globToRegExp(glob.replace(/^\.\//, "")).test(normalized));
601
+ }
602
+ function readRunnerFacts(root, harnessText) {
603
+ const invokedByHarness = harnessText.includes("vitest");
604
+ const file = RUNNER_CONFIG_FILES.find((candidate) => existsSync5(path5.join(root, candidate))) ?? null;
605
+ if (file == null) {
606
+ return {
607
+ file: null,
608
+ globs: null,
609
+ note: `no runner config file (${RUNNER_CONFIG_FILES[0]} or a sibling) exists, so the include list cannot be read`,
610
+ invokedByHarness
611
+ };
612
+ }
613
+ let source;
614
+ try {
615
+ source = readFileSync5(path5.join(root, file), "utf8");
616
+ } catch {
617
+ return { file, globs: null, note: `${file} cannot be read, so the include list is unknown`, invokedByHarness };
618
+ }
619
+ const globs = includeGlobs(source);
620
+ if (globs == null)
621
+ return { file, globs: null, note: `the include in ${file} is not a literal list of strings, so doctor cannot say what the runner collects`, invokedByHarness };
622
+ return { file, globs, note: `${file} includes ${globs.map((glob) => `"${glob}"`).join(", ")}`, invokedByHarness };
178
623
  }
179
624
 
180
- // src/commands/doctor.ts
625
+ // src/commands/doctor/checks/construct-tests.ts
626
+ var ID2 = "construct-tests";
627
+ function constructTestsCheck(evidence) {
628
+ const recorded = evidence.recordedTests;
629
+ if (recorded.length === 0)
630
+ return absent(ID2, "construct.json records no test file; weakest link: there are no construct tests to run");
631
+ const runner = evidence.runner;
632
+ if (runner.globs == null)
633
+ return reached(ID2, "unknown", evidence, `construct.json records ${recorded.length} test files, but ${runner.note}`);
634
+ const orphan = recorded.find((file) => !matchesAnyGlob(file, runner.globs ?? []));
635
+ if (orphan != null)
636
+ return absent(ID2, `${orphan} is recorded in construct.json, but ${runner.note}, so the runner never collects it; weakest link: a construct test that nothing runs`);
637
+ if (!runner.invokedByHarness)
638
+ return absent(ID2, `construct.json records ${recorded.length} test files and ${runner.note}, but "${evidence.harness.script}" does not run the test runner; weakest link: the harness command never reaches them`);
639
+ return reached(ID2, "present", evidence, `all ${recorded.length} test files recorded in construct.json match the include in ${runner.file}, and "${evidence.harness.script}" runs the test runner`);
640
+ }
641
+
642
+ // src/commands/doctor/checks/hook.ts
643
+ var ID3 = "hook";
644
+ function hookCheck(evidence) {
645
+ const hooks = evidence.hooks;
646
+ if (hooks.manager != null) {
647
+ const runs = hooks.runsHarness ? `runs "${evidence.harness.command}"` : `does not run "${evidence.harness.command}"`;
648
+ return { id: ID3, level: "L2", state: "present", evidence: `${hooks.manager} installs a git hook and ${runs}; a local hook is bypassable with --no-verify` };
649
+ }
650
+ if (hooks.script != null)
651
+ return { id: ID3, level: "L0", state: "present", evidence: `package.json script "${hooks.script}" is claimed as a guard, but no .husky, lefthook, simple-git-hooks or core.hooksPath configuration installs it; weakest link: nobody is obliged to run it` };
652
+ return { id: ID3, level: "L0", state: "absent", evidence: "no .husky, lefthook, simple-git-hooks or core.hooksPath configuration and no pre-commit script in package.json" };
653
+ }
654
+
655
+ // src/commands/doctor/checks/lint-policy.ts
656
+ var ID4 = "lint-policy";
657
+ function lintPolicyCheck(evidence) {
658
+ const policy = evidence.policyTests[0];
659
+ if (policy == null) {
660
+ if (evidence.unreadable.length > 0)
661
+ return reached(ID4, "unknown", evidence, `${evidence.unreadable[0]} cannot be read, so doctor cannot say whether a lint policy check exists`);
662
+ return absent(ID4, "no file recorded in construct.json runs ESLint over the lint policy this repository declares; weakest link: the repository has no policy check to run");
663
+ }
664
+ const runner = evidence.runner;
665
+ if (runner.globs == null)
666
+ return reached(ID4, "unknown", evidence, `${policy} runs ESLint over the lint policy this repository declares, but ${runner.note}`);
667
+ if (!matchesAnyGlob(policy, runner.globs))
668
+ return absent(ID4, `${policy} runs ESLint over the lint policy this repository declares, but ${runner.note}; weakest link: the runner never collects the policy check`);
669
+ if (!runner.invokedByHarness)
670
+ return absent(ID4, `${policy} runs ESLint over the lint policy this repository declares and ${runner.note}, but "${evidence.harness.script}" does not run the test runner; weakest link: the harness command never reaches the policy check`);
671
+ return reached(ID4, "present", evidence, `${policy} runs ESLint over the lint policy this repository declares, ${runner.note}, and "${evidence.harness.script}" runs the test runner`);
672
+ }
673
+
674
+ // src/commands/doctor/checks/red-gate.ts
675
+ var ID5 = "red-gate";
676
+ function redGateCheck(evidence) {
677
+ return reached(ID5, "unknown", evidence, `doctor executes nothing from the repository it inspects, so whether "${evidence.harness.command}" passes on a clean checkout is unproven here; CI is where that is proven`);
678
+ }
679
+
680
+ // src/commands/doctor/discovery.ts
681
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
682
+ import path6 from "path";
181
683
  var DISCOVERY_PLACEHOLDER = "_Not discovered yet \u2014 run `/construct-discover`._";
182
684
  function markerOpen(marker) {
183
685
  return `<!-- construct:discover:${marker} -->`;
@@ -185,66 +687,290 @@ function markerOpen(marker) {
185
687
  function markerClose(marker) {
186
688
  return `<!-- /construct:discover:${marker} -->`;
187
689
  }
188
- function isMarkerFilled(document, marker) {
690
+ function blockBody(document, marker) {
189
691
  const start = document.indexOf(markerOpen(marker));
190
692
  const stop = document.indexOf(markerClose(marker));
191
693
  if (start === -1 || stop === -1 || stop < start)
192
- return false;
694
+ return null;
193
695
  const body = document.slice(start + markerOpen(marker).length, stop).trim();
194
- return body !== "" && body !== DISCOVERY_PLACEHOLDER;
696
+ return body === "" || body === DISCOVERY_PLACEHOLDER ? null : body;
697
+ }
698
+ function compositionBody(directory) {
699
+ if (!existsSync6(directory))
700
+ return null;
701
+ const models = readdirSync2(directory).filter((file) => file.endsWith(".yaml")).sort();
702
+ if (models.length === 0)
703
+ return null;
704
+ return models.map((model) => `${model}
705
+ ${readFileSync6(path6.join(directory, model), "utf8")}`).join("\n");
706
+ }
707
+ function markerBody(root, marker, file) {
708
+ const location = path6.join(root, file);
709
+ if (marker === "composition")
710
+ return compositionBody(location);
711
+ if (!existsSync6(location))
712
+ return null;
713
+ return blockBody(readFileSync6(location, "utf8"), marker);
195
714
  }
715
+ function missingDiscovery(root, manifest) {
716
+ return DISCOVERY_MARKERS.filter((marker) => markerBody(root, marker, manifest.discovery.markers[marker].file) == null);
717
+ }
718
+
719
+ // src/commands/doctor/evidence.ts
720
+ import { readFileSync as readFileSync10 } from "fs";
721
+ import path10 from "path";
722
+
723
+ // src/commands/doctor/harness.ts
724
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
725
+ import path7 from "path";
196
726
  var REQUIRED_QUALITY_STEPS = ["lint", "typecheck", "test"];
197
- function contractProblems(root, contracts, scriptName, quality) {
727
+ var SCRIPT_REFERENCE = /(?:^|&&|\|\||;)\s*(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?([\w:.-]+)/g;
728
+ function harnessScriptName(command) {
729
+ return command.replace(/^(pnpm|npm|yarn|bun)\s+(run\s+)?/, "");
730
+ }
731
+ function expandScript(scripts, name, seen) {
732
+ if (seen.has(name))
733
+ return "";
734
+ seen.add(name);
735
+ const body = scripts[name];
736
+ if (body == null)
737
+ return "";
738
+ const referenced = [...body.matchAll(SCRIPT_REFERENCE)].map((match) => match[1]);
739
+ return [body, ...referenced.map((reference) => expandScript(scripts, reference, seen))].join(" && ");
740
+ }
741
+ function readHarnessFacts(root, command) {
742
+ const script = harnessScriptName(command);
743
+ const manifestPath = path7.join(root, "package.json");
744
+ const packageJson = existsSync7(manifestPath) ? JSON.parse(readFileSync7(manifestPath, "utf8")) : null;
745
+ const scripts = packageJson?.scripts ?? {};
746
+ const body = scripts[script] ?? null;
747
+ return {
748
+ command,
749
+ script,
750
+ scripts,
751
+ body,
752
+ resolved: expandScript(scripts, script, /* @__PURE__ */ new Set()),
753
+ commandForms: [command, `pnpm run ${script}`, `pnpm ${script}`, `npm run ${script}`, `yarn ${script}`],
754
+ packageJson
755
+ };
756
+ }
757
+ function runsHarnessCommand(text2, forms) {
758
+ return forms.some((form) => text2.includes(form));
759
+ }
760
+ function contractProblems(root, contracts, script, body) {
198
761
  if (contracts == null)
199
762
  return [];
200
- const problems = [contracts.path, contracts.types].filter((file) => !existsSync3(path3.join(root, file))).map((file) => `${file} is missing (construct.json \u2192 contracts)`);
201
- if (!quality.includes("contracts:check"))
202
- problems.push(`"${scriptName}" does not run contracts:check`);
763
+ const problems = [contracts.path, contracts.types].filter((file) => !existsSync7(path7.join(root, file))).map((file) => `${file} is missing (construct.json \u2192 contracts)`);
764
+ if (!body.includes("contracts:check"))
765
+ problems.push(`"${script}" does not run contracts:check`);
203
766
  return problems;
204
767
  }
205
- function harnessProblems(root, manifest) {
206
- const command = manifest.harness.command;
207
- const manifestPath = path3.join(root, "package.json");
208
- if (!existsSync3(manifestPath))
768
+ function harnessProblems(root, manifest, facts) {
769
+ if (facts.packageJson == null)
209
770
  return ["package.json is missing"];
210
- const pkg = JSON.parse(readFileSync3(manifestPath, "utf8"));
211
- const scripts = pkg.scripts ?? {};
212
- const scriptName = command.replace(/^(pnpm|npm|yarn|bun)\s+(run\s+)?/, "");
213
- const quality = scripts[scriptName];
214
- if (quality == null)
215
- return [`package.json has no "${scriptName}" script (harness command is "${command}")`];
771
+ const body = facts.body;
772
+ if (body == null)
773
+ return [`package.json has no "${facts.script}" script (harness command is "${facts.command}")`];
216
774
  return [
217
- ...REQUIRED_QUALITY_STEPS.filter((step) => !quality.includes(step)).map((step) => `"${scriptName}" does not run ${step}`),
218
- ...contractProblems(root, manifest.contracts, scriptName, quality)
775
+ ...REQUIRED_QUALITY_STEPS.filter((step) => !body.includes(step)).map((step) => `"${facts.script}" does not run ${step}`),
776
+ ...contractProblems(root, manifest.contracts, facts.script, body)
219
777
  ];
220
778
  }
221
- function runDoctor(root) {
222
- const manifest = readManifest(root);
223
- if (manifest == null)
779
+
780
+ // src/commands/doctor/hooks.ts
781
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
782
+ import path8 from "path";
783
+ var LEFTHOOK_FILES = ["lefthook.yml", "lefthook.yaml", "lefthook.toml", "lefthook.json", ".lefthook.yml", ".lefthook.yaml"];
784
+ var SIMPLE_GIT_HOOKS_FILES = [".simple-git-hooks.js", ".simple-git-hooks.cjs", ".simple-git-hooks.mjs", ".simple-git-hooks.json", "simple-git-hooks.json"];
785
+ var HUSKY_DIR = ".husky";
786
+ var GIT_CONFIG = ".git/config";
787
+ var HOOK_SCRIPTS = ["precommit", "pre-commit", "prepush", "pre-push"];
788
+ function read(root, file) {
789
+ try {
790
+ return readFileSync8(path8.join(root, file), "utf8");
791
+ } catch {
224
792
  return null;
225
- const missingFiles = [];
226
- const modifiedFiles = [];
227
- for (const [file, hash] of Object.entries(manifest.files)) {
228
- const absolute = path3.join(root, file);
229
- if (!existsSync3(absolute))
230
- missingFiles.push(file);
231
- else if (sha256(readFileSync3(absolute, "utf8")) !== hash)
232
- modifiedFiles.push(file);
233
793
  }
234
- const missingDiscovery = DISCOVERY_MARKERS.filter((marker) => {
235
- const location = path3.join(root, manifest.discovery[marker]);
236
- if (marker === "composition")
237
- return !existsSync3(location) || !readdirSync2(location).some((file) => file.endsWith(".yaml"));
238
- return !existsSync3(location) || !isMarkerFilled(readFileSync3(location, "utf8"), marker);
794
+ }
795
+ function huskyHooks(root) {
796
+ const directory = path8.join(root, HUSKY_DIR);
797
+ if (!existsSync8(directory) || !statSync2(directory).isDirectory())
798
+ return [];
799
+ return readdirSync3(directory).filter((entry) => !entry.startsWith("_") && !entry.startsWith(".")).sort().map((entry) => `${HUSKY_DIR}/${entry}`);
800
+ }
801
+ function managerFiles(root, packageJson) {
802
+ const files = [...huskyHooks(root)];
803
+ files.push(...[...LEFTHOOK_FILES, ...SIMPLE_GIT_HOOKS_FILES].filter((file) => existsSync8(path8.join(root, file))));
804
+ if (packageJson != null && "simple-git-hooks" in packageJson)
805
+ files.push("package.json (simple-git-hooks)");
806
+ const gitConfig = read(root, GIT_CONFIG);
807
+ if (gitConfig != null && gitConfig.includes("hooksPath"))
808
+ files.push(`${GIT_CONFIG} (core.hooksPath)`);
809
+ return files;
810
+ }
811
+ function readHookFacts(root, packageJson, scripts, commandForms) {
812
+ const files = managerFiles(root, packageJson);
813
+ const script = HOOK_SCRIPTS.find((name) => scripts[name] != null) ?? null;
814
+ const sources = files.map((file) => {
815
+ if (file.startsWith("package.json"))
816
+ return JSON.stringify(packageJson?.["simple-git-hooks"] ?? "");
817
+ return read(root, file.split(" ")[0]) ?? "";
239
818
  });
240
- const problems = harnessProblems(root, manifest);
241
- return {
242
- ok: missingFiles.length === 0 && problems.length === 0,
243
- missingFiles,
244
- modifiedFiles,
245
- missingDiscovery,
246
- harnessProblems: problems
247
- };
819
+ const manager = files[0] ?? null;
820
+ const scriptRunsHarness = script != null && runsHarnessCommand(scripts[script] ?? "", commandForms);
821
+ const installed = sources.some((source) => runsHarnessCommand(source, commandForms) || scriptRunsHarness && script != null && source.includes(script));
822
+ return { manager, script, runsHarness: manager != null && installed };
823
+ }
824
+
825
+ // src/commands/doctor/workflows.ts
826
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
827
+ import path9 from "path";
828
+ var WORKFLOWS_DIR = ".github/workflows";
829
+ var RUN_STEP = /(?:^|\s)run:[ \t]*(\S.*)$/;
830
+ function runStepTexts(source) {
831
+ const lines = source.split("\n");
832
+ const steps = [];
833
+ for (let index = 0; index < lines.length; index += 1) {
834
+ const inline = RUN_STEP.exec(lines[index])?.[1];
835
+ if (inline == null)
836
+ continue;
837
+ if (!inline.startsWith("|") && !inline.startsWith(">")) {
838
+ steps.push(inline.trim());
839
+ continue;
840
+ }
841
+ const indent = lines[index].length - lines[index].trimStart().length;
842
+ const block = [];
843
+ for (let next = index + 1; next < lines.length; next += 1) {
844
+ const line = lines[next];
845
+ if (line.trim() !== "" && line.length - line.trimStart().length <= indent)
846
+ break;
847
+ block.push(line.trim());
848
+ }
849
+ steps.push(block.join("\n"));
850
+ }
851
+ return steps;
852
+ }
853
+ function readWorkflowFacts(root, commandForms) {
854
+ const directory = path9.join(root, WORKFLOWS_DIR);
855
+ if (!existsSync9(directory))
856
+ return { directory: WORKFLOWS_DIR, files: [], harnessWorkflow: null, unreadable: [] };
857
+ const files = readdirSync4(directory).filter((file) => file.endsWith(".yml") || file.endsWith(".yaml")).sort();
858
+ const unreadable = [];
859
+ let harnessWorkflow = null;
860
+ for (const file of files) {
861
+ let source;
862
+ try {
863
+ source = readFileSync9(path9.join(directory, file), "utf8");
864
+ } catch {
865
+ unreadable.push(`${WORKFLOWS_DIR}/${file}`);
866
+ continue;
867
+ }
868
+ if (harnessWorkflow == null && runStepTexts(source).some((step) => runsHarnessCommand(step, commandForms)))
869
+ harnessWorkflow = `${WORKFLOWS_DIR}/${file}`;
870
+ }
871
+ return { directory: WORKFLOWS_DIR, files, harnessWorkflow, unreadable };
872
+ }
873
+
874
+ // src/commands/doctor/evidence.ts
875
+ var TEST_FILE = /\.test\.[cm]?[jt]s$/;
876
+ var LINT_POLICY_MARKERS = ["calculateConfigForFile", "lintText", "lintFiles"];
877
+ function recordedTestFiles(manifest) {
878
+ return Object.keys(manifest.files).filter((file) => TEST_FILE.test(file)).sort();
879
+ }
880
+ function gatherEvidence(root, manifest) {
881
+ const harness = readHarnessFacts(root, manifest.harness.command);
882
+ const runner = readRunnerFacts(root, harness.resolved);
883
+ const workflows = readWorkflowFacts(root, harness.commandForms);
884
+ const hooks = readHookFacts(root, harness.packageJson, harness.scripts, harness.commandForms);
885
+ const recordedTests = recordedTestFiles(manifest);
886
+ const policyTests = [];
887
+ const unreadable = [...workflows.unreadable];
888
+ for (const file of recordedTests) {
889
+ try {
890
+ const source = readFileSync10(path10.join(root, file), "utf8");
891
+ if (LINT_POLICY_MARKERS.some((marker) => source.includes(marker)))
892
+ policyTests.push(file);
893
+ } catch {
894
+ unreadable.push(file);
895
+ }
896
+ }
897
+ return { harness, runner, workflows, hooks, recordedTests, policyTests, unreadable };
898
+ }
899
+
900
+ // src/commands/doctor/provenance.ts
901
+ function markerAuthorship(recorded, body) {
902
+ if (recorded.authoredBy !== "construct" || recorded.sha == null || body == null)
903
+ return "unknown";
904
+ return sha256(body) === recorded.sha ? "construct" : "owner";
905
+ }
906
+ function discoveryProvenance(root, manifest) {
907
+ return DISCOVERY_MARKERS.map((marker) => {
908
+ const recorded = manifest.discovery.markers[marker];
909
+ return {
910
+ marker,
911
+ file: recorded.file,
912
+ authorship: markerAuthorship(recorded, markerBody(root, marker, recorded.file))
913
+ };
914
+ });
915
+ }
916
+ function constructAuthored(readings) {
917
+ return readings.filter((reading) => reading.authorship === "construct");
918
+ }
919
+
920
+ // src/commands/doctor/typecheck.ts
921
+ var CAVEATS = {
922
+ "node-frontend": {
923
+ checkers: ["vue-tsc", "svelte-check"],
924
+ text: "node-frontend: `tsc --noEmit` does not see `.vue` or `.svelte` single-file components, so a Vite app that adds them needs the framework's own checker (vue-tsc, svelte-check) in the harness"
925
+ }
926
+ };
927
+ function typecheckWarnings(preset, evidence) {
928
+ const caveat = CAVEATS[preset];
929
+ if (caveat == null || caveat.checkers.some((checker) => evidence.harness.resolved.includes(checker)))
930
+ return [];
931
+ return [caveat.text];
932
+ }
933
+
934
+ // src/commands/doctor/verdict.ts
935
+ var CHECK_IDS = ["lint-policy", "construct-tests", "ci", "hook", "red-gate"];
936
+ var LEVELS = ["L0", "L1", "L2", "L3", "L4"];
937
+ function weakestLink(checks) {
938
+ const claimed = checks.filter((check) => check.state === "present");
939
+ if (claimed.length === 0)
940
+ return null;
941
+ const ordered = [...claimed].sort((left, right) => CHECK_IDS.indexOf(left.id) - CHECK_IDS.indexOf(right.id));
942
+ const weakest = ordered.reduce((current, check) => LEVELS.indexOf(check.level) < LEVELS.indexOf(current.level) ? check : current);
943
+ return { id: weakest.id, level: weakest.level };
944
+ }
945
+
946
+ // src/commands/doctor/report.ts
947
+ function checkLine(ui2, check) {
948
+ return ` ${check.id.padEnd(16)} ${check.level} ${check.state.padEnd(8)} ${ui2.theme.dim(check.evidence)}`;
949
+ }
950
+ function printChecks(ui2, checks) {
951
+ ui2.line();
952
+ ui2.line(ui2.theme.accent(ui2.lore.enforcement));
953
+ for (const check of checks)
954
+ ui2.line(checkLine(ui2, check));
955
+ }
956
+ function printProvenance(ui2, provenance) {
957
+ const authored = constructAuthored(provenance);
958
+ if (authored.length === 0)
959
+ return;
960
+ ui2.line();
961
+ ui2.line(ui2.theme.accent(ui2.lore.provenance));
962
+ for (const reading of authored)
963
+ ui2.line(` ${reading.marker.padEnd(20)} ${ui2.theme.dim(reading.file)}`);
964
+ ui2.line(ui2.theme.dim(` ${ui2.lore.stillConstructAuthored(authored.length)}`));
965
+ }
966
+ function printWeakestLink(ui2, weakest) {
967
+ ui2.line();
968
+ if (weakest == null) {
969
+ ui2.line(ui2.theme.bold(ui2.lore.weakestLinkNone));
970
+ return;
971
+ }
972
+ const lift = ui2.lore.levelLift[weakest.level];
973
+ ui2.line(`${ui2.theme.bold(ui2.lore.weakestLink(weakest.id, weakest.level))}${lift == null ? "" : ui2.theme.dim(` \u2014 ${lift}`)}`);
248
974
  }
249
975
  function printDoctor(ui2, result) {
250
976
  if (result == null) {
@@ -255,79 +981,109 @@ function printDoctor(ui2, result) {
255
981
  ui2.glitch("Harness is broken.", result.harnessProblems);
256
982
  if (result.missingFiles.length > 0)
257
983
  ui2.glitch("Baseline files are missing.", result.missingFiles);
258
- if (result.missingDiscovery.length > 0) {
984
+ if (result.missingDiscovery.length > 0)
259
985
  ui2.glitch(ui2.lore.discoveryIncomplete, ["", "Missing:", ...result.missingDiscovery.map((marker) => ` ${marker}`), "", "Run: claude \u2192 /construct-discover"]);
260
- }
986
+ if (result.warnings.length > 0)
987
+ ui2.glitch(ui2.lore.typecheckCaveat, result.warnings);
261
988
  if (result.modifiedFiles.length > 0)
262
989
  ui2.line(ui2.theme.dim(` ${result.modifiedFiles.length} baseline files modified since init (expected once the project evolves).`));
263
- if (result.ok) {
990
+ if (result.ok)
264
991
  ui2.ok(ui2.lore.stable);
265
- return 0;
266
- }
267
- return 1;
992
+ printProvenance(ui2, result.provenance);
993
+ printChecks(ui2, result.checks);
994
+ printWeakestLink(ui2, result.weakestLink);
995
+ return result.ok ? 0 : 1;
996
+ }
997
+
998
+ // src/commands/doctor/index.ts
999
+ function runDoctor(root) {
1000
+ const manifest = readManifest(root);
1001
+ if (manifest == null)
1002
+ return null;
1003
+ const evidence = gatherEvidence(root, manifest);
1004
+ const baseline = baselineVerdict(root, manifest);
1005
+ const problems = harnessProblems(root, manifest, evidence.harness);
1006
+ const checks = [
1007
+ lintPolicyCheck(evidence),
1008
+ constructTestsCheck(evidence),
1009
+ ciCheck(evidence),
1010
+ hookCheck(evidence),
1011
+ redGateCheck(evidence)
1012
+ ];
1013
+ return {
1014
+ ok: baseline.missingFiles.length === 0 && problems.length === 0,
1015
+ missingFiles: baseline.missingFiles,
1016
+ modifiedFiles: baseline.modifiedFiles,
1017
+ missingDiscovery: missingDiscovery(root, manifest),
1018
+ provenance: discoveryProvenance(root, manifest),
1019
+ harnessProblems: problems,
1020
+ warnings: typecheckWarnings(manifest.preset, evidence),
1021
+ checks,
1022
+ weakestLink: weakestLink(checks)
1023
+ };
268
1024
  }
269
1025
 
270
1026
  // src/commands/init.ts
271
1027
  import { mkdirSync as mkdirSync2 } from "fs";
272
- import path13 from "path";
1028
+ import path20 from "path";
273
1029
 
274
1030
  // src/detect/index.ts
275
- import { existsSync as existsSync8 } from "fs";
276
- import path8 from "path";
277
- import process from "process";
1031
+ import { existsSync as existsSync14 } from "fs";
1032
+ import path15 from "path";
1033
+ import process3 from "process";
278
1034
 
279
1035
  // src/detect/existing.ts
280
- import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
281
- import path4 from "path";
1036
+ import { existsSync as existsSync10, readdirSync as readdirSync5 } from "fs";
1037
+ import path11 from "path";
282
1038
  var ESLINT_CONFIGS = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", ".eslintrc", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc.yml"];
283
1039
  var COMPOSITION_CANDIDATES = ["architecture/composition", "docs/architecture/composition", "docs/composition", "composition"];
284
1040
  var OPENAPI_CANDIDATES = ["contracts/api/openapi.yaml", "contracts/api/openapi.yml", "contracts/openapi.yaml", "openapi.yaml", "openapi.yml", "openapi.json", "api/openapi.yaml", "docs/openapi.yaml"];
285
1041
  function anyExists(dir, candidates) {
286
- return candidates.some((candidate) => existsSync4(path4.join(dir, candidate)));
1042
+ return candidates.some((candidate) => existsSync10(path11.join(dir, candidate)));
287
1043
  }
288
1044
  function firstExisting(dir, candidates) {
289
- return candidates.find((candidate) => existsSync4(path4.join(dir, candidate))) ?? null;
1045
+ return candidates.find((candidate) => existsSync10(path11.join(dir, candidate))) ?? null;
290
1046
  }
291
1047
  function compositionDir(dir) {
292
1048
  return COMPOSITION_CANDIDATES.find((candidate) => {
293
- const absolute = path4.join(dir, candidate);
294
- return existsSync4(absolute) && readdirSync3(absolute).some((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
1049
+ const absolute = path11.join(dir, candidate);
1050
+ return existsSync10(absolute) && readdirSync5(absolute).some((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
295
1051
  }) ?? null;
296
1052
  }
297
1053
  function hasWorkflows(dir) {
298
- const workflows = path4.join(dir, ".github", "workflows");
299
- return existsSync4(workflows) && readdirSync3(workflows).some((file) => file.endsWith(".yml") || file.endsWith(".yaml"));
1054
+ const workflows = path11.join(dir, ".github", "workflows");
1055
+ return existsSync10(workflows) && readdirSync5(workflows).some((file) => file.endsWith(".yml") || file.endsWith(".yaml"));
300
1056
  }
301
1057
  function detectExisting(dir) {
302
1058
  return {
303
- packageJson: existsSync4(path4.join(dir, "package.json")),
304
- tsconfig: existsSync4(path4.join(dir, "tsconfig.json")),
1059
+ packageJson: existsSync10(path11.join(dir, "package.json")),
1060
+ tsconfig: existsSync10(path11.join(dir, "tsconfig.json")),
305
1061
  eslintConfig: anyExists(dir, ESLINT_CONFIGS),
306
1062
  githubWorkflows: hasWorkflows(dir),
307
- claudeMd: existsSync4(path4.join(dir, "CLAUDE.md")),
308
- agentsMd: existsSync4(path4.join(dir, "AGENTS.md")),
309
- cursorRules: existsSync4(path4.join(dir, ".cursor", "rules")),
1063
+ claudeMd: existsSync10(path11.join(dir, "CLAUDE.md")),
1064
+ agentsMd: existsSync10(path11.join(dir, "AGENTS.md")),
1065
+ cursorRules: existsSync10(path11.join(dir, ".cursor", "rules")),
310
1066
  openapi: firstExisting(dir, OPENAPI_CANDIDATES),
311
1067
  compositionDir: compositionDir(dir),
312
- constructJson: existsSync4(path4.join(dir, "construct.json"))
1068
+ constructJson: existsSync10(path11.join(dir, "construct.json"))
313
1069
  };
314
1070
  }
315
1071
 
316
1072
  // src/detect/layout.ts
317
- import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
318
- import path6 from "path";
1073
+ import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
1074
+ import path13 from "path";
319
1075
 
320
1076
  // src/detect/workspaces.ts
321
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
322
- import path5 from "path";
1077
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
1078
+ import path12 from "path";
323
1079
  var TOP_LEVEL_PACKAGES_KEY = /^packages:(.*)$/m;
324
1080
  var EMPTY_FLOW_SEQUENCE = /^\[\s*\]$/;
325
1081
  var BLOCK_SEQUENCE_ENTRY = /^[ \t]+-[ \t]*\S/;
326
1082
  function readIfPresent(file) {
327
- if (!existsSync5(file))
1083
+ if (!existsSync11(file))
328
1084
  return null;
329
1085
  try {
330
- return readFileSync4(file, "utf8");
1086
+ return readFileSync11(file, "utf8");
331
1087
  } catch {
332
1088
  return null;
333
1089
  }
@@ -341,7 +1097,7 @@ function startsBlockSequence(rest) {
341
1097
  return false;
342
1098
  }
343
1099
  function declaresPnpmPackages(dir) {
344
- const content = readIfPresent(path5.join(dir, "pnpm-workspace.yaml"));
1100
+ const content = readIfPresent(path12.join(dir, "pnpm-workspace.yaml"));
345
1101
  if (content == null)
346
1102
  return false;
347
1103
  const match = TOP_LEVEL_PACKAGES_KEY.exec(content);
@@ -355,7 +1111,7 @@ function declaresPnpmPackages(dir) {
355
1111
  return startsBlockSequence(content.slice(match.index + match[0].length));
356
1112
  }
357
1113
  function declaresNpmWorkspaces(dir) {
358
- const content = readIfPresent(path5.join(dir, "package.json"));
1114
+ const content = readIfPresent(path12.join(dir, "package.json"));
359
1115
  if (content == null)
360
1116
  return false;
361
1117
  let workspaces;
@@ -373,9 +1129,9 @@ function declaresNpmWorkspaces(dir) {
373
1129
  // src/detect/layout.ts
374
1130
  var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
375
1131
  function isEmptyDir(dir) {
376
- if (!existsSync6(dir))
1132
+ if (!existsSync12(dir))
377
1133
  return true;
378
- return readdirSync4(dir).every((entry) => IGNORED_ENTRIES.has(entry));
1134
+ return readdirSync6(dir).every((entry) => IGNORED_ENTRIES.has(entry));
379
1135
  }
380
1136
  function detectMonorepoTools(dir) {
381
1137
  const tools = [];
@@ -383,40 +1139,40 @@ function detectMonorepoTools(dir) {
383
1139
  tools.push("pnpm-workspace");
384
1140
  if (declaresNpmWorkspaces(dir))
385
1141
  tools.push("npm-workspaces");
386
- if (existsSync6(path6.join(dir, "turbo.json")))
1142
+ if (existsSync12(path13.join(dir, "turbo.json")))
387
1143
  tools.push("turbo");
388
- if (existsSync6(path6.join(dir, "nx.json")))
1144
+ if (existsSync12(path13.join(dir, "nx.json")))
389
1145
  tools.push("nx");
390
1146
  return tools;
391
1147
  }
392
1148
  function detectWorkspaceDirs(dir) {
393
- return ["apps", "packages", "libs", "services"].filter((name) => existsSync6(path6.join(dir, name)) && statSync2(path6.join(dir, name)).isDirectory());
1149
+ return ["apps", "packages", "libs", "services"].filter((name) => existsSync12(path13.join(dir, name)) && statSync3(path13.join(dir, name)).isDirectory());
394
1150
  }
395
1151
  function packageName(dir) {
396
1152
  try {
397
- const parsed = JSON.parse(readFileSync5(path6.join(dir, "package.json"), "utf8"));
1153
+ const parsed = JSON.parse(readFileSync12(path13.join(dir, "package.json"), "utf8"));
398
1154
  return typeof parsed.name === "string" && parsed.name !== "" ? parsed.name : null;
399
1155
  } catch {
400
1156
  return null;
401
1157
  }
402
1158
  }
403
1159
  function detectWorkspacePackages(root, workspaceDirs) {
404
- return workspaceDirs.flatMap((parent) => readdirSync4(path6.join(root, parent)).sort().map((entry) => `${parent}/${entry}`).filter((dir) => existsSync6(path6.join(root, dir, "package.json"))).map((dir) => ({ dir, name: packageName(path6.join(root, dir)) ?? dir.split("/").at(-1) ?? dir })));
1160
+ return workspaceDirs.flatMap((parent) => readdirSync6(path13.join(root, parent)).sort().map((entry) => `${parent}/${entry}`).filter((dir) => existsSync12(path13.join(root, dir, "package.json"))).map((dir) => ({ dir, name: packageName(path13.join(root, dir)) ?? dir.split("/").at(-1) ?? dir })));
405
1161
  }
406
1162
  function detectLayout(dir, monorepoTools, workspaceDirs, hasSrc) {
407
1163
  if (isEmptyDir(dir))
408
1164
  return "empty";
409
1165
  if (monorepoTools.length > 0 || workspaceDirs.length > 0)
410
1166
  return "monorepo";
411
- if (hasSrc || existsSync6(path6.join(dir, "package.json")))
1167
+ if (hasSrc || existsSync12(path13.join(dir, "package.json")))
412
1168
  return "single";
413
1169
  return "unknown";
414
1170
  }
415
1171
 
416
1172
  // src/detect/package-manager.ts
417
1173
  import { execFileSync } from "child_process";
418
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
419
- import path7 from "path";
1174
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
1175
+ import path14 from "path";
420
1176
  var LOCKFILES = [
421
1177
  ["pnpm-lock.yaml", "pnpm"],
422
1178
  ["bun.lockb", "bun"],
@@ -425,11 +1181,11 @@ var LOCKFILES = [
425
1181
  ["package-lock.json", "npm"]
426
1182
  ];
427
1183
  function fromPackageManagerField(dir) {
428
- const manifest = path7.join(dir, "package.json");
429
- if (!existsSync7(manifest))
1184
+ const manifest = path14.join(dir, "package.json");
1185
+ if (!existsSync13(manifest))
430
1186
  return null;
431
1187
  try {
432
- const parsed = JSON.parse(readFileSync6(manifest, "utf8"));
1188
+ const parsed = JSON.parse(readFileSync13(manifest, "utf8"));
433
1189
  const name = parsed.packageManager?.split("@")[0];
434
1190
  return name === "pnpm" || name === "npm" || name === "yarn" || name === "bun" ? name : null;
435
1191
  } catch {
@@ -441,10 +1197,10 @@ function detectPackageManager(dir) {
441
1197
  if (declared != null)
442
1198
  return declared;
443
1199
  for (const [lockfile, manager] of LOCKFILES) {
444
- if (existsSync7(path7.join(dir, lockfile)))
1200
+ if (existsSync13(path14.join(dir, lockfile)))
445
1201
  return manager;
446
1202
  }
447
- return existsSync7(path7.join(dir, "package.json")) ? "npm" : "none";
1203
+ return existsSync13(path14.join(dir, "package.json")) ? "npm" : "none";
448
1204
  }
449
1205
  var pnpmVersionCache;
450
1206
  function detectPnpmVersion() {
@@ -460,10 +1216,10 @@ function detectPnpmVersion() {
460
1216
 
461
1217
  // src/detect/index.ts
462
1218
  function detect(dir) {
463
- const root = path8.resolve(dir);
1219
+ const root = path15.resolve(dir);
464
1220
  const monorepoTools = detectMonorepoTools(root);
465
1221
  const workspaceDirs = detectWorkspaceDirs(root);
466
- const hasSrc = existsSync8(path8.join(root, "src"));
1222
+ const hasSrc = existsSync14(path15.join(root, "src"));
467
1223
  return {
468
1224
  dir: root,
469
1225
  packageManager: detectPackageManager(root),
@@ -473,21 +1229,21 @@ function detect(dir) {
473
1229
  workspaceDirs,
474
1230
  workspacePackages: detectWorkspacePackages(root, workspaceDirs),
475
1231
  hasSrc,
476
- nodeMajor: Number(process.versions.node.split(".")[0]),
1232
+ nodeMajor: Number(process3.versions.node.split(".")[0]),
477
1233
  existing: detectExisting(root)
478
1234
  };
479
1235
  }
480
1236
 
481
1237
  // src/materialize/apply.ts
482
1238
  import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
483
- import path9 from "path";
1239
+ import path16 from "path";
484
1240
  function applyPlan(root, ops) {
485
1241
  const written = [];
486
1242
  for (const op of ops) {
487
1243
  if (op.action === "skip")
488
1244
  continue;
489
- const absolute = path9.join(root, op.target);
490
- mkdirSync(path9.dirname(absolute), { recursive: true });
1245
+ const absolute = path16.join(root, op.target);
1246
+ mkdirSync(path16.dirname(absolute), { recursive: true });
491
1247
  writeFileSync2(absolute, op.content);
492
1248
  written.push(op);
493
1249
  }
@@ -495,8 +1251,8 @@ function applyPlan(root, ops) {
495
1251
  }
496
1252
 
497
1253
  // src/materialize/plan.ts
498
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
499
- import path11 from "path";
1254
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
1255
+ import path18 from "path";
500
1256
 
501
1257
  // src/materialize/rules.ts
502
1258
  var CLAUDE_RULES_DIR = ".claude/rules/";
@@ -658,43 +1414,43 @@ ${end}
658
1414
  }
659
1415
 
660
1416
  // src/materialize/templates.ts
661
- import { existsSync as existsSync9, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
662
- import path10 from "path";
1417
+ import { existsSync as existsSync15, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
1418
+ import path17 from "path";
663
1419
  import { fileURLToPath } from "url";
664
- var HERE = path10.dirname(fileURLToPath(import.meta.url));
1420
+ var HERE = path17.dirname(fileURLToPath(import.meta.url));
665
1421
  function templatesRoot() {
666
- const candidates = [path10.resolve(HERE, "../templates"), path10.resolve(HERE, "../../templates")];
667
- const found = candidates.find((candidate) => existsSync9(candidate));
1422
+ const candidates = [path17.resolve(HERE, "../templates"), path17.resolve(HERE, "../../templates")];
1423
+ const found = candidates.find((candidate) => existsSync15(candidate));
668
1424
  if (found == null)
669
1425
  throw new Error(`templates directory not found next to ${HERE}`);
670
1426
  return found;
671
1427
  }
672
1428
  var EXISTING_SUFFIX = ".existing.eta";
673
1429
  function toTargetPath(relative) {
674
- const segments = relative.split(path10.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
1430
+ const segments = relative.split(path17.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
675
1431
  const joined = segments.join("/");
676
1432
  if (joined.endsWith(EXISTING_SUFFIX))
677
1433
  return { target: joined.slice(0, -EXISTING_SUFFIX.length), rendered: true, variant: "existing" };
678
1434
  return joined.endsWith(".eta") ? { target: joined.slice(0, -".eta".length), rendered: true, variant: "default" } : { target: joined, rendered: false, variant: "default" };
679
1435
  }
680
1436
  function walk(root, current, files) {
681
- for (const entry of readdirSync5(current).sort()) {
682
- const absolute = path10.join(current, entry);
683
- if (statSync3(absolute).isDirectory())
1437
+ for (const entry of readdirSync7(current).sort()) {
1438
+ const absolute = path17.join(current, entry);
1439
+ if (statSync4(absolute).isDirectory())
684
1440
  walk(root, absolute, files);
685
1441
  else if (entry !== ".DS_Store")
686
- files.push(path10.relative(root, absolute));
1442
+ files.push(path17.relative(root, absolute));
687
1443
  }
688
1444
  }
689
1445
  function listTemplateFiles(group) {
690
- const root = path10.join(templatesRoot(), group);
691
- if (!existsSync9(root))
1446
+ const root = path17.join(templatesRoot(), group);
1447
+ if (!existsSync15(root))
692
1448
  throw new Error(`template group "${group}" does not exist`);
693
1449
  const files = [];
694
1450
  walk(root, root, files);
695
1451
  return files.map((relative) => {
696
1452
  const { target, rendered, variant } = toTargetPath(relative);
697
- return { group, source: path10.join(root, relative), target, rendered, variant };
1453
+ return { group, source: path17.join(root, relative), target, rendered, variant };
698
1454
  });
699
1455
  }
700
1456
  var BLOCK = /^[ \t]*\{\{#(if|unless) (\w+)\}\}[ \t]*\n([\s\S]*?)^[ \t]*\{\{\/\1\}\}[ \t]*\n/gm;
@@ -721,7 +1477,7 @@ function mountTarget(mount, target) {
721
1477
  return mount.into == null || mount.into === "." ? target : `${mount.into.replace(/\/$/, "")}/${target}`;
722
1478
  }
723
1479
  function readTemplate(source, rendered, vars) {
724
- const raw = readFileSync7(source, "utf8");
1480
+ const raw = readFileSync14(source, "utf8");
725
1481
  return rendered ? render(raw, vars) : raw;
726
1482
  }
727
1483
  var SORTED_SECTIONS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
@@ -779,12 +1535,12 @@ function layerJson(earlier, later) {
779
1535
  var NOT_ADDED_TO_EXISTING_MANIFEST = ["version"];
780
1536
  function planOne(root, target, content, conflicts, existingVariant) {
781
1537
  const strategy = strategyFor(target);
782
- const absolute = path11.join(root, target);
783
- const exists = existsSync10(absolute);
1538
+ const absolute = path18.join(root, target);
1539
+ const exists = existsSync16(absolute);
784
1540
  if (!exists)
785
1541
  return { target, strategy, action: "create", content: strategy === "append-block" ? appendBlock("", content, target) : content };
786
1542
  if (strategy === "merge-json") {
787
- const existing = JSON.parse(readFileSync7(absolute, "utf8"));
1543
+ const existing = JSON.parse(readFileSync14(absolute, "utf8"));
788
1544
  const incoming = JSON.parse(content);
789
1545
  for (const key of NOT_ADDED_TO_EXISTING_MANIFEST)
790
1546
  delete incoming[key];
@@ -795,7 +1551,7 @@ function planOne(root, target, content, conflicts, existingVariant) {
795
1551
  ` };
796
1552
  }
797
1553
  if (strategy === "append-block") {
798
- const existing = readFileSync7(absolute, "utf8");
1554
+ const existing = readFileSync14(absolute, "utf8");
799
1555
  return { target, strategy, action: "append", content: appendBlock(existing, existingVariant ?? content, target) };
800
1556
  }
801
1557
  return { target, strategy, action: "skip", content, note: "exists, review manually" };
@@ -869,7 +1625,6 @@ var PRESETS = {
869
1625
  HTTP_CONTRACT,
870
1626
  { group: EXPRESS_APP, onlyWhenEmpty: true },
871
1627
  { group: EXPRESS_REPO, onlyWhenEmpty: true },
872
- { group: "presets/node-backend/sample", onlyWhenEmpty: true },
873
1628
  "presets/node-backend/baseline"
874
1629
  ],
875
1630
  contracts: true,
@@ -1031,14 +1786,14 @@ function createClackPrompter(lore, streams = {}) {
1031
1786
  }
1032
1787
 
1033
1788
  // src/version.ts
1034
- import { readFileSync as readFileSync8 } from "fs";
1035
- import path12 from "path";
1789
+ import { readFileSync as readFileSync15 } from "fs";
1790
+ import path19 from "path";
1036
1791
  import { fileURLToPath as fileURLToPath2 } from "url";
1037
- var HERE2 = path12.dirname(fileURLToPath2(import.meta.url));
1792
+ var HERE2 = path19.dirname(fileURLToPath2(import.meta.url));
1038
1793
  function readVersion() {
1039
1794
  for (const candidate of ["../package.json", "../../package.json"]) {
1040
1795
  try {
1041
- const parsed = JSON.parse(readFileSync8(path12.resolve(HERE2, candidate), "utf8"));
1796
+ const parsed = JSON.parse(readFileSync15(path19.resolve(HERE2, candidate), "utf8"));
1042
1797
  if (parsed.name === "mikoshi-construct" && parsed.version != null)
1043
1798
  return parsed.version;
1044
1799
  } catch {
@@ -1138,7 +1893,7 @@ async function askChoices(ui2, options, report, root, prompter) {
1138
1893
  return { presetId, ai, projectName, review };
1139
1894
  }
1140
1895
  async function runInit(ui2, options, prompter) {
1141
- const root = path13.resolve(options.dir);
1896
+ const root = path20.resolve(options.dir);
1142
1897
  mkdirSync2(root, { recursive: true });
1143
1898
  if (!options.yes && prompter == null) {
1144
1899
  ui2.glitch(ui2.lore.needsTerminal);
@@ -1226,7 +1981,7 @@ async function runInit(ui2, options, prompter) {
1226
1981
  }
1227
1982
 
1228
1983
  // src/ui/console.ts
1229
- import process2 from "process";
1984
+ import process4 from "process";
1230
1985
 
1231
1986
  // src/ui/lore.ts
1232
1987
  var BANNER = String.raw`
@@ -1264,12 +2019,34 @@ var LORE = {
1264
2019
  flatlined: "FLATLINED",
1265
2020
  stable: "CONSTRUCT STABLE",
1266
2021
  discoveryIncomplete: "Discovery incomplete.",
2022
+ provenance: "AUTHORSHIP TRACE",
2023
+ stillConstructAuthored: (count) => `Still the construct's own words: ${count} marker${count === 1 ? "" : "s"} nobody has stood behind yet.`,
2024
+ enforcement: "ENFORCEMENT TRACE",
2025
+ typecheckCaveat: "Typecheck cannot carry this stack alone.",
2026
+ weakestLink: (id, level) => `WEAKEST LINK: ${id} at ${level}`,
2027
+ levelLift: {
2028
+ L0: "wire it to a hook or a workflow step and it climbs",
2029
+ L1: "only a reviewer stands behind it; a hook or a workflow step raises it",
2030
+ L2: "a hook is bypassable with --no-verify; running it in CI too raises it",
2031
+ L3: "L3 is the ceiling doctor can read: branch protection is what makes it blocking, and that lives in the API",
2032
+ L4: "nothing above this"
2033
+ },
2034
+ weakestLinkNone: "WEAKEST LINK: nothing is claimed",
1267
2035
  wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
1268
2036
  wireHarnessSteps: [
1269
2037
  "eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
1270
2038
  "tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
1271
2039
  "package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
1272
- ]
2040
+ ],
2041
+ costUnsupported: (runtime) => `No usage feed: the ${runtime} runtime does not expose per-run token usage.`,
2042
+ costEmpty: "No /implement runs recorded here yet.",
2043
+ costKeyMismatch: (key) => `Runs for this repository were recorded under another path. Looked up: ${key}`,
2044
+ costKeyUnknown: (key) => `Runs may be recorded under another path \u2014 the evidence is not conclusive. Looked up: ${key}`,
2045
+ ledgerCounts: (runs, agents, failures, tokens2) => `Ledger, kept by hand and trusted by nobody: ${runs} runs, ${agents} agents, ${failures} unfinished, ${tokens2} tokens.`,
2046
+ ledgerMalformed: (count) => `${count} ledger line${count === 1 ? "" : "s"} could not be read as a run record.`,
2047
+ ledgerDrift: (entriesWithoutSession, sessionsWithoutEntry, unjoinable) => `Ledger against the traces it claims: ${entriesWithoutSession} entries with no session, ${sessionsWithoutEntry} sessions with no entry, ${unjoinable} entries with no run id.`,
2048
+ ledgerEntryWithoutSession: "logged as a run, no session behind it",
2049
+ ledgerSessionWithoutEntry: "ran, never logged"
1273
2050
  };
1274
2051
  var PLAIN_LORE = {
1275
2052
  subtitle: (version) => `mikoshi-construct v${version}`,
@@ -1299,17 +2076,39 @@ var PLAIN_LORE = {
1299
2076
  flatlined: "ERROR",
1300
2077
  stable: "OK",
1301
2078
  discoveryIncomplete: "Discovery incomplete.",
2079
+ provenance: "Discovery provenance",
2080
+ stillConstructAuthored: (count) => `Unchanged since discovery wrote them: ${count} marker${count === 1 ? "" : "s"} nobody has stood behind yet.`,
2081
+ enforcement: "Enforcement",
2082
+ typecheckCaveat: "Typecheck cannot carry this stack alone.",
2083
+ weakestLink: (id, level) => `Weakest link: ${id} at ${level}`,
2084
+ levelLift: {
2085
+ L0: "wire it to a hook or a workflow step and it climbs",
2086
+ L1: "only a reviewer stands behind it; a hook or a workflow step raises it",
2087
+ L2: "a hook is bypassable with --no-verify; running it in CI too raises it",
2088
+ L3: "L3 is the ceiling doctor can read: branch protection is what makes it blocking, and that lives in the API",
2089
+ L4: "nothing above this"
2090
+ },
2091
+ weakestLinkNone: "Weakest link: nothing is claimed",
1302
2092
  wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
1303
2093
  wireHarnessSteps: [
1304
2094
  "eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
1305
2095
  "tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
1306
2096
  "package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
1307
- ]
2097
+ ],
2098
+ costUnsupported: (runtime) => `The ${runtime} runtime does not expose per-run token usage.`,
2099
+ costEmpty: "No /implement runs recorded here yet.",
2100
+ costKeyMismatch: (key) => `Runs for this repository were recorded under another path. Looked up: ${key}`,
2101
+ costKeyUnknown: (key) => `Runs may be recorded under another path \u2014 the evidence is not conclusive. Looked up: ${key}`,
2102
+ ledgerCounts: (runs, agents, failures, tokens2) => `Ledger (a skill step writes it, nothing enforces it): ${runs} runs, ${agents} agents, ${failures} unfinished, ${tokens2} tokens.`,
2103
+ ledgerMalformed: (count) => `${count} ledger line${count === 1 ? "" : "s"} could not be read as a run record.`,
2104
+ ledgerDrift: (entriesWithoutSession, sessionsWithoutEntry, unjoinable) => `Ledger against the runtime: ${entriesWithoutSession} entries with no session, ${sessionsWithoutEntry} sessions with no entry, ${unjoinable} entries with no run id.`,
2105
+ ledgerEntryWithoutSession: "logged as a run, no session behind it",
2106
+ ledgerSessionWithoutEntry: "ran, never logged"
1308
2107
  };
1309
2108
 
1310
2109
  // src/ui/console.ts
1311
2110
  var stdoutWriter = (text2) => {
1312
- process2.stdout.write(text2);
2111
+ process4.stdout.write(text2);
1313
2112
  };
1314
2113
  function createUi(theme, write = stdoutWriter) {
1315
2114
  const plain = theme.name === "plain";
@@ -1368,21 +2167,21 @@ function createUi(theme, write = stdoutWriter) {
1368
2167
  }
1369
2168
 
1370
2169
  // src/ui/theme.ts
1371
- import process3 from "process";
2170
+ import process5 from "process";
1372
2171
  import pc from "picocolors";
1373
2172
  function rgb(r, g, b) {
1374
2173
  return (text2) => `\x1B[38;2;${r};${g};${b}m${text2}\x1B[39m`;
1375
2174
  }
1376
2175
  var identity = (text2) => text2;
1377
2176
  function supportsColor() {
1378
- if (process3.env.NO_COLOR != null && process3.env.NO_COLOR !== "")
2177
+ if (process5.env.NO_COLOR != null && process5.env.NO_COLOR !== "")
1379
2178
  return false;
1380
- if (process3.env.FORCE_COLOR != null && process3.env.FORCE_COLOR !== "0")
2179
+ if (process5.env.FORCE_COLOR != null && process5.env.FORCE_COLOR !== "0")
1381
2180
  return true;
1382
2181
  return pc.isColorSupported;
1383
2182
  }
1384
2183
  function truecolor() {
1385
- const term = process3.env.COLORTERM ?? "";
2184
+ const term = process5.env.COLORTERM ?? "";
1386
2185
  return term === "truecolor" || term === "24bit";
1387
2186
  }
1388
2187
  var PLAIN = {
@@ -1451,12 +2250,12 @@ var init = defineCommand({
1451
2250
  const console = ui(args);
1452
2251
  console.banner(VERSION, args.johnny);
1453
2252
  try {
1454
- const prompter = isTTY(process4.stdout) && process4.stdin.isTTY === true ? createClackPrompter(console.lore) : void 0;
2253
+ const prompter = isTTY(process6.stdout) && process6.stdin.isTTY === true ? createClackPrompter(console.lore) : void 0;
1455
2254
  const result = await runInit(console, { dir: args.dir, preset: args.preset, ai: args.ai, name: args.name, review: args.review, reviewModel: args.reviewModel, yes: args.yes, dryRun: args.dryRun }, prompter);
1456
- process4.exitCode = result.status === "aborted" ? 1 : 0;
2255
+ process6.exitCode = result.status === "aborted" ? 1 : 0;
1457
2256
  } catch (error) {
1458
2257
  console.flatline(error instanceof Error ? error.message : String(error));
1459
- process4.exitCode = 1;
2258
+ process6.exitCode = 1;
1460
2259
  }
1461
2260
  }
1462
2261
  });
@@ -1469,7 +2268,7 @@ var soulkill = defineCommand({
1469
2268
  run({ args }) {
1470
2269
  const report = detect(args.dir);
1471
2270
  if (args.json) {
1472
- process4.stdout.write(`${JSON.stringify(report, null, 2)}
2271
+ process6.stdout.write(`${JSON.stringify(report, null, 2)}
1473
2272
  `);
1474
2273
  return;
1475
2274
  }
@@ -1489,13 +2288,13 @@ var doctor = defineCommand({
1489
2288
  run({ args }) {
1490
2289
  const result = runDoctor(args.dir);
1491
2290
  if (args.json) {
1492
- process4.stdout.write(`${JSON.stringify(result, null, 2)}
2291
+ process6.stdout.write(`${JSON.stringify(result, null, 2)}
1493
2292
  `);
1494
- process4.exitCode = result?.ok === true ? 0 : 1;
2293
+ process6.exitCode = result?.ok === true ? 0 : 1;
1495
2294
  return;
1496
2295
  }
1497
2296
  const console = ui(args);
1498
- process4.exitCode = printDoctor(console, result);
2297
+ process6.exitCode = printDoctor(console, result);
1499
2298
  }
1500
2299
  });
1501
2300
  var cost = defineCommand({
@@ -1506,16 +2305,14 @@ var cost = defineCommand({
1506
2305
  json: { type: "boolean", description: "Machine-readable report", default: false }
1507
2306
  },
1508
2307
  run({ args }) {
1509
- const runs = collectWorkflowRuns(path14.resolve(args.dir));
2308
+ const report = costReport(path21.resolve(args.dir));
1510
2309
  if (args.json) {
1511
- const selected = runs == null ? null : args.last ? runs.slice(-1) : runs;
1512
- process4.stdout.write(`${JSON.stringify(selected, null, 2)}
2310
+ process6.stdout.write(`${JSON.stringify(costJson(report, args.last), null, 2)}
1513
2311
  `);
1514
- process4.exitCode = runs == null ? 1 : 0;
2312
+ process6.exitCode = COST_EXIT[report.status];
1515
2313
  return;
1516
2314
  }
1517
- const console = ui(args);
1518
- process4.exitCode = printCost(console, runs, args.last);
2315
+ process6.exitCode = printCost(ui(args), report, args.last);
1519
2316
  }
1520
2317
  });
1521
2318
  var main = defineCommand({