mikoshi-construct 0.1.2 → 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 path13 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 };
623
+ }
624
+
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`);
178
672
  }
179
673
 
180
- // src/commands/doctor.ts
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;
195
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);
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,124 +981,198 @@ 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 path12 from "path";
1028
+ import path20 from "path";
273
1029
 
274
1030
  // src/detect/index.ts
275
- import { existsSync as existsSync7 } from "fs";
276
- import path7 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 existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
318
- import path5 from "path";
319
- var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
320
- function isEmptyDir(dir) {
321
- if (!existsSync5(dir))
322
- return true;
323
- return readdirSync4(dir).every((entry) => IGNORED_ENTRIES.has(entry));
1073
+ import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
1074
+ import path13 from "path";
1075
+
1076
+ // src/detect/workspaces.ts
1077
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
1078
+ import path12 from "path";
1079
+ var TOP_LEVEL_PACKAGES_KEY = /^packages:(.*)$/m;
1080
+ var EMPTY_FLOW_SEQUENCE = /^\[\s*\]$/;
1081
+ var BLOCK_SEQUENCE_ENTRY = /^[ \t]+-[ \t]*\S/;
1082
+ function readIfPresent(file) {
1083
+ if (!existsSync11(file))
1084
+ return null;
1085
+ try {
1086
+ return readFileSync11(file, "utf8");
1087
+ } catch {
1088
+ return null;
1089
+ }
324
1090
  }
325
- function hasWorkspacesField(dir) {
326
- const manifest = path5.join(dir, "package.json");
327
- if (!existsSync5(manifest))
1091
+ function startsBlockSequence(rest) {
1092
+ for (const line of rest.split("\n")) {
1093
+ if (line.trim() === "" || line.trimStart().startsWith("#"))
1094
+ continue;
1095
+ return BLOCK_SEQUENCE_ENTRY.test(line);
1096
+ }
1097
+ return false;
1098
+ }
1099
+ function declaresPnpmPackages(dir) {
1100
+ const content = readIfPresent(path12.join(dir, "pnpm-workspace.yaml"));
1101
+ if (content == null)
1102
+ return false;
1103
+ const match = TOP_LEVEL_PACKAGES_KEY.exec(content);
1104
+ if (match == null)
328
1105
  return false;
1106
+ const inline = match[1].trim();
1107
+ if (inline.startsWith("["))
1108
+ return !EMPTY_FLOW_SEQUENCE.test(inline);
1109
+ if (inline !== "")
1110
+ return false;
1111
+ return startsBlockSequence(content.slice(match.index + match[0].length));
1112
+ }
1113
+ function declaresNpmWorkspaces(dir) {
1114
+ const content = readIfPresent(path12.join(dir, "package.json"));
1115
+ if (content == null)
1116
+ return false;
1117
+ let workspaces;
329
1118
  try {
330
- const parsed = JSON.parse(readFileSync4(manifest, "utf8"));
331
- return parsed.workspaces != null;
1119
+ ({ workspaces } = JSON.parse(content));
332
1120
  } catch {
333
1121
  return false;
334
1122
  }
1123
+ if (Array.isArray(workspaces))
1124
+ return workspaces.length > 0;
1125
+ const packages = workspaces?.packages;
1126
+ return Array.isArray(packages) && packages.length > 0;
1127
+ }
1128
+
1129
+ // src/detect/layout.ts
1130
+ var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
1131
+ function isEmptyDir(dir) {
1132
+ if (!existsSync12(dir))
1133
+ return true;
1134
+ return readdirSync6(dir).every((entry) => IGNORED_ENTRIES.has(entry));
335
1135
  }
336
1136
  function detectMonorepoTools(dir) {
337
1137
  const tools = [];
338
- if (existsSync5(path5.join(dir, "pnpm-workspace.yaml")))
1138
+ if (declaresPnpmPackages(dir))
339
1139
  tools.push("pnpm-workspace");
340
- if (hasWorkspacesField(dir))
1140
+ if (declaresNpmWorkspaces(dir))
341
1141
  tools.push("npm-workspaces");
342
- if (existsSync5(path5.join(dir, "turbo.json")))
1142
+ if (existsSync12(path13.join(dir, "turbo.json")))
343
1143
  tools.push("turbo");
344
- if (existsSync5(path5.join(dir, "nx.json")))
1144
+ if (existsSync12(path13.join(dir, "nx.json")))
345
1145
  tools.push("nx");
346
1146
  return tools;
347
1147
  }
348
1148
  function detectWorkspaceDirs(dir) {
349
- return ["apps", "packages", "libs", "services"].filter((name) => existsSync5(path5.join(dir, name)) && statSync2(path5.join(dir, name)).isDirectory());
1149
+ return ["apps", "packages", "libs", "services"].filter((name) => existsSync12(path13.join(dir, name)) && statSync3(path13.join(dir, name)).isDirectory());
350
1150
  }
351
1151
  function packageName(dir) {
352
1152
  try {
353
- const parsed = JSON.parse(readFileSync4(path5.join(dir, "package.json"), "utf8"));
1153
+ const parsed = JSON.parse(readFileSync12(path13.join(dir, "package.json"), "utf8"));
354
1154
  return typeof parsed.name === "string" && parsed.name !== "" ? parsed.name : null;
355
1155
  } catch {
356
1156
  return null;
357
1157
  }
358
1158
  }
359
1159
  function detectWorkspacePackages(root, workspaceDirs) {
360
- return workspaceDirs.flatMap((parent) => readdirSync4(path5.join(root, parent)).sort().map((entry) => `${parent}/${entry}`).filter((dir) => existsSync5(path5.join(root, dir, "package.json"))).map((dir) => ({ dir, name: packageName(path5.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 })));
361
1161
  }
362
1162
  function detectLayout(dir, monorepoTools, workspaceDirs, hasSrc) {
363
1163
  if (isEmptyDir(dir))
364
1164
  return "empty";
365
1165
  if (monorepoTools.length > 0 || workspaceDirs.length > 0)
366
1166
  return "monorepo";
367
- if (hasSrc || existsSync5(path5.join(dir, "package.json")))
1167
+ if (hasSrc || existsSync12(path13.join(dir, "package.json")))
368
1168
  return "single";
369
1169
  return "unknown";
370
1170
  }
371
1171
 
372
1172
  // src/detect/package-manager.ts
373
1173
  import { execFileSync } from "child_process";
374
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
375
- import path6 from "path";
1174
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
1175
+ import path14 from "path";
376
1176
  var LOCKFILES = [
377
1177
  ["pnpm-lock.yaml", "pnpm"],
378
1178
  ["bun.lockb", "bun"],
@@ -381,11 +1181,11 @@ var LOCKFILES = [
381
1181
  ["package-lock.json", "npm"]
382
1182
  ];
383
1183
  function fromPackageManagerField(dir) {
384
- const manifest = path6.join(dir, "package.json");
385
- if (!existsSync6(manifest))
1184
+ const manifest = path14.join(dir, "package.json");
1185
+ if (!existsSync13(manifest))
386
1186
  return null;
387
1187
  try {
388
- const parsed = JSON.parse(readFileSync5(manifest, "utf8"));
1188
+ const parsed = JSON.parse(readFileSync13(manifest, "utf8"));
389
1189
  const name = parsed.packageManager?.split("@")[0];
390
1190
  return name === "pnpm" || name === "npm" || name === "yarn" || name === "bun" ? name : null;
391
1191
  } catch {
@@ -397,10 +1197,10 @@ function detectPackageManager(dir) {
397
1197
  if (declared != null)
398
1198
  return declared;
399
1199
  for (const [lockfile, manager] of LOCKFILES) {
400
- if (existsSync6(path6.join(dir, lockfile)))
1200
+ if (existsSync13(path14.join(dir, lockfile)))
401
1201
  return manager;
402
1202
  }
403
- return existsSync6(path6.join(dir, "package.json")) ? "npm" : "none";
1203
+ return existsSync13(path14.join(dir, "package.json")) ? "npm" : "none";
404
1204
  }
405
1205
  var pnpmVersionCache;
406
1206
  function detectPnpmVersion() {
@@ -416,10 +1216,10 @@ function detectPnpmVersion() {
416
1216
 
417
1217
  // src/detect/index.ts
418
1218
  function detect(dir) {
419
- const root = path7.resolve(dir);
1219
+ const root = path15.resolve(dir);
420
1220
  const monorepoTools = detectMonorepoTools(root);
421
1221
  const workspaceDirs = detectWorkspaceDirs(root);
422
- const hasSrc = existsSync7(path7.join(root, "src"));
1222
+ const hasSrc = existsSync14(path15.join(root, "src"));
423
1223
  return {
424
1224
  dir: root,
425
1225
  packageManager: detectPackageManager(root),
@@ -429,21 +1229,21 @@ function detect(dir) {
429
1229
  workspaceDirs,
430
1230
  workspacePackages: detectWorkspacePackages(root, workspaceDirs),
431
1231
  hasSrc,
432
- nodeMajor: Number(process.versions.node.split(".")[0]),
1232
+ nodeMajor: Number(process3.versions.node.split(".")[0]),
433
1233
  existing: detectExisting(root)
434
1234
  };
435
1235
  }
436
1236
 
437
1237
  // src/materialize/apply.ts
438
1238
  import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
439
- import path8 from "path";
1239
+ import path16 from "path";
440
1240
  function applyPlan(root, ops) {
441
1241
  const written = [];
442
1242
  for (const op of ops) {
443
1243
  if (op.action === "skip")
444
1244
  continue;
445
- const absolute = path8.join(root, op.target);
446
- mkdirSync(path8.dirname(absolute), { recursive: true });
1245
+ const absolute = path16.join(root, op.target);
1246
+ mkdirSync(path16.dirname(absolute), { recursive: true });
447
1247
  writeFileSync2(absolute, op.content);
448
1248
  written.push(op);
449
1249
  }
@@ -451,8 +1251,8 @@ function applyPlan(root, ops) {
451
1251
  }
452
1252
 
453
1253
  // src/materialize/plan.ts
454
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
455
- import path10 from "path";
1254
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
1255
+ import path18 from "path";
456
1256
 
457
1257
  // src/materialize/rules.ts
458
1258
  var CLAUDE_RULES_DIR = ".claude/rules/";
@@ -614,43 +1414,43 @@ ${end}
614
1414
  }
615
1415
 
616
1416
  // src/materialize/templates.ts
617
- import { existsSync as existsSync8, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
618
- import path9 from "path";
1417
+ import { existsSync as existsSync15, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
1418
+ import path17 from "path";
619
1419
  import { fileURLToPath } from "url";
620
- var HERE = path9.dirname(fileURLToPath(import.meta.url));
1420
+ var HERE = path17.dirname(fileURLToPath(import.meta.url));
621
1421
  function templatesRoot() {
622
- const candidates = [path9.resolve(HERE, "../templates"), path9.resolve(HERE, "../../templates")];
623
- const found = candidates.find((candidate) => existsSync8(candidate));
1422
+ const candidates = [path17.resolve(HERE, "../templates"), path17.resolve(HERE, "../../templates")];
1423
+ const found = candidates.find((candidate) => existsSync15(candidate));
624
1424
  if (found == null)
625
1425
  throw new Error(`templates directory not found next to ${HERE}`);
626
1426
  return found;
627
1427
  }
628
1428
  var EXISTING_SUFFIX = ".existing.eta";
629
1429
  function toTargetPath(relative) {
630
- const segments = relative.split(path9.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
1430
+ const segments = relative.split(path17.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
631
1431
  const joined = segments.join("/");
632
1432
  if (joined.endsWith(EXISTING_SUFFIX))
633
1433
  return { target: joined.slice(0, -EXISTING_SUFFIX.length), rendered: true, variant: "existing" };
634
1434
  return joined.endsWith(".eta") ? { target: joined.slice(0, -".eta".length), rendered: true, variant: "default" } : { target: joined, rendered: false, variant: "default" };
635
1435
  }
636
1436
  function walk(root, current, files) {
637
- for (const entry of readdirSync5(current).sort()) {
638
- const absolute = path9.join(current, entry);
639
- if (statSync3(absolute).isDirectory())
1437
+ for (const entry of readdirSync7(current).sort()) {
1438
+ const absolute = path17.join(current, entry);
1439
+ if (statSync4(absolute).isDirectory())
640
1440
  walk(root, absolute, files);
641
1441
  else if (entry !== ".DS_Store")
642
- files.push(path9.relative(root, absolute));
1442
+ files.push(path17.relative(root, absolute));
643
1443
  }
644
1444
  }
645
1445
  function listTemplateFiles(group) {
646
- const root = path9.join(templatesRoot(), group);
647
- if (!existsSync8(root))
1446
+ const root = path17.join(templatesRoot(), group);
1447
+ if (!existsSync15(root))
648
1448
  throw new Error(`template group "${group}" does not exist`);
649
1449
  const files = [];
650
1450
  walk(root, root, files);
651
1451
  return files.map((relative) => {
652
1452
  const { target, rendered, variant } = toTargetPath(relative);
653
- return { group, source: path9.join(root, relative), target, rendered, variant };
1453
+ return { group, source: path17.join(root, relative), target, rendered, variant };
654
1454
  });
655
1455
  }
656
1456
  var BLOCK = /^[ \t]*\{\{#(if|unless) (\w+)\}\}[ \t]*\n([\s\S]*?)^[ \t]*\{\{\/\1\}\}[ \t]*\n/gm;
@@ -677,7 +1477,7 @@ function mountTarget(mount, target) {
677
1477
  return mount.into == null || mount.into === "." ? target : `${mount.into.replace(/\/$/, "")}/${target}`;
678
1478
  }
679
1479
  function readTemplate(source, rendered, vars) {
680
- const raw = readFileSync6(source, "utf8");
1480
+ const raw = readFileSync14(source, "utf8");
681
1481
  return rendered ? render(raw, vars) : raw;
682
1482
  }
683
1483
  var SORTED_SECTIONS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
@@ -735,12 +1535,12 @@ function layerJson(earlier, later) {
735
1535
  var NOT_ADDED_TO_EXISTING_MANIFEST = ["version"];
736
1536
  function planOne(root, target, content, conflicts, existingVariant) {
737
1537
  const strategy = strategyFor(target);
738
- const absolute = path10.join(root, target);
739
- const exists = existsSync9(absolute);
1538
+ const absolute = path18.join(root, target);
1539
+ const exists = existsSync16(absolute);
740
1540
  if (!exists)
741
1541
  return { target, strategy, action: "create", content: strategy === "append-block" ? appendBlock("", content, target) : content };
742
1542
  if (strategy === "merge-json") {
743
- const existing = JSON.parse(readFileSync6(absolute, "utf8"));
1543
+ const existing = JSON.parse(readFileSync14(absolute, "utf8"));
744
1544
  const incoming = JSON.parse(content);
745
1545
  for (const key of NOT_ADDED_TO_EXISTING_MANIFEST)
746
1546
  delete incoming[key];
@@ -751,7 +1551,7 @@ function planOne(root, target, content, conflicts, existingVariant) {
751
1551
  ` };
752
1552
  }
753
1553
  if (strategy === "append-block") {
754
- const existing = readFileSync6(absolute, "utf8");
1554
+ const existing = readFileSync14(absolute, "utf8");
755
1555
  return { target, strategy, action: "append", content: appendBlock(existing, existingVariant ?? content, target) };
756
1556
  }
757
1557
  return { target, strategy, action: "skip", content, note: "exists, review manually" };
@@ -825,7 +1625,6 @@ var PRESETS = {
825
1625
  HTTP_CONTRACT,
826
1626
  { group: EXPRESS_APP, onlyWhenEmpty: true },
827
1627
  { group: EXPRESS_REPO, onlyWhenEmpty: true },
828
- { group: "presets/node-backend/sample", onlyWhenEmpty: true },
829
1628
  "presets/node-backend/baseline"
830
1629
  ],
831
1630
  contracts: true,
@@ -987,14 +1786,14 @@ function createClackPrompter(lore, streams = {}) {
987
1786
  }
988
1787
 
989
1788
  // src/version.ts
990
- import { readFileSync as readFileSync7 } from "fs";
991
- import path11 from "path";
1789
+ import { readFileSync as readFileSync15 } from "fs";
1790
+ import path19 from "path";
992
1791
  import { fileURLToPath as fileURLToPath2 } from "url";
993
- var HERE2 = path11.dirname(fileURLToPath2(import.meta.url));
1792
+ var HERE2 = path19.dirname(fileURLToPath2(import.meta.url));
994
1793
  function readVersion() {
995
1794
  for (const candidate of ["../package.json", "../../package.json"]) {
996
1795
  try {
997
- const parsed = JSON.parse(readFileSync7(path11.resolve(HERE2, candidate), "utf8"));
1796
+ const parsed = JSON.parse(readFileSync15(path19.resolve(HERE2, candidate), "utf8"));
998
1797
  if (parsed.name === "mikoshi-construct" && parsed.version != null)
999
1798
  return parsed.version;
1000
1799
  } catch {
@@ -1094,7 +1893,7 @@ async function askChoices(ui2, options, report, root, prompter) {
1094
1893
  return { presetId, ai, projectName, review };
1095
1894
  }
1096
1895
  async function runInit(ui2, options, prompter) {
1097
- const root = path12.resolve(options.dir);
1896
+ const root = path20.resolve(options.dir);
1098
1897
  mkdirSync2(root, { recursive: true });
1099
1898
  if (!options.yes && prompter == null) {
1100
1899
  ui2.glitch(ui2.lore.needsTerminal);
@@ -1182,7 +1981,7 @@ async function runInit(ui2, options, prompter) {
1182
1981
  }
1183
1982
 
1184
1983
  // src/ui/console.ts
1185
- import process2 from "process";
1984
+ import process4 from "process";
1186
1985
 
1187
1986
  // src/ui/lore.ts
1188
1987
  var BANNER = String.raw`
@@ -1220,12 +2019,34 @@ var LORE = {
1220
2019
  flatlined: "FLATLINED",
1221
2020
  stable: "CONSTRUCT STABLE",
1222
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",
1223
2035
  wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
1224
2036
  wireHarnessSteps: [
1225
2037
  "eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
1226
2038
  "tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
1227
2039
  "package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
1228
- ]
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"
1229
2050
  };
1230
2051
  var PLAIN_LORE = {
1231
2052
  subtitle: (version) => `mikoshi-construct v${version}`,
@@ -1255,17 +2076,39 @@ var PLAIN_LORE = {
1255
2076
  flatlined: "ERROR",
1256
2077
  stable: "OK",
1257
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",
1258
2092
  wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
1259
2093
  wireHarnessSteps: [
1260
2094
  "eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
1261
2095
  "tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
1262
2096
  "package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
1263
- ]
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"
1264
2107
  };
1265
2108
 
1266
2109
  // src/ui/console.ts
1267
2110
  var stdoutWriter = (text2) => {
1268
- process2.stdout.write(text2);
2111
+ process4.stdout.write(text2);
1269
2112
  };
1270
2113
  function createUi(theme, write = stdoutWriter) {
1271
2114
  const plain = theme.name === "plain";
@@ -1324,21 +2167,21 @@ function createUi(theme, write = stdoutWriter) {
1324
2167
  }
1325
2168
 
1326
2169
  // src/ui/theme.ts
1327
- import process3 from "process";
2170
+ import process5 from "process";
1328
2171
  import pc from "picocolors";
1329
2172
  function rgb(r, g, b) {
1330
2173
  return (text2) => `\x1B[38;2;${r};${g};${b}m${text2}\x1B[39m`;
1331
2174
  }
1332
2175
  var identity = (text2) => text2;
1333
2176
  function supportsColor() {
1334
- if (process3.env.NO_COLOR != null && process3.env.NO_COLOR !== "")
2177
+ if (process5.env.NO_COLOR != null && process5.env.NO_COLOR !== "")
1335
2178
  return false;
1336
- if (process3.env.FORCE_COLOR != null && process3.env.FORCE_COLOR !== "0")
2179
+ if (process5.env.FORCE_COLOR != null && process5.env.FORCE_COLOR !== "0")
1337
2180
  return true;
1338
2181
  return pc.isColorSupported;
1339
2182
  }
1340
2183
  function truecolor() {
1341
- const term = process3.env.COLORTERM ?? "";
2184
+ const term = process5.env.COLORTERM ?? "";
1342
2185
  return term === "truecolor" || term === "24bit";
1343
2186
  }
1344
2187
  var PLAIN = {
@@ -1407,12 +2250,12 @@ var init = defineCommand({
1407
2250
  const console = ui(args);
1408
2251
  console.banner(VERSION, args.johnny);
1409
2252
  try {
1410
- 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;
1411
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);
1412
- process4.exitCode = result.status === "aborted" ? 1 : 0;
2255
+ process6.exitCode = result.status === "aborted" ? 1 : 0;
1413
2256
  } catch (error) {
1414
2257
  console.flatline(error instanceof Error ? error.message : String(error));
1415
- process4.exitCode = 1;
2258
+ process6.exitCode = 1;
1416
2259
  }
1417
2260
  }
1418
2261
  });
@@ -1425,7 +2268,7 @@ var soulkill = defineCommand({
1425
2268
  run({ args }) {
1426
2269
  const report = detect(args.dir);
1427
2270
  if (args.json) {
1428
- process4.stdout.write(`${JSON.stringify(report, null, 2)}
2271
+ process6.stdout.write(`${JSON.stringify(report, null, 2)}
1429
2272
  `);
1430
2273
  return;
1431
2274
  }
@@ -1445,13 +2288,13 @@ var doctor = defineCommand({
1445
2288
  run({ args }) {
1446
2289
  const result = runDoctor(args.dir);
1447
2290
  if (args.json) {
1448
- process4.stdout.write(`${JSON.stringify(result, null, 2)}
2291
+ process6.stdout.write(`${JSON.stringify(result, null, 2)}
1449
2292
  `);
1450
- process4.exitCode = result?.ok === true ? 0 : 1;
2293
+ process6.exitCode = result?.ok === true ? 0 : 1;
1451
2294
  return;
1452
2295
  }
1453
2296
  const console = ui(args);
1454
- process4.exitCode = printDoctor(console, result);
2297
+ process6.exitCode = printDoctor(console, result);
1455
2298
  }
1456
2299
  });
1457
2300
  var cost = defineCommand({
@@ -1462,16 +2305,14 @@ var cost = defineCommand({
1462
2305
  json: { type: "boolean", description: "Machine-readable report", default: false }
1463
2306
  },
1464
2307
  run({ args }) {
1465
- const runs = collectWorkflowRuns(path13.resolve(args.dir));
2308
+ const report = costReport(path21.resolve(args.dir));
1466
2309
  if (args.json) {
1467
- const selected = runs == null ? null : args.last ? runs.slice(-1) : runs;
1468
- process4.stdout.write(`${JSON.stringify(selected, null, 2)}
2310
+ process6.stdout.write(`${JSON.stringify(costJson(report, args.last), null, 2)}
1469
2311
  `);
1470
- process4.exitCode = runs == null ? 1 : 0;
2312
+ process6.exitCode = COST_EXIT[report.status];
1471
2313
  return;
1472
2314
  }
1473
- const console = ui(args);
1474
- process4.exitCode = printCost(console, runs, args.last);
2315
+ process6.exitCode = printCost(ui(args), report, args.last);
1475
2316
  }
1476
2317
  });
1477
2318
  var main = defineCommand({