mikoshi-construct 0.1.3 → 0.3.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/README.md +34 -5
- package/dist/cli.js +2060 -525
- package/package.json +15 -10
- package/templates/ai/claude/_claude/agents/architect.md +7 -11
- package/templates/ai/claude/_claude/agents/harness.md +7 -11
- package/templates/ai/claude/_claude/agents/implementer.md +6 -10
- package/templates/ai/claude/_claude/commands/plan.md +2 -0
- package/templates/ai/claude/_claude/skills/implement/SKILL.md +43 -8
- package/templates/ai/claude/scripts/construct/implement.workflow.mjs +103 -30
- package/templates/ai/cursor/_cursor/rules/construct.mdc +1 -1
- package/templates/ai/shared/_claude/commands/construct-discover.md +25 -4
- package/templates/base/architecture/decisions/README.md +26 -0
- package/templates/base/architecture/principles.md +3 -1
- package/templates/presets/monorepo/baseline/eslint.config.mjs.eta +22 -6
- package/templates/presets/monorepo/sample/scripts/tests/lint/syntax-policy.test.ts.eta +99 -25
- package/templates/presets/node-backend/baseline/eslint.config.mjs +11 -3
- package/templates/presets/node-backend/sample/scripts/tests/lint/syntax-policy.test.ts +73 -18
- package/templates/presets/node-frontend/baseline/eslint.config.mjs +12 -3
- package/templates/presets/node-frontend/sample/scripts/tests/lint/syntax-policy.test.ts +78 -0
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import path22 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
|
|
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,221 @@ function collectRuns(sessionDir) {
|
|
|
80
87
|
});
|
|
81
88
|
}
|
|
82
89
|
function collectWorkflowRuns(cwd, projectsDir = claudeProjectsDir()) {
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
97
|
+
}
|
|
87
98
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
117
|
+
projectsDir;
|
|
118
|
+
runtime = "claude-code";
|
|
119
|
+
readable() {
|
|
120
|
+
return existsSync(this.projectsDir);
|
|
100
121
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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/
|
|
117
|
-
import
|
|
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
|
|
123
|
-
import
|
|
261
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
262
|
+
import path4 from "path";
|
|
263
|
+
|
|
264
|
+
// src/detect/existing.ts
|
|
265
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
|
|
266
|
+
import path3 from "path";
|
|
267
|
+
var ESLINT_CONFIGS = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", ".eslintrc", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc.yml"];
|
|
268
|
+
var DEFAULT_COMPOSITION_DIR = "architecture/composition";
|
|
269
|
+
var COMPOSITION_CANDIDATES = [DEFAULT_COMPOSITION_DIR, "docs/architecture/composition", "docs/composition", "composition"];
|
|
270
|
+
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"];
|
|
271
|
+
function anyExists(dir, candidates) {
|
|
272
|
+
return candidates.some((candidate) => existsSync3(path3.join(dir, candidate)));
|
|
273
|
+
}
|
|
274
|
+
function firstExisting(dir, candidates) {
|
|
275
|
+
return candidates.find((candidate) => existsSync3(path3.join(dir, candidate))) ?? null;
|
|
276
|
+
}
|
|
277
|
+
function compositionDir(dir) {
|
|
278
|
+
return COMPOSITION_CANDIDATES.find((candidate) => {
|
|
279
|
+
const absolute = path3.join(dir, candidate);
|
|
280
|
+
return existsSync3(absolute) && readdirSync2(absolute).some((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
|
|
281
|
+
}) ?? null;
|
|
282
|
+
}
|
|
283
|
+
function hasWorkflows(dir) {
|
|
284
|
+
const workflows = path3.join(dir, ".github", "workflows");
|
|
285
|
+
return existsSync3(workflows) && readdirSync2(workflows).some((file) => file.endsWith(".yml") || file.endsWith(".yaml"));
|
|
286
|
+
}
|
|
287
|
+
function detectExisting(dir) {
|
|
288
|
+
return {
|
|
289
|
+
packageJson: existsSync3(path3.join(dir, "package.json")),
|
|
290
|
+
tsconfig: existsSync3(path3.join(dir, "tsconfig.json")),
|
|
291
|
+
eslintConfig: anyExists(dir, ESLINT_CONFIGS),
|
|
292
|
+
githubWorkflows: hasWorkflows(dir),
|
|
293
|
+
claudeMd: existsSync3(path3.join(dir, "CLAUDE.md")),
|
|
294
|
+
agentsMd: existsSync3(path3.join(dir, "AGENTS.md")),
|
|
295
|
+
cursorRules: existsSync3(path3.join(dir, ".cursor", "rules")),
|
|
296
|
+
openapi: firstExisting(dir, OPENAPI_CANDIDATES),
|
|
297
|
+
compositionDir: compositionDir(dir),
|
|
298
|
+
constructJson: existsSync3(path3.join(dir, "construct.json"))
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/manifest.ts
|
|
124
303
|
var MANIFEST_FILE = "construct.json";
|
|
304
|
+
var MANIFEST_VERSION = 4;
|
|
125
305
|
var DISCOVERY_MARKERS = [
|
|
126
306
|
"product",
|
|
127
307
|
"module-map",
|
|
@@ -137,7 +317,7 @@ var DISCOVERY_MARKERS = [
|
|
|
137
317
|
function sha256(content) {
|
|
138
318
|
return createHash("sha256").update(content).digest("hex");
|
|
139
319
|
}
|
|
140
|
-
function markerFile(marker, compositionDir2
|
|
320
|
+
function markerFile(marker, compositionDir2) {
|
|
141
321
|
switch (marker) {
|
|
142
322
|
case "composition":
|
|
143
323
|
return compositionDir2;
|
|
@@ -148,13 +328,19 @@ function markerFile(marker, compositionDir2 = "architecture/composition") {
|
|
|
148
328
|
}
|
|
149
329
|
}
|
|
150
330
|
function buildManifest(input) {
|
|
151
|
-
const
|
|
331
|
+
const written = {};
|
|
152
332
|
for (const op of input.written)
|
|
153
|
-
|
|
154
|
-
const
|
|
333
|
+
written[op.target] = sha256(op.content);
|
|
334
|
+
const files = { ...input.previous?.files, ...written };
|
|
335
|
+
const markers = Object.fromEntries(DISCOVERY_MARKERS.map((marker) => [marker, {
|
|
336
|
+
file: markerFile(marker, input.vars.compositionDir),
|
|
337
|
+
authoredBy: "unknown",
|
|
338
|
+
sha: null
|
|
339
|
+
}]));
|
|
155
340
|
return {
|
|
156
|
-
|
|
157
|
-
|
|
341
|
+
manifestVersion: MANIFEST_VERSION,
|
|
342
|
+
construct: input.previous?.construct ?? input.version,
|
|
343
|
+
createdAt: input.previous?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
158
344
|
preset: input.preset,
|
|
159
345
|
ai: input.ai,
|
|
160
346
|
review: input.review === "none" ? null : { provider: input.review, model: input.vars.reviewModel },
|
|
@@ -163,21 +349,448 @@ function buildManifest(input) {
|
|
|
163
349
|
contracts: input.contracts ? { path: input.vars.contractPath, types: input.vars.contractTypesOutput } : null,
|
|
164
350
|
vars: input.vars,
|
|
165
351
|
files,
|
|
166
|
-
|
|
352
|
+
variants: { ...input.previous?.variants, ...variantsOf(input.written) },
|
|
353
|
+
discovery: input.previous?.discovery ?? { baseSha: null, filledAt: null, markers },
|
|
354
|
+
sync: input.previous?.sync ?? null
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function variantsOf(written) {
|
|
358
|
+
return Object.fromEntries(written.flatMap((op) => op.variant == null ? [] : [[op.target, op.variant]]));
|
|
359
|
+
}
|
|
360
|
+
function isTemplateVariant(value) {
|
|
361
|
+
return value === "default" || value === "existing";
|
|
362
|
+
}
|
|
363
|
+
function upgradeVariants(raw) {
|
|
364
|
+
const value = raw ?? {};
|
|
365
|
+
return Object.fromEntries(Object.entries(value).flatMap(([target, variant]) => isTemplateVariant(variant) ? [[target, variant]] : []));
|
|
366
|
+
}
|
|
367
|
+
function upgradeMarker(recorded, file) {
|
|
368
|
+
if (typeof recorded === "string")
|
|
369
|
+
return { file: recorded, authoredBy: "unknown", sha: null };
|
|
370
|
+
const value = recorded ?? {};
|
|
371
|
+
return {
|
|
372
|
+
file: typeof value.file === "string" ? value.file : file,
|
|
373
|
+
authoredBy: value.authoredBy === "construct" ? "construct" : "unknown",
|
|
374
|
+
sha: typeof value.sha === "string" ? value.sha : null
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function upgradeSync(raw) {
|
|
378
|
+
const value = raw ?? {};
|
|
379
|
+
if (typeof value.ranAt !== "string" || typeof value.fromVersion !== "string" || typeof value.toVersion !== "string")
|
|
380
|
+
return null;
|
|
381
|
+
return {
|
|
382
|
+
ranAt: value.ranAt,
|
|
383
|
+
fromVersion: value.fromVersion,
|
|
384
|
+
toVersion: value.toVersion,
|
|
385
|
+
files: typeof value.files === "object" && value.files != null ? { ...value.files } : {},
|
|
386
|
+
variants: upgradeVariants(value.variants)
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function upgradeManifest(raw) {
|
|
390
|
+
const manifest = raw;
|
|
391
|
+
const discovery = manifest.discovery ?? {};
|
|
392
|
+
const recorded = discovery.markers ?? discovery;
|
|
393
|
+
const markers = Object.fromEntries(DISCOVERY_MARKERS.map((marker) => [
|
|
394
|
+
marker,
|
|
395
|
+
upgradeMarker(recorded[marker], markerFile(marker, manifest.vars?.compositionDir ?? DEFAULT_COMPOSITION_DIR))
|
|
396
|
+
]));
|
|
397
|
+
return {
|
|
398
|
+
...manifest,
|
|
399
|
+
manifestVersion: MANIFEST_VERSION,
|
|
400
|
+
variants: upgradeVariants(manifest.variants),
|
|
401
|
+
discovery: {
|
|
402
|
+
baseSha: typeof discovery.baseSha === "string" ? discovery.baseSha : null,
|
|
403
|
+
filledAt: typeof discovery.filledAt === "string" ? discovery.filledAt : null,
|
|
404
|
+
markers
|
|
405
|
+
},
|
|
406
|
+
sync: upgradeSync(manifest.sync)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function recordSync(manifest, run) {
|
|
410
|
+
return {
|
|
411
|
+
...manifest,
|
|
412
|
+
sync: {
|
|
413
|
+
ranAt: run.ranAt,
|
|
414
|
+
fromVersion: manifest.construct,
|
|
415
|
+
toVersion: run.toVersion,
|
|
416
|
+
files: { ...manifest.sync?.files, ...run.files },
|
|
417
|
+
variants: { ...manifest.sync?.variants, ...run.variants }
|
|
418
|
+
}
|
|
167
419
|
};
|
|
168
420
|
}
|
|
421
|
+
function recordedShas(manifest) {
|
|
422
|
+
return { ...manifest.files, ...manifest.sync?.files };
|
|
423
|
+
}
|
|
424
|
+
function recordedVariants(manifest) {
|
|
425
|
+
return { ...manifest.variants, ...manifest.sync?.variants };
|
|
426
|
+
}
|
|
169
427
|
function writeManifest(root, manifest) {
|
|
170
|
-
writeFileSync(
|
|
428
|
+
writeFileSync(path4.join(root, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
171
429
|
`);
|
|
172
430
|
}
|
|
173
431
|
function readManifest(root) {
|
|
174
|
-
const file =
|
|
175
|
-
if (!
|
|
432
|
+
const file = path4.join(root, MANIFEST_FILE);
|
|
433
|
+
if (!existsSync4(file))
|
|
176
434
|
return null;
|
|
177
|
-
return JSON.parse(
|
|
435
|
+
return upgradeManifest(JSON.parse(readFileSync3(file, "utf8")));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/commands/cost/runtime.ts
|
|
439
|
+
var CLAUDE_CODE_ENV = ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"];
|
|
440
|
+
var CURSOR_ENV = ["CURSOR_AGENT", "CURSOR_TRACE_ID"];
|
|
441
|
+
function anySet(env, names) {
|
|
442
|
+
return names.some((name) => (env[name] ?? "") !== "");
|
|
443
|
+
}
|
|
444
|
+
function resolveRuntime(root, env = process.env) {
|
|
445
|
+
if (anySet(env, CLAUDE_CODE_ENV))
|
|
446
|
+
return "claude-code";
|
|
447
|
+
if (anySet(env, CURSOR_ENV))
|
|
448
|
+
return "cursor";
|
|
449
|
+
return readManifest(root)?.ai === "cursor" ? "cursor" : "claude-code";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/commands/cost/report.ts
|
|
453
|
+
var COST_EXIT = {
|
|
454
|
+
ok: 0,
|
|
455
|
+
empty: 0,
|
|
456
|
+
mismatch: 1,
|
|
457
|
+
unknown: 1,
|
|
458
|
+
unsupported: 3
|
|
459
|
+
};
|
|
460
|
+
function selectRuns(report, last) {
|
|
461
|
+
const runs = report.runs ?? [];
|
|
462
|
+
return last ? runs.slice(-1) : runs;
|
|
463
|
+
}
|
|
464
|
+
function costJson(report, last) {
|
|
465
|
+
const runs = selectRuns(report, last);
|
|
466
|
+
return {
|
|
467
|
+
status: report.status,
|
|
468
|
+
runtime: report.runtime,
|
|
469
|
+
...report.key == null ? {} : { key: report.key },
|
|
470
|
+
...report.candidates == null || report.candidates.length === 0 ? {} : { candidates: report.candidates },
|
|
471
|
+
...runs.length === 0 ? {} : { runs },
|
|
472
|
+
...report.ledger == null ? {} : { ledger: report.ledger },
|
|
473
|
+
...report.reconciliation == null ? {} : { reconciliation: report.reconciliation }
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function fmt(value) {
|
|
477
|
+
return value.toLocaleString("en-US");
|
|
478
|
+
}
|
|
479
|
+
function tokens(value) {
|
|
480
|
+
return value === "unknown" ? "unknown" : fmt(value);
|
|
481
|
+
}
|
|
482
|
+
function printLedger(ui2, ledger, reconciliation) {
|
|
483
|
+
if (ledger == null && reconciliation == null)
|
|
484
|
+
return;
|
|
485
|
+
if (ledger != null) {
|
|
486
|
+
ui2.line(ui2.theme.dim(ui2.lore.ledgerCounts(ledger.runs, ledger.agents, ledger.failures, tokens(ledger.tokens))));
|
|
487
|
+
if (ledger.malformed.length > 0)
|
|
488
|
+
ui2.glitch(ui2.lore.ledgerMalformed(ledger.malformed.length), ledger.malformed.map((entry) => `line ${entry.line}: ${entry.reason}`));
|
|
489
|
+
}
|
|
490
|
+
if (reconciliation == null)
|
|
491
|
+
return;
|
|
492
|
+
ui2.line(ui2.theme.dim(ui2.lore.ledgerDrift(reconciliation.entriesWithoutSession.length, reconciliation.sessionsWithoutEntry.length, reconciliation.unjoinable)));
|
|
493
|
+
for (const run of reconciliation.entriesWithoutSession)
|
|
494
|
+
ui2.line(ui2.theme.dim(` ${run}: ${ui2.lore.ledgerEntryWithoutSession}`));
|
|
495
|
+
for (const run of reconciliation.sessionsWithoutEntry)
|
|
496
|
+
ui2.line(ui2.theme.dim(` ${run}: ${ui2.lore.ledgerSessionWithoutEntry}`));
|
|
497
|
+
}
|
|
498
|
+
function printRuns(ui2, runs) {
|
|
499
|
+
const grand = emptyUsage();
|
|
500
|
+
for (const run of runs) {
|
|
501
|
+
ui2.line(`${ui2.theme.accent(run.run)} ${ui2.theme.dim(`${run.startedAt} \xB7 ${run.total.models.join(", ")}`)}`);
|
|
502
|
+
for (const agent of run.agents)
|
|
503
|
+
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)}`);
|
|
504
|
+
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`)}`);
|
|
505
|
+
ui2.line();
|
|
506
|
+
add(grand, run.total);
|
|
507
|
+
}
|
|
508
|
+
if (runs.length > 1)
|
|
509
|
+
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})`)}`);
|
|
510
|
+
}
|
|
511
|
+
function printCost(ui2, report, last) {
|
|
512
|
+
const key = report.key ?? "";
|
|
513
|
+
const candidates = report.candidates ?? [];
|
|
514
|
+
switch (report.status) {
|
|
515
|
+
case "unsupported":
|
|
516
|
+
ui2.glitch(ui2.lore.costUnsupported(report.runtime));
|
|
517
|
+
break;
|
|
518
|
+
case "mismatch":
|
|
519
|
+
ui2.glitch(ui2.lore.costKeyMismatch(key), candidates);
|
|
520
|
+
break;
|
|
521
|
+
case "unknown":
|
|
522
|
+
ui2.glitch(ui2.lore.costKeyUnknown(key), candidates);
|
|
523
|
+
break;
|
|
524
|
+
case "empty":
|
|
525
|
+
ui2.glitch(ui2.lore.costEmpty);
|
|
526
|
+
break;
|
|
527
|
+
case "ok":
|
|
528
|
+
printRuns(ui2, selectRuns(report, last));
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
printLedger(ui2, report.ledger, report.reconciliation);
|
|
532
|
+
return COST_EXIT[report.status];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/commands/cost/index.ts
|
|
536
|
+
function costReport(cwd, options = {}) {
|
|
537
|
+
const runtime = resolveRuntime(cwd, options.env ?? process2.env);
|
|
538
|
+
const reading = readLedger(cwd);
|
|
539
|
+
const ledger = summarizeLedger(reading);
|
|
540
|
+
const reported = hasLedgerFindings(ledger);
|
|
541
|
+
const source = runtime === "claude-code" ? new ClaudeCodeCostSource(options.projectsDir) : null;
|
|
542
|
+
if (source == null || !source.readable())
|
|
543
|
+
return { status: "unsupported", runtime, ...reported ? { ledger: withoutTokenTotals(ledger) } : {} };
|
|
544
|
+
const result = source.read(cwd);
|
|
545
|
+
const joinable = result.status === "ok" || result.status === "empty";
|
|
546
|
+
return {
|
|
547
|
+
runtime,
|
|
548
|
+
...result,
|
|
549
|
+
...reported ? { ledger } : {},
|
|
550
|
+
...joinable && (reported || result.runs.length > 0) ? { reconciliation: reconcile(reading.entries, result.runs) } : {}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/version.ts
|
|
555
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
556
|
+
import path5 from "path";
|
|
557
|
+
import { fileURLToPath } from "url";
|
|
558
|
+
var HERE = path5.dirname(fileURLToPath(import.meta.url));
|
|
559
|
+
function readVersion() {
|
|
560
|
+
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
561
|
+
try {
|
|
562
|
+
const parsed = JSON.parse(readFileSync4(path5.resolve(HERE, candidate), "utf8"));
|
|
563
|
+
if (parsed.name === "mikoshi-construct" && parsed.version != null)
|
|
564
|
+
return parsed.version;
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return "0.0.0";
|
|
569
|
+
}
|
|
570
|
+
var VERSION = readVersion();
|
|
571
|
+
|
|
572
|
+
// src/commands/doctor/baseline.ts
|
|
573
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
574
|
+
import path6 from "path";
|
|
575
|
+
function baselineVerdict(root, manifest) {
|
|
576
|
+
const missingFiles = [];
|
|
577
|
+
const modifiedFiles = [];
|
|
578
|
+
for (const [file, hash] of Object.entries(manifest.files)) {
|
|
579
|
+
const absolute = path6.join(root, file);
|
|
580
|
+
if (!existsSync5(absolute))
|
|
581
|
+
missingFiles.push(file);
|
|
582
|
+
else if (sha256(readFileSync5(absolute, "utf8")) !== hash)
|
|
583
|
+
modifiedFiles.push(file);
|
|
584
|
+
}
|
|
585
|
+
return { missingFiles, modifiedFiles };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/commands/doctor/enforcement.ts
|
|
589
|
+
function harnessReach(evidence) {
|
|
590
|
+
const command = `"${evidence.harness.command}"`;
|
|
591
|
+
if (evidence.workflows.harnessWorkflow != null)
|
|
592
|
+
return { level: "L3", evidence: `${evidence.workflows.harnessWorkflow} runs ${command}` };
|
|
593
|
+
if (evidence.hooks.runsHarness && evidence.hooks.manager != null)
|
|
594
|
+
return { level: "L2", evidence: `${evidence.hooks.manager} runs ${command}, and a local hook is bypassable with --no-verify` };
|
|
595
|
+
return { level: "L0", evidence: `no workflow and no hook configuration runs ${command}` };
|
|
596
|
+
}
|
|
597
|
+
function reached(id, state, evidence, detail) {
|
|
598
|
+
const reach = harnessReach(evidence);
|
|
599
|
+
return { id, level: reach.level, state, evidence: `${detail}; ${reach.evidence}` };
|
|
600
|
+
}
|
|
601
|
+
function absent(id, detail) {
|
|
602
|
+
return { id, level: "L0", state: "absent", evidence: detail };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/commands/doctor/checks/ci.ts
|
|
606
|
+
var ID = "ci";
|
|
607
|
+
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";
|
|
608
|
+
function ciCheck(evidence) {
|
|
609
|
+
const workflows = evidence.workflows;
|
|
610
|
+
const reach = harnessReach(evidence);
|
|
611
|
+
if (workflows.harnessWorkflow != null)
|
|
612
|
+
return { id: ID, level: reach.level, state: "present", evidence: `${reach.evidence}; ${SCOPE}` };
|
|
613
|
+
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}"`;
|
|
614
|
+
return { id: ID, level: reach.level, state: "unknown", evidence: `${seen}; ${SCOPE}` };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/commands/doctor/runner.ts
|
|
618
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
619
|
+
import path7 from "path";
|
|
620
|
+
var RUNNER_CONFIG_FILES = [
|
|
621
|
+
"vitest.config.ts",
|
|
622
|
+
"vitest.config.mts",
|
|
623
|
+
"vitest.config.js",
|
|
624
|
+
"vitest.config.mjs",
|
|
625
|
+
"vite.config.ts",
|
|
626
|
+
"vite.config.mts",
|
|
627
|
+
"vite.config.js",
|
|
628
|
+
"vite.config.mjs"
|
|
629
|
+
];
|
|
630
|
+
var STRING_LITERAL = /^(['"])(.*)\1$/;
|
|
631
|
+
function literalEntries(body) {
|
|
632
|
+
const trimmed = body.trim();
|
|
633
|
+
if (trimmed === "")
|
|
634
|
+
return [];
|
|
635
|
+
const entries = trimmed.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
636
|
+
const values = entries.map((entry) => STRING_LITERAL.exec(entry)?.[2]);
|
|
637
|
+
return values.every((value) => value != null) ? values : null;
|
|
638
|
+
}
|
|
639
|
+
function includeGlobs(source) {
|
|
640
|
+
const found = [];
|
|
641
|
+
let literal = false;
|
|
642
|
+
for (const match of source.matchAll(/include\s*:\s*/g)) {
|
|
643
|
+
const rest = source.slice(match.index + match[0].length);
|
|
644
|
+
if (!rest.startsWith("["))
|
|
645
|
+
return null;
|
|
646
|
+
const close = rest.indexOf("]");
|
|
647
|
+
if (close === -1)
|
|
648
|
+
return null;
|
|
649
|
+
const entries = literalEntries(rest.slice(1, close));
|
|
650
|
+
if (entries == null)
|
|
651
|
+
return null;
|
|
652
|
+
literal = true;
|
|
653
|
+
found.push(...entries);
|
|
654
|
+
}
|
|
655
|
+
return literal ? found : null;
|
|
656
|
+
}
|
|
657
|
+
function escapeLiteral(character) {
|
|
658
|
+
return /[.+^${}()|[\]\\]/.test(character) ? `\\${character}` : character;
|
|
659
|
+
}
|
|
660
|
+
function globToRegExp(glob) {
|
|
661
|
+
let pattern = "";
|
|
662
|
+
let braces = 0;
|
|
663
|
+
for (let index = 0; index < glob.length; index += 1) {
|
|
664
|
+
const character = glob[index];
|
|
665
|
+
if (character === "*" && glob[index + 1] === "*" && glob[index + 2] === "/") {
|
|
666
|
+
pattern += "(?:[^/]+/)*";
|
|
667
|
+
index += 2;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (character === "*" && glob[index + 1] === "*") {
|
|
671
|
+
pattern += ".*";
|
|
672
|
+
index += 1;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (character === "*") {
|
|
676
|
+
pattern += "[^/]*";
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (character === "?") {
|
|
680
|
+
pattern += "[^/]";
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (character === "{") {
|
|
684
|
+
braces += 1;
|
|
685
|
+
pattern += "(?:";
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (character === "}" && braces > 0) {
|
|
689
|
+
braces -= 1;
|
|
690
|
+
pattern += ")";
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (character === "," && braces > 0) {
|
|
694
|
+
pattern += "|";
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
pattern += escapeLiteral(character);
|
|
698
|
+
}
|
|
699
|
+
return new RegExp(`^${pattern}$`);
|
|
700
|
+
}
|
|
701
|
+
function matchesAnyGlob(file, globs) {
|
|
702
|
+
const normalized = file.replace(/^\.\//, "");
|
|
703
|
+
return globs.some((glob) => globToRegExp(glob.replace(/^\.\//, "")).test(normalized));
|
|
704
|
+
}
|
|
705
|
+
function readRunnerFacts(root, harnessText) {
|
|
706
|
+
const invokedByHarness = harnessText.includes("vitest");
|
|
707
|
+
const file = RUNNER_CONFIG_FILES.find((candidate) => existsSync6(path7.join(root, candidate))) ?? null;
|
|
708
|
+
if (file == null) {
|
|
709
|
+
return {
|
|
710
|
+
file: null,
|
|
711
|
+
globs: null,
|
|
712
|
+
note: `no runner config file (${RUNNER_CONFIG_FILES[0]} or a sibling) exists, so the include list cannot be read`,
|
|
713
|
+
invokedByHarness
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
let source;
|
|
717
|
+
try {
|
|
718
|
+
source = readFileSync6(path7.join(root, file), "utf8");
|
|
719
|
+
} catch {
|
|
720
|
+
return { file, globs: null, note: `${file} cannot be read, so the include list is unknown`, invokedByHarness };
|
|
721
|
+
}
|
|
722
|
+
const globs = includeGlobs(source);
|
|
723
|
+
if (globs == null)
|
|
724
|
+
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 };
|
|
725
|
+
return { file, globs, note: `${file} includes ${globs.map((glob) => `"${glob}"`).join(", ")}`, invokedByHarness };
|
|
178
726
|
}
|
|
179
727
|
|
|
180
|
-
// src/commands/doctor.ts
|
|
728
|
+
// src/commands/doctor/checks/construct-tests.ts
|
|
729
|
+
var ID2 = "construct-tests";
|
|
730
|
+
function constructTestsCheck(evidence) {
|
|
731
|
+
const recorded = evidence.recordedTests;
|
|
732
|
+
if (recorded.length === 0)
|
|
733
|
+
return absent(ID2, "construct.json records no test file; weakest link: there are no construct tests to run");
|
|
734
|
+
const runner = evidence.runner;
|
|
735
|
+
if (runner.globs == null)
|
|
736
|
+
return reached(ID2, "unknown", evidence, `construct.json records ${recorded.length} test files, but ${runner.note}`);
|
|
737
|
+
const orphan = recorded.find((file) => !matchesAnyGlob(file, runner.globs ?? []));
|
|
738
|
+
if (orphan != null)
|
|
739
|
+
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`);
|
|
740
|
+
if (!runner.invokedByHarness)
|
|
741
|
+
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`);
|
|
742
|
+
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`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/commands/doctor/checks/hook.ts
|
|
746
|
+
var ID3 = "hook";
|
|
747
|
+
function hookCheck(evidence) {
|
|
748
|
+
const hooks = evidence.hooks;
|
|
749
|
+
if (hooks.manager != null) {
|
|
750
|
+
const runs = hooks.runsHarness ? `runs "${evidence.harness.command}"` : `does not run "${evidence.harness.command}"`;
|
|
751
|
+
return { id: ID3, level: "L2", state: "present", evidence: `${hooks.manager} installs a git hook and ${runs}; a local hook is bypassable with --no-verify` };
|
|
752
|
+
}
|
|
753
|
+
if (hooks.script != null)
|
|
754
|
+
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` };
|
|
755
|
+
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" };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// src/commands/doctor/checks/lint-policy.ts
|
|
759
|
+
var ID4 = "lint-policy";
|
|
760
|
+
function lintPolicyCheck(evidence) {
|
|
761
|
+
const policy = evidence.policyTests[0];
|
|
762
|
+
if (policy == null) {
|
|
763
|
+
if (evidence.unreadable.length > 0)
|
|
764
|
+
return reached(ID4, "unknown", evidence, `${evidence.unreadable[0]} cannot be read, so doctor cannot say whether a lint policy check exists`);
|
|
765
|
+
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");
|
|
766
|
+
}
|
|
767
|
+
const runner = evidence.runner;
|
|
768
|
+
if (runner.globs == null)
|
|
769
|
+
return reached(ID4, "unknown", evidence, `${policy} runs ESLint over the lint policy this repository declares, but ${runner.note}`);
|
|
770
|
+
if (!matchesAnyGlob(policy, runner.globs))
|
|
771
|
+
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`);
|
|
772
|
+
if (!runner.invokedByHarness)
|
|
773
|
+
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`);
|
|
774
|
+
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`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/commands/doctor/checks/red-gate.ts
|
|
778
|
+
var ID5 = "red-gate";
|
|
779
|
+
function redGateCheck(evidence) {
|
|
780
|
+
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`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/commands/doctor/discovery.ts
|
|
784
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
|
|
785
|
+
import path8 from "path";
|
|
786
|
+
|
|
787
|
+
// src/detect/facts.ts
|
|
788
|
+
function factsTheRepositoryEstablishes(root) {
|
|
789
|
+
const existing = detectExisting(root);
|
|
790
|
+
return existing.compositionDir == null ? {} : { compositionDir: existing.compositionDir };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/commands/doctor/discovery.ts
|
|
181
794
|
var DISCOVERY_PLACEHOLDER = "_Not discovered yet \u2014 run `/construct-discover`._";
|
|
182
795
|
function markerOpen(marker) {
|
|
183
796
|
return `<!-- construct:discover:${marker} -->`;
|
|
@@ -185,318 +798,279 @@ function markerOpen(marker) {
|
|
|
185
798
|
function markerClose(marker) {
|
|
186
799
|
return `<!-- /construct:discover:${marker} -->`;
|
|
187
800
|
}
|
|
188
|
-
function
|
|
801
|
+
function blockBody(document, marker) {
|
|
189
802
|
const start = document.indexOf(markerOpen(marker));
|
|
190
803
|
const stop = document.indexOf(markerClose(marker));
|
|
191
804
|
if (start === -1 || stop === -1 || stop < start)
|
|
192
|
-
return
|
|
805
|
+
return null;
|
|
193
806
|
const body = document.slice(start + markerOpen(marker).length, stop).trim();
|
|
194
|
-
return body
|
|
807
|
+
return body === "" || body === DISCOVERY_PLACEHOLDER ? null : body;
|
|
195
808
|
}
|
|
809
|
+
function compositionBody(directory) {
|
|
810
|
+
if (!existsSync7(directory))
|
|
811
|
+
return null;
|
|
812
|
+
const models = readdirSync3(directory).filter((file) => file.endsWith(".yaml")).sort();
|
|
813
|
+
if (models.length === 0)
|
|
814
|
+
return null;
|
|
815
|
+
return models.map((model) => `${model}
|
|
816
|
+
${readFileSync7(path8.join(directory, model), "utf8")}`).join("\n");
|
|
817
|
+
}
|
|
818
|
+
function markerBody(root, marker, file) {
|
|
819
|
+
const location = path8.join(root, file);
|
|
820
|
+
if (marker === "composition")
|
|
821
|
+
return compositionBody(location);
|
|
822
|
+
if (!existsSync7(location))
|
|
823
|
+
return null;
|
|
824
|
+
return blockBody(readFileSync7(location, "utf8"), marker);
|
|
825
|
+
}
|
|
826
|
+
function markerFileFor(root, manifest, marker) {
|
|
827
|
+
const recordedFile = manifest.discovery.markers[marker].file;
|
|
828
|
+
if (marker !== "composition")
|
|
829
|
+
return recordedFile;
|
|
830
|
+
const decided = manifest.vars?.compositionDir;
|
|
831
|
+
if (decided != null && decided !== "")
|
|
832
|
+
return decided;
|
|
833
|
+
return factsTheRepositoryEstablishes(root).compositionDir ?? recordedFile;
|
|
834
|
+
}
|
|
835
|
+
function missingDiscovery(root, manifest) {
|
|
836
|
+
return DISCOVERY_MARKERS.filter((marker) => markerBody(root, marker, markerFileFor(root, manifest, marker)) == null);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/commands/doctor/evidence.ts
|
|
840
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
841
|
+
import path12 from "path";
|
|
842
|
+
|
|
843
|
+
// src/commands/doctor/harness.ts
|
|
844
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
845
|
+
import path9 from "path";
|
|
196
846
|
var REQUIRED_QUALITY_STEPS = ["lint", "typecheck", "test"];
|
|
197
|
-
|
|
847
|
+
var SCRIPT_REFERENCE = /(?:^|&&|\|\||;)\s*(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?([\w:.-]+)/g;
|
|
848
|
+
function harnessScriptName(command) {
|
|
849
|
+
return command.replace(/^(pnpm|npm|yarn|bun)\s+(run\s+)?/, "");
|
|
850
|
+
}
|
|
851
|
+
function expandScript(scripts, name, seen) {
|
|
852
|
+
if (seen.has(name))
|
|
853
|
+
return "";
|
|
854
|
+
seen.add(name);
|
|
855
|
+
const body = scripts[name];
|
|
856
|
+
if (body == null)
|
|
857
|
+
return "";
|
|
858
|
+
const referenced = [...body.matchAll(SCRIPT_REFERENCE)].map((match) => match[1]);
|
|
859
|
+
return [body, ...referenced.map((reference) => expandScript(scripts, reference, seen))].join(" && ");
|
|
860
|
+
}
|
|
861
|
+
function readHarnessFacts(root, command) {
|
|
862
|
+
const script = harnessScriptName(command);
|
|
863
|
+
const manifestPath = path9.join(root, "package.json");
|
|
864
|
+
const packageJson = existsSync8(manifestPath) ? JSON.parse(readFileSync8(manifestPath, "utf8")) : null;
|
|
865
|
+
const scripts = packageJson?.scripts ?? {};
|
|
866
|
+
const body = scripts[script] ?? null;
|
|
867
|
+
return {
|
|
868
|
+
command,
|
|
869
|
+
script,
|
|
870
|
+
scripts,
|
|
871
|
+
body,
|
|
872
|
+
resolved: expandScript(scripts, script, /* @__PURE__ */ new Set()),
|
|
873
|
+
commandForms: [command, `pnpm run ${script}`, `pnpm ${script}`, `npm run ${script}`, `yarn ${script}`],
|
|
874
|
+
packageJson
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
function runsHarnessCommand(text2, forms) {
|
|
878
|
+
return forms.some((form) => text2.includes(form));
|
|
879
|
+
}
|
|
880
|
+
function contractProblems(root, contracts, script, body) {
|
|
198
881
|
if (contracts == null)
|
|
199
882
|
return [];
|
|
200
|
-
const problems = [contracts.path, contracts.types].filter((file) => !
|
|
201
|
-
if (!
|
|
202
|
-
problems.push(`"${
|
|
883
|
+
const problems = [contracts.path, contracts.types].filter((file) => !existsSync8(path9.join(root, file))).map((file) => `${file} is missing (construct.json \u2192 contracts)`);
|
|
884
|
+
if (!body.includes("contracts:check"))
|
|
885
|
+
problems.push(`"${script}" does not run contracts:check`);
|
|
203
886
|
return problems;
|
|
204
887
|
}
|
|
205
|
-
function harnessProblems(root, manifest) {
|
|
206
|
-
|
|
207
|
-
const manifestPath = path3.join(root, "package.json");
|
|
208
|
-
if (!existsSync3(manifestPath))
|
|
888
|
+
function harnessProblems(root, manifest, facts) {
|
|
889
|
+
if (facts.packageJson == null)
|
|
209
890
|
return ["package.json is missing"];
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const quality = scripts[scriptName];
|
|
214
|
-
if (quality == null)
|
|
215
|
-
return [`package.json has no "${scriptName}" script (harness command is "${command}")`];
|
|
891
|
+
const body = facts.body;
|
|
892
|
+
if (body == null)
|
|
893
|
+
return [`package.json has no "${facts.script}" script (harness command is "${facts.command}")`];
|
|
216
894
|
return [
|
|
217
|
-
...REQUIRED_QUALITY_STEPS.filter((step) => !
|
|
218
|
-
...contractProblems(root, manifest.contracts,
|
|
895
|
+
...REQUIRED_QUALITY_STEPS.filter((step) => !body.includes(step)).map((step) => `"${facts.script}" does not run ${step}`),
|
|
896
|
+
...contractProblems(root, manifest.contracts, facts.script, body)
|
|
219
897
|
];
|
|
220
898
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
899
|
+
|
|
900
|
+
// src/commands/doctor/hooks.ts
|
|
901
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync2 } from "fs";
|
|
902
|
+
import path10 from "path";
|
|
903
|
+
var LEFTHOOK_FILES = ["lefthook.yml", "lefthook.yaml", "lefthook.toml", "lefthook.json", ".lefthook.yml", ".lefthook.yaml"];
|
|
904
|
+
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"];
|
|
905
|
+
var HUSKY_DIR = ".husky";
|
|
906
|
+
var GIT_CONFIG = ".git/config";
|
|
907
|
+
var HOOK_SCRIPTS = ["precommit", "pre-commit", "prepush", "pre-push"];
|
|
908
|
+
function read(root, file) {
|
|
909
|
+
try {
|
|
910
|
+
return readFileSync9(path10.join(root, file), "utf8");
|
|
911
|
+
} catch {
|
|
224
912
|
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
913
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return
|
|
914
|
+
}
|
|
915
|
+
function huskyHooks(root) {
|
|
916
|
+
const directory = path10.join(root, HUSKY_DIR);
|
|
917
|
+
if (!existsSync9(directory) || !statSync2(directory).isDirectory())
|
|
918
|
+
return [];
|
|
919
|
+
return readdirSync4(directory).filter((entry) => !entry.startsWith("_") && !entry.startsWith(".")).sort().map((entry) => `${HUSKY_DIR}/${entry}`);
|
|
920
|
+
}
|
|
921
|
+
function managerFiles(root, packageJson) {
|
|
922
|
+
const files = [...huskyHooks(root)];
|
|
923
|
+
files.push(...[...LEFTHOOK_FILES, ...SIMPLE_GIT_HOOKS_FILES].filter((file) => existsSync9(path10.join(root, file))));
|
|
924
|
+
if (packageJson != null && "simple-git-hooks" in packageJson)
|
|
925
|
+
files.push("package.json (simple-git-hooks)");
|
|
926
|
+
const gitConfig = read(root, GIT_CONFIG);
|
|
927
|
+
if (gitConfig != null && gitConfig.includes("hooksPath"))
|
|
928
|
+
files.push(`${GIT_CONFIG} (core.hooksPath)`);
|
|
929
|
+
return files;
|
|
930
|
+
}
|
|
931
|
+
function readHookFacts(root, packageJson, scripts, commandForms) {
|
|
932
|
+
const files = managerFiles(root, packageJson);
|
|
933
|
+
const script = HOOK_SCRIPTS.find((name) => scripts[name] != null) ?? null;
|
|
934
|
+
const sources = files.map((file) => {
|
|
935
|
+
if (file.startsWith("package.json"))
|
|
936
|
+
return JSON.stringify(packageJson?.["simple-git-hooks"] ?? "");
|
|
937
|
+
return read(root, file.split(" ")[0]) ?? "";
|
|
239
938
|
});
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
modifiedFiles,
|
|
245
|
-
missingDiscovery,
|
|
246
|
-
harnessProblems: problems
|
|
247
|
-
};
|
|
939
|
+
const manager = files[0] ?? null;
|
|
940
|
+
const scriptRunsHarness = script != null && runsHarnessCommand(scripts[script] ?? "", commandForms);
|
|
941
|
+
const installed = sources.some((source) => runsHarnessCommand(source, commandForms) || scriptRunsHarness && script != null && source.includes(script));
|
|
942
|
+
return { manager, script, runsHarness: manager != null && installed };
|
|
248
943
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
944
|
+
|
|
945
|
+
// src/commands/doctor/workflows.ts
|
|
946
|
+
import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync10 } from "fs";
|
|
947
|
+
import path11 from "path";
|
|
948
|
+
var WORKFLOWS_DIR = ".github/workflows";
|
|
949
|
+
var RUN_STEP = /(?:^|\s)run:[ \t]*(\S.*)$/;
|
|
950
|
+
function runStepTexts(source) {
|
|
951
|
+
const lines = source.split("\n");
|
|
952
|
+
const steps = [];
|
|
953
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
954
|
+
const inline = RUN_STEP.exec(lines[index])?.[1];
|
|
955
|
+
if (inline == null)
|
|
956
|
+
continue;
|
|
957
|
+
if (!inline.startsWith("|") && !inline.startsWith(">")) {
|
|
958
|
+
steps.push(inline.trim());
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
const indent = lines[index].length - lines[index].trimStart().length;
|
|
962
|
+
const block = [];
|
|
963
|
+
for (let next = index + 1; next < lines.length; next += 1) {
|
|
964
|
+
const line = lines[next];
|
|
965
|
+
if (line.trim() !== "" && line.length - line.trimStart().length <= indent)
|
|
966
|
+
break;
|
|
967
|
+
block.push(line.trim());
|
|
968
|
+
}
|
|
969
|
+
steps.push(block.join("\n"));
|
|
253
970
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
971
|
+
return steps;
|
|
972
|
+
}
|
|
973
|
+
function readWorkflowFacts(root, commandForms) {
|
|
974
|
+
const directory = path11.join(root, WORKFLOWS_DIR);
|
|
975
|
+
if (!existsSync10(directory))
|
|
976
|
+
return { directory: WORKFLOWS_DIR, files: [], harnessWorkflow: null, unreadable: [] };
|
|
977
|
+
const files = readdirSync5(directory).filter((file) => file.endsWith(".yml") || file.endsWith(".yaml")).sort();
|
|
978
|
+
const unreadable = [];
|
|
979
|
+
let harnessWorkflow = null;
|
|
980
|
+
for (const file of files) {
|
|
981
|
+
let source;
|
|
982
|
+
try {
|
|
983
|
+
source = readFileSync10(path11.join(directory, file), "utf8");
|
|
984
|
+
} catch {
|
|
985
|
+
unreadable.push(`${WORKFLOWS_DIR}/${file}`);
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
if (harnessWorkflow == null && runStepTexts(source).some((step) => runsHarnessCommand(step, commandForms)))
|
|
989
|
+
harnessWorkflow = `${WORKFLOWS_DIR}/${file}`;
|
|
260
990
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
991
|
+
return { directory: WORKFLOWS_DIR, files, harnessWorkflow, unreadable };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/commands/doctor/evidence.ts
|
|
995
|
+
var TEST_FILE = /\.test\.[cm]?[jt]s$/;
|
|
996
|
+
var LINT_POLICY_MARKERS = ["calculateConfigForFile", "lintText", "lintFiles"];
|
|
997
|
+
function recordedTestFiles(manifest) {
|
|
998
|
+
return Object.keys(manifest.files).filter((file) => TEST_FILE.test(file)).sort();
|
|
999
|
+
}
|
|
1000
|
+
function gatherEvidence(root, manifest) {
|
|
1001
|
+
const harness = readHarnessFacts(root, manifest.harness.command);
|
|
1002
|
+
const runner = readRunnerFacts(root, harness.resolved);
|
|
1003
|
+
const workflows = readWorkflowFacts(root, harness.commandForms);
|
|
1004
|
+
const hooks = readHookFacts(root, harness.packageJson, harness.scripts, harness.commandForms);
|
|
1005
|
+
const recordedTests = recordedTestFiles(manifest);
|
|
1006
|
+
const policyTests = [];
|
|
1007
|
+
const unreadable = [...workflows.unreadable];
|
|
1008
|
+
for (const file of recordedTests) {
|
|
1009
|
+
try {
|
|
1010
|
+
const source = readFileSync11(path12.join(root, file), "utf8");
|
|
1011
|
+
if (LINT_POLICY_MARKERS.some((marker) => source.includes(marker)))
|
|
1012
|
+
policyTests.push(file);
|
|
1013
|
+
} catch {
|
|
1014
|
+
unreadable.push(file);
|
|
1015
|
+
}
|
|
266
1016
|
}
|
|
267
|
-
return
|
|
1017
|
+
return { harness, runner, workflows, hooks, recordedTests, policyTests, unreadable };
|
|
268
1018
|
}
|
|
269
1019
|
|
|
270
|
-
// src/commands/
|
|
271
|
-
|
|
272
|
-
|
|
1020
|
+
// src/commands/doctor/provenance.ts
|
|
1021
|
+
function markerAuthorship(recorded, body) {
|
|
1022
|
+
if (recorded.authoredBy !== "construct" || recorded.sha == null || body == null)
|
|
1023
|
+
return "unknown";
|
|
1024
|
+
return sha256(body) === recorded.sha ? "construct" : "owner";
|
|
1025
|
+
}
|
|
1026
|
+
function discoveryProvenance(root, manifest) {
|
|
1027
|
+
return DISCOVERY_MARKERS.map((marker) => {
|
|
1028
|
+
const recorded = manifest.discovery.markers[marker];
|
|
1029
|
+
return {
|
|
1030
|
+
marker,
|
|
1031
|
+
file: recorded.file,
|
|
1032
|
+
authorship: markerAuthorship(recorded, markerBody(root, marker, markerFileFor(root, manifest, marker)))
|
|
1033
|
+
};
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
function constructAuthored(readings) {
|
|
1037
|
+
return readings.filter((reading) => reading.authorship === "construct");
|
|
1038
|
+
}
|
|
273
1039
|
|
|
274
|
-
// src/
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
// src/detect/existing.ts
|
|
280
|
-
import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
|
|
281
|
-
import path4 from "path";
|
|
282
|
-
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
|
-
var COMPOSITION_CANDIDATES = ["architecture/composition", "docs/architecture/composition", "docs/composition", "composition"];
|
|
284
|
-
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
|
-
function anyExists(dir, candidates) {
|
|
286
|
-
return candidates.some((candidate) => existsSync4(path4.join(dir, candidate)));
|
|
287
|
-
}
|
|
288
|
-
function firstExisting(dir, candidates) {
|
|
289
|
-
return candidates.find((candidate) => existsSync4(path4.join(dir, candidate))) ?? null;
|
|
290
|
-
}
|
|
291
|
-
function compositionDir(dir) {
|
|
292
|
-
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"));
|
|
295
|
-
}) ?? null;
|
|
296
|
-
}
|
|
297
|
-
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"));
|
|
300
|
-
}
|
|
301
|
-
function detectExisting(dir) {
|
|
302
|
-
return {
|
|
303
|
-
packageJson: existsSync4(path4.join(dir, "package.json")),
|
|
304
|
-
tsconfig: existsSync4(path4.join(dir, "tsconfig.json")),
|
|
305
|
-
eslintConfig: anyExists(dir, ESLINT_CONFIGS),
|
|
306
|
-
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")),
|
|
310
|
-
openapi: firstExisting(dir, OPENAPI_CANDIDATES),
|
|
311
|
-
compositionDir: compositionDir(dir),
|
|
312
|
-
constructJson: existsSync4(path4.join(dir, "construct.json"))
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// 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";
|
|
319
|
-
|
|
320
|
-
// src/detect/workspaces.ts
|
|
321
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
322
|
-
import path5 from "path";
|
|
323
|
-
var TOP_LEVEL_PACKAGES_KEY = /^packages:(.*)$/m;
|
|
324
|
-
var EMPTY_FLOW_SEQUENCE = /^\[\s*\]$/;
|
|
325
|
-
var BLOCK_SEQUENCE_ENTRY = /^[ \t]+-[ \t]*\S/;
|
|
326
|
-
function readIfPresent(file) {
|
|
327
|
-
if (!existsSync5(file))
|
|
328
|
-
return null;
|
|
329
|
-
try {
|
|
330
|
-
return readFileSync4(file, "utf8");
|
|
331
|
-
} catch {
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
function startsBlockSequence(rest) {
|
|
336
|
-
for (const line of rest.split("\n")) {
|
|
337
|
-
if (line.trim() === "" || line.trimStart().startsWith("#"))
|
|
338
|
-
continue;
|
|
339
|
-
return BLOCK_SEQUENCE_ENTRY.test(line);
|
|
340
|
-
}
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
function declaresPnpmPackages(dir) {
|
|
344
|
-
const content = readIfPresent(path5.join(dir, "pnpm-workspace.yaml"));
|
|
345
|
-
if (content == null)
|
|
346
|
-
return false;
|
|
347
|
-
const match = TOP_LEVEL_PACKAGES_KEY.exec(content);
|
|
348
|
-
if (match == null)
|
|
349
|
-
return false;
|
|
350
|
-
const inline = match[1].trim();
|
|
351
|
-
if (inline.startsWith("["))
|
|
352
|
-
return !EMPTY_FLOW_SEQUENCE.test(inline);
|
|
353
|
-
if (inline !== "")
|
|
354
|
-
return false;
|
|
355
|
-
return startsBlockSequence(content.slice(match.index + match[0].length));
|
|
356
|
-
}
|
|
357
|
-
function declaresNpmWorkspaces(dir) {
|
|
358
|
-
const content = readIfPresent(path5.join(dir, "package.json"));
|
|
359
|
-
if (content == null)
|
|
360
|
-
return false;
|
|
361
|
-
let workspaces;
|
|
362
|
-
try {
|
|
363
|
-
({ workspaces } = JSON.parse(content));
|
|
364
|
-
} catch {
|
|
365
|
-
return false;
|
|
366
|
-
}
|
|
367
|
-
if (Array.isArray(workspaces))
|
|
368
|
-
return workspaces.length > 0;
|
|
369
|
-
const packages = workspaces?.packages;
|
|
370
|
-
return Array.isArray(packages) && packages.length > 0;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// src/detect/layout.ts
|
|
374
|
-
var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
|
|
375
|
-
function isEmptyDir(dir) {
|
|
376
|
-
if (!existsSync6(dir))
|
|
377
|
-
return true;
|
|
378
|
-
return readdirSync4(dir).every((entry) => IGNORED_ENTRIES.has(entry));
|
|
379
|
-
}
|
|
380
|
-
function detectMonorepoTools(dir) {
|
|
381
|
-
const tools = [];
|
|
382
|
-
if (declaresPnpmPackages(dir))
|
|
383
|
-
tools.push("pnpm-workspace");
|
|
384
|
-
if (declaresNpmWorkspaces(dir))
|
|
385
|
-
tools.push("npm-workspaces");
|
|
386
|
-
if (existsSync6(path6.join(dir, "turbo.json")))
|
|
387
|
-
tools.push("turbo");
|
|
388
|
-
if (existsSync6(path6.join(dir, "nx.json")))
|
|
389
|
-
tools.push("nx");
|
|
390
|
-
return tools;
|
|
391
|
-
}
|
|
392
|
-
function detectWorkspaceDirs(dir) {
|
|
393
|
-
return ["apps", "packages", "libs", "services"].filter((name) => existsSync6(path6.join(dir, name)) && statSync2(path6.join(dir, name)).isDirectory());
|
|
394
|
-
}
|
|
395
|
-
function packageName(dir) {
|
|
396
|
-
try {
|
|
397
|
-
const parsed = JSON.parse(readFileSync5(path6.join(dir, "package.json"), "utf8"));
|
|
398
|
-
return typeof parsed.name === "string" && parsed.name !== "" ? parsed.name : null;
|
|
399
|
-
} catch {
|
|
400
|
-
return null;
|
|
1040
|
+
// src/commands/doctor/typecheck.ts
|
|
1041
|
+
var CAVEATS = {
|
|
1042
|
+
"node-frontend": {
|
|
1043
|
+
checkers: ["vue-tsc", "svelte-check"],
|
|
1044
|
+
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"
|
|
401
1045
|
}
|
|
402
|
-
}
|
|
403
|
-
function
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return "empty";
|
|
409
|
-
if (monorepoTools.length > 0 || workspaceDirs.length > 0)
|
|
410
|
-
return "monorepo";
|
|
411
|
-
if (hasSrc || existsSync6(path6.join(dir, "package.json")))
|
|
412
|
-
return "single";
|
|
413
|
-
return "unknown";
|
|
1046
|
+
};
|
|
1047
|
+
function typecheckWarnings(preset, evidence) {
|
|
1048
|
+
const caveat = CAVEATS[preset];
|
|
1049
|
+
if (caveat == null || caveat.checkers.some((checker) => evidence.harness.resolved.includes(checker)))
|
|
1050
|
+
return [];
|
|
1051
|
+
return [caveat.text];
|
|
414
1052
|
}
|
|
415
1053
|
|
|
416
|
-
// src/
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
["bun.lockb", "bun"],
|
|
423
|
-
["bun.lock", "bun"],
|
|
424
|
-
["yarn.lock", "yarn"],
|
|
425
|
-
["package-lock.json", "npm"]
|
|
426
|
-
];
|
|
427
|
-
function fromPackageManagerField(dir) {
|
|
428
|
-
const manifest = path7.join(dir, "package.json");
|
|
429
|
-
if (!existsSync7(manifest))
|
|
430
|
-
return null;
|
|
431
|
-
try {
|
|
432
|
-
const parsed = JSON.parse(readFileSync6(manifest, "utf8"));
|
|
433
|
-
const name = parsed.packageManager?.split("@")[0];
|
|
434
|
-
return name === "pnpm" || name === "npm" || name === "yarn" || name === "bun" ? name : null;
|
|
435
|
-
} catch {
|
|
1054
|
+
// src/commands/doctor/verdict.ts
|
|
1055
|
+
var CHECK_IDS = ["lint-policy", "construct-tests", "ci", "hook", "red-gate"];
|
|
1056
|
+
var LEVELS = ["L0", "L1", "L2", "L3", "L4"];
|
|
1057
|
+
function weakestLink(checks) {
|
|
1058
|
+
const claimed = checks.filter((check) => check.state === "present");
|
|
1059
|
+
if (claimed.length === 0)
|
|
436
1060
|
return null;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const declared = fromPackageManagerField(dir);
|
|
441
|
-
if (declared != null)
|
|
442
|
-
return declared;
|
|
443
|
-
for (const [lockfile, manager] of LOCKFILES) {
|
|
444
|
-
if (existsSync7(path7.join(dir, lockfile)))
|
|
445
|
-
return manager;
|
|
446
|
-
}
|
|
447
|
-
return existsSync7(path7.join(dir, "package.json")) ? "npm" : "none";
|
|
448
|
-
}
|
|
449
|
-
var pnpmVersionCache;
|
|
450
|
-
function detectPnpmVersion() {
|
|
451
|
-
if (pnpmVersionCache !== void 0)
|
|
452
|
-
return pnpmVersionCache;
|
|
453
|
-
try {
|
|
454
|
-
pnpmVersionCache = execFileSync("pnpm", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
455
|
-
} catch {
|
|
456
|
-
pnpmVersionCache = null;
|
|
457
|
-
}
|
|
458
|
-
return pnpmVersionCache;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// src/detect/index.ts
|
|
462
|
-
function detect(dir) {
|
|
463
|
-
const root = path8.resolve(dir);
|
|
464
|
-
const monorepoTools = detectMonorepoTools(root);
|
|
465
|
-
const workspaceDirs = detectWorkspaceDirs(root);
|
|
466
|
-
const hasSrc = existsSync8(path8.join(root, "src"));
|
|
467
|
-
return {
|
|
468
|
-
dir: root,
|
|
469
|
-
packageManager: detectPackageManager(root),
|
|
470
|
-
pnpmVersion: detectPnpmVersion(),
|
|
471
|
-
layout: detectLayout(root, monorepoTools, workspaceDirs, hasSrc),
|
|
472
|
-
monorepoTools,
|
|
473
|
-
workspaceDirs,
|
|
474
|
-
workspacePackages: detectWorkspacePackages(root, workspaceDirs),
|
|
475
|
-
hasSrc,
|
|
476
|
-
nodeMajor: Number(process.versions.node.split(".")[0]),
|
|
477
|
-
existing: detectExisting(root)
|
|
478
|
-
};
|
|
1061
|
+
const ordered = [...claimed].sort((left, right) => CHECK_IDS.indexOf(left.id) - CHECK_IDS.indexOf(right.id));
|
|
1062
|
+
const weakest = ordered.reduce((current, check) => LEVELS.indexOf(check.level) < LEVELS.indexOf(current.level) ? check : current);
|
|
1063
|
+
return { id: weakest.id, level: weakest.level };
|
|
479
1064
|
}
|
|
480
1065
|
|
|
481
|
-
// src/
|
|
482
|
-
import {
|
|
483
|
-
import
|
|
484
|
-
|
|
485
|
-
const written = [];
|
|
486
|
-
for (const op of ops) {
|
|
487
|
-
if (op.action === "skip")
|
|
488
|
-
continue;
|
|
489
|
-
const absolute = path9.join(root, op.target);
|
|
490
|
-
mkdirSync(path9.dirname(absolute), { recursive: true });
|
|
491
|
-
writeFileSync2(absolute, op.content);
|
|
492
|
-
written.push(op);
|
|
493
|
-
}
|
|
494
|
-
return written;
|
|
495
|
-
}
|
|
1066
|
+
// src/sync/replay.ts
|
|
1067
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
|
|
1068
|
+
import { tmpdir } from "os";
|
|
1069
|
+
import path15 from "path";
|
|
496
1070
|
|
|
497
1071
|
// src/materialize/plan.ts
|
|
498
|
-
import { existsSync as
|
|
499
|
-
import
|
|
1072
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
|
|
1073
|
+
import path14 from "path";
|
|
500
1074
|
|
|
501
1075
|
// src/materialize/rules.ts
|
|
502
1076
|
var CLAUDE_RULES_DIR = ".claude/rules/";
|
|
@@ -593,7 +1167,7 @@ function strategyFor(target) {
|
|
|
593
1167
|
return "append-block";
|
|
594
1168
|
return "create";
|
|
595
1169
|
}
|
|
596
|
-
function
|
|
1170
|
+
function isJsonObject(value) {
|
|
597
1171
|
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
598
1172
|
}
|
|
599
1173
|
function mergeJson(existing, incoming, conflicts, prefix = "") {
|
|
@@ -605,7 +1179,7 @@ function mergeJson(existing, incoming, conflicts, prefix = "") {
|
|
|
605
1179
|
continue;
|
|
606
1180
|
}
|
|
607
1181
|
const current = existing[key];
|
|
608
|
-
if (
|
|
1182
|
+
if (isJsonObject(current) && isJsonObject(value)) {
|
|
609
1183
|
result[key] = mergeJson(current, value, conflicts, at);
|
|
610
1184
|
continue;
|
|
611
1185
|
}
|
|
@@ -614,7 +1188,7 @@ function mergeJson(existing, incoming, conflicts, prefix = "") {
|
|
|
614
1188
|
}
|
|
615
1189
|
return result;
|
|
616
1190
|
}
|
|
617
|
-
function
|
|
1191
|
+
function blockMarkers(target) {
|
|
618
1192
|
return target.endsWith(".gitignore") ? [GITIGNORE_BEGIN, GITIGNORE_END] : [BLOCK_BEGIN, BLOCK_END];
|
|
619
1193
|
}
|
|
620
1194
|
var DISCOVERY_OPEN = /<!-- construct:discover:([\w-]+) -->/g;
|
|
@@ -627,6 +1201,16 @@ function discoveryBlock(document, marker) {
|
|
|
627
1201
|
return null;
|
|
628
1202
|
return { start: start + open.length, end, body: document.slice(start + open.length, end) };
|
|
629
1203
|
}
|
|
1204
|
+
function withoutDiscoveryBodies(document) {
|
|
1205
|
+
let result = document;
|
|
1206
|
+
for (const [, marker] of document.matchAll(DISCOVERY_OPEN)) {
|
|
1207
|
+
const block = discoveryBlock(result, marker);
|
|
1208
|
+
if (block == null)
|
|
1209
|
+
continue;
|
|
1210
|
+
result = `${result.slice(0, block.start)}${result.slice(block.end)}`;
|
|
1211
|
+
}
|
|
1212
|
+
return result;
|
|
1213
|
+
}
|
|
630
1214
|
function preserveDiscovery(existing, incoming) {
|
|
631
1215
|
let result = incoming;
|
|
632
1216
|
for (const [, marker] of existing.matchAll(DISCOVERY_OPEN)) {
|
|
@@ -638,13 +1222,20 @@ function preserveDiscovery(existing, incoming) {
|
|
|
638
1222
|
}
|
|
639
1223
|
return result;
|
|
640
1224
|
}
|
|
1225
|
+
function substituteBlock(existing, produced, target) {
|
|
1226
|
+
const [begin, end] = blockMarkers(target);
|
|
1227
|
+
const opening = existing.indexOf(begin) + begin.length;
|
|
1228
|
+
const closing = existing.indexOf(end);
|
|
1229
|
+
const incoming = produced.slice(produced.indexOf(begin) + begin.length, produced.indexOf(end));
|
|
1230
|
+
return `${existing.slice(0, opening)}${preserveDiscovery(existing, incoming)}${existing.slice(closing)}`;
|
|
1231
|
+
}
|
|
641
1232
|
function withoutSecondH1(existing, block) {
|
|
642
1233
|
if (existing.trim() === "" || !/^# /m.test(existing))
|
|
643
1234
|
return block;
|
|
644
1235
|
return block.replace(/^# (.*)$/m, "## $1");
|
|
645
1236
|
}
|
|
646
1237
|
function appendBlock(existing, block, target) {
|
|
647
|
-
const [begin, end] =
|
|
1238
|
+
const [begin, end] = blockMarkers(target);
|
|
648
1239
|
const wrapped = `${begin}
|
|
649
1240
|
${preserveDiscovery(existing, withoutSecondH1(existing, block)).trimEnd()}
|
|
650
1241
|
${end}
|
|
@@ -658,43 +1249,43 @@ ${end}
|
|
|
658
1249
|
}
|
|
659
1250
|
|
|
660
1251
|
// src/materialize/templates.ts
|
|
661
|
-
import { existsSync as
|
|
662
|
-
import
|
|
663
|
-
import { fileURLToPath } from "url";
|
|
664
|
-
var
|
|
1252
|
+
import { existsSync as existsSync11, readdirSync as readdirSync6, statSync as statSync3 } from "fs";
|
|
1253
|
+
import path13 from "path";
|
|
1254
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1255
|
+
var HERE2 = path13.dirname(fileURLToPath2(import.meta.url));
|
|
665
1256
|
function templatesRoot() {
|
|
666
|
-
const candidates = [
|
|
667
|
-
const found = candidates.find((candidate) =>
|
|
1257
|
+
const candidates = [path13.resolve(HERE2, "../templates"), path13.resolve(HERE2, "../../templates")];
|
|
1258
|
+
const found = candidates.find((candidate) => existsSync11(candidate));
|
|
668
1259
|
if (found == null)
|
|
669
|
-
throw new Error(`templates directory not found next to ${
|
|
1260
|
+
throw new Error(`templates directory not found next to ${HERE2}`);
|
|
670
1261
|
return found;
|
|
671
1262
|
}
|
|
672
1263
|
var EXISTING_SUFFIX = ".existing.eta";
|
|
673
1264
|
function toTargetPath(relative) {
|
|
674
|
-
const segments = relative.split(
|
|
1265
|
+
const segments = relative.split(path13.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
|
|
675
1266
|
const joined = segments.join("/");
|
|
676
1267
|
if (joined.endsWith(EXISTING_SUFFIX))
|
|
677
1268
|
return { target: joined.slice(0, -EXISTING_SUFFIX.length), rendered: true, variant: "existing" };
|
|
678
1269
|
return joined.endsWith(".eta") ? { target: joined.slice(0, -".eta".length), rendered: true, variant: "default" } : { target: joined, rendered: false, variant: "default" };
|
|
679
1270
|
}
|
|
680
1271
|
function walk(root, current, files) {
|
|
681
|
-
for (const entry of
|
|
682
|
-
const absolute =
|
|
1272
|
+
for (const entry of readdirSync6(current).sort()) {
|
|
1273
|
+
const absolute = path13.join(current, entry);
|
|
683
1274
|
if (statSync3(absolute).isDirectory())
|
|
684
1275
|
walk(root, absolute, files);
|
|
685
1276
|
else if (entry !== ".DS_Store")
|
|
686
|
-
files.push(
|
|
1277
|
+
files.push(path13.relative(root, absolute));
|
|
687
1278
|
}
|
|
688
1279
|
}
|
|
689
1280
|
function listTemplateFiles(group) {
|
|
690
|
-
const root =
|
|
691
|
-
if (!
|
|
1281
|
+
const root = path13.join(templatesRoot(), group);
|
|
1282
|
+
if (!existsSync11(root))
|
|
692
1283
|
throw new Error(`template group "${group}" does not exist`);
|
|
693
1284
|
const files = [];
|
|
694
1285
|
walk(root, root, files);
|
|
695
1286
|
return files.map((relative) => {
|
|
696
1287
|
const { target, rendered, variant } = toTargetPath(relative);
|
|
697
|
-
return { group, source:
|
|
1288
|
+
return { group, source: path13.join(root, relative), target, rendered, variant };
|
|
698
1289
|
});
|
|
699
1290
|
}
|
|
700
1291
|
var BLOCK = /^[ \t]*\{\{#(if|unless) (\w+)\}\}[ \t]*\n([\s\S]*?)^[ \t]*\{\{\/\1\}\}[ \t]*\n/gm;
|
|
@@ -721,7 +1312,7 @@ function mountTarget(mount, target) {
|
|
|
721
1312
|
return mount.into == null || mount.into === "." ? target : `${mount.into.replace(/\/$/, "")}/${target}`;
|
|
722
1313
|
}
|
|
723
1314
|
function readTemplate(source, rendered, vars) {
|
|
724
|
-
const raw =
|
|
1315
|
+
const raw = readFileSync12(source, "utf8");
|
|
725
1316
|
return rendered ? render(raw, vars) : raw;
|
|
726
1317
|
}
|
|
727
1318
|
var SORTED_SECTIONS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
@@ -779,12 +1370,13 @@ function layerJson(earlier, later) {
|
|
|
779
1370
|
var NOT_ADDED_TO_EXISTING_MANIFEST = ["version"];
|
|
780
1371
|
function planOne(root, target, content, conflicts, existingVariant) {
|
|
781
1372
|
const strategy = strategyFor(target);
|
|
782
|
-
const absolute =
|
|
783
|
-
const exists =
|
|
784
|
-
if (!exists)
|
|
785
|
-
return { target, strategy, action: "create", content:
|
|
1373
|
+
const absolute = path14.join(root, target);
|
|
1374
|
+
const exists = existsSync12(absolute);
|
|
1375
|
+
if (!exists) {
|
|
1376
|
+
return strategy === "append-block" ? { target, strategy, action: "create", content: appendBlock("", content, target), variant: "default" } : { target, strategy, action: "create", content };
|
|
1377
|
+
}
|
|
786
1378
|
if (strategy === "merge-json") {
|
|
787
|
-
const existing = JSON.parse(
|
|
1379
|
+
const existing = JSON.parse(readFileSync12(absolute, "utf8"));
|
|
788
1380
|
const incoming = JSON.parse(content);
|
|
789
1381
|
for (const key of NOT_ADDED_TO_EXISTING_MANIFEST)
|
|
790
1382
|
delete incoming[key];
|
|
@@ -795,8 +1387,14 @@ function planOne(root, target, content, conflicts, existingVariant) {
|
|
|
795
1387
|
` };
|
|
796
1388
|
}
|
|
797
1389
|
if (strategy === "append-block") {
|
|
798
|
-
const existing =
|
|
799
|
-
return {
|
|
1390
|
+
const existing = readFileSync12(absolute, "utf8");
|
|
1391
|
+
return {
|
|
1392
|
+
target,
|
|
1393
|
+
strategy,
|
|
1394
|
+
action: "append",
|
|
1395
|
+
content: appendBlock(existing, existingVariant ?? content, target),
|
|
1396
|
+
variant: existingVariant == null ? "default" : "existing"
|
|
1397
|
+
};
|
|
800
1398
|
}
|
|
801
1399
|
return { target, strategy, action: "skip", content, note: "exists, review manually" };
|
|
802
1400
|
}
|
|
@@ -821,143 +1419,743 @@ function planMaterialize(root, groups, vars, options) {
|
|
|
821
1419
|
layered.set(target, previous == null || strategyFor(target) !== "merge-json" ? content : layerJson(previous, content));
|
|
822
1420
|
}
|
|
823
1421
|
}
|
|
824
|
-
const ops = [...mapRulesForTargets(layered, options.ai).entries()].map(([target, content]) => planOne(root, target, content, conflicts, existingVariants.get(target))).sort((a, b) => a.target.localeCompare(b.target));
|
|
825
|
-
return { ops, conflicts, omittedGroups };
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// src/presets/index.ts
|
|
829
|
-
var AI_TARGET_LABELS = {
|
|
830
|
-
claude: "Claude Code",
|
|
831
|
-
cursor: "Cursor",
|
|
832
|
-
both: "Claude Code, Cursor"
|
|
833
|
-
};
|
|
834
|
-
var EXPRESS_APP = "stacks/express-api/app";
|
|
835
|
-
var EXPRESS_REPO = "stacks/express-api/repo";
|
|
836
|
-
var HTTP_CONTRACT = "stacks/http-contract";
|
|
837
|
-
function sampleWorkspace(scope) {
|
|
838
|
-
return [
|
|
839
|
-
{ dir: "packages/shared", name: `${scope}/shared` },
|
|
840
|
-
{ dir: "apps/api", name: `${scope}/api` }
|
|
841
|
-
];
|
|
1422
|
+
const ops = [...mapRulesForTargets(layered, options.ai).entries()].map(([target, content]) => planOne(root, target, content, conflicts, existingVariants.get(target))).sort((a, b) => a.target.localeCompare(b.target));
|
|
1423
|
+
return { ops, conflicts, omittedGroups, existingVariants: Object.fromEntries(existingVariants) };
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/presets/index.ts
|
|
1427
|
+
var AI_TARGET_LABELS = {
|
|
1428
|
+
claude: "Claude Code",
|
|
1429
|
+
cursor: "Cursor",
|
|
1430
|
+
both: "Claude Code, Cursor"
|
|
1431
|
+
};
|
|
1432
|
+
var EXPRESS_APP = "stacks/express-api/app";
|
|
1433
|
+
var EXPRESS_REPO = "stacks/express-api/repo";
|
|
1434
|
+
var HTTP_CONTRACT = "stacks/http-contract";
|
|
1435
|
+
function sampleWorkspace(scope) {
|
|
1436
|
+
return [
|
|
1437
|
+
{ dir: "packages/shared", name: `${scope}/shared` },
|
|
1438
|
+
{ dir: "apps/api", name: `${scope}/api` }
|
|
1439
|
+
];
|
|
1440
|
+
}
|
|
1441
|
+
function quote(value) {
|
|
1442
|
+
return `'${value}'`;
|
|
1443
|
+
}
|
|
1444
|
+
function renderWorkspacePolicy(packages, sample) {
|
|
1445
|
+
const names = packages.map((pkg) => pkg.name);
|
|
1446
|
+
const allowedFor = (pkg) => {
|
|
1447
|
+
if (sample)
|
|
1448
|
+
return pkg.dir.startsWith("apps/") ? names.filter((name) => name !== pkg.name) : [];
|
|
1449
|
+
return names.filter((name) => name !== pkg.name);
|
|
1450
|
+
};
|
|
1451
|
+
const lines = packages.map((pkg) => ` ${quote(pkg.dir)}: [${allowedFor(pkg).map(quote).join(", ")}],`);
|
|
1452
|
+
return {
|
|
1453
|
+
workspacePackages: `[${names.map(quote).join(", ")}]`,
|
|
1454
|
+
allowedWorkspaceImports: `{
|
|
1455
|
+
${lines.join("\n")}
|
|
1456
|
+
}`
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
var PRESETS = {
|
|
1460
|
+
"node-backend": {
|
|
1461
|
+
id: "node-backend",
|
|
1462
|
+
label: "Node.js backend",
|
|
1463
|
+
description: "Express + TypeScript, contract-first HTTP API, composition root, harness",
|
|
1464
|
+
groups: [
|
|
1465
|
+
"base",
|
|
1466
|
+
"harness",
|
|
1467
|
+
HTTP_CONTRACT,
|
|
1468
|
+
{ group: EXPRESS_APP, onlyWhenEmpty: true },
|
|
1469
|
+
{ group: EXPRESS_REPO, onlyWhenEmpty: true },
|
|
1470
|
+
{ group: "presets/node-backend/sample", onlyWhenEmpty: true },
|
|
1471
|
+
"presets/node-backend/baseline"
|
|
1472
|
+
],
|
|
1473
|
+
contracts: true,
|
|
1474
|
+
available: true,
|
|
1475
|
+
vars: () => ({
|
|
1476
|
+
contractPath: "contracts/api/openapi.yaml",
|
|
1477
|
+
contractTypesOutput: "src/contracts/openapi.ts",
|
|
1478
|
+
contractTypesImport: "./openapi.js",
|
|
1479
|
+
contractPathFromConfig: "../contracts/api/openapi.yaml",
|
|
1480
|
+
appRoot: ""
|
|
1481
|
+
})
|
|
1482
|
+
},
|
|
1483
|
+
"node-frontend": {
|
|
1484
|
+
id: "node-frontend",
|
|
1485
|
+
label: "Node.js frontend",
|
|
1486
|
+
description: "Vite + TypeScript, platform CSS rules, composition root, harness; no API contract",
|
|
1487
|
+
groups: [
|
|
1488
|
+
"base",
|
|
1489
|
+
"harness",
|
|
1490
|
+
{ group: "presets/node-frontend/sample", onlyWhenEmpty: true },
|
|
1491
|
+
"presets/node-frontend/baseline"
|
|
1492
|
+
],
|
|
1493
|
+
contracts: false,
|
|
1494
|
+
available: true,
|
|
1495
|
+
vars: () => ({
|
|
1496
|
+
contractPath: "",
|
|
1497
|
+
contractTypesOutput: ""
|
|
1498
|
+
})
|
|
1499
|
+
},
|
|
1500
|
+
"node-library": {
|
|
1501
|
+
id: "node-library",
|
|
1502
|
+
label: "Node.js library or CLI",
|
|
1503
|
+
description: "TypeScript package with no HTTP contract: architecture policy, composition models, harness",
|
|
1504
|
+
groups: ["base", "harness"],
|
|
1505
|
+
contracts: false,
|
|
1506
|
+
available: true,
|
|
1507
|
+
vars: () => ({
|
|
1508
|
+
contractPath: "",
|
|
1509
|
+
contractTypesOutput: ""
|
|
1510
|
+
})
|
|
1511
|
+
},
|
|
1512
|
+
"monorepo": {
|
|
1513
|
+
id: "monorepo",
|
|
1514
|
+
label: "pnpm monorepo",
|
|
1515
|
+
description: "apps/* + packages/*, catalog:, contract types in packages/shared, dependency policy in lint",
|
|
1516
|
+
groups: [
|
|
1517
|
+
"base",
|
|
1518
|
+
"harness",
|
|
1519
|
+
HTTP_CONTRACT,
|
|
1520
|
+
{ group: EXPRESS_APP, into: "apps/api", onlyWhenEmpty: true },
|
|
1521
|
+
{ group: EXPRESS_REPO, onlyWhenEmpty: true },
|
|
1522
|
+
{ group: "presets/monorepo/sample", onlyWhenEmpty: true },
|
|
1523
|
+
"presets/monorepo/baseline"
|
|
1524
|
+
],
|
|
1525
|
+
contracts: true,
|
|
1526
|
+
available: true,
|
|
1527
|
+
vars: (report, projectName) => {
|
|
1528
|
+
const detected = report.workspacePackages;
|
|
1529
|
+
const packages = detected.length > 0 ? detected : sampleWorkspace(`@${projectName}`);
|
|
1530
|
+
return {
|
|
1531
|
+
contractPath: "contracts/api/openapi.yaml",
|
|
1532
|
+
contractTypesOutput: "packages/shared/src/api/openapi.ts",
|
|
1533
|
+
contractTypesImport: `@${projectName}/shared`,
|
|
1534
|
+
contractPathFromConfig: "../../../contracts/api/openapi.yaml",
|
|
1535
|
+
appRoot: "apps/api/",
|
|
1536
|
+
...renderWorkspacePolicy(packages, detected.length === 0)
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
var PRESET_IDS = Object.keys(PRESETS);
|
|
1542
|
+
var PRESET_LIST = Object.values(PRESETS);
|
|
1543
|
+
function isPresetId(value) {
|
|
1544
|
+
return value in PRESETS;
|
|
1545
|
+
}
|
|
1546
|
+
function getPreset(id) {
|
|
1547
|
+
return PRESETS[id];
|
|
1548
|
+
}
|
|
1549
|
+
function aiGroups(target) {
|
|
1550
|
+
return target === "both" ? ["ai/shared", "ai/claude", "ai/cursor"] : ["ai/shared", `ai/${target}`];
|
|
1551
|
+
}
|
|
1552
|
+
var DEFAULT_REVIEW_MODEL = "claude-sonnet-5";
|
|
1553
|
+
function reviewGroups(provider) {
|
|
1554
|
+
return provider === "claude" ? ["ai/review"] : [];
|
|
1555
|
+
}
|
|
1556
|
+
function defaultProjectName(dir) {
|
|
1557
|
+
const base = dir.split(/[\\/]/).filter(Boolean).at(-1) ?? "project";
|
|
1558
|
+
return base.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/sync/ownership.ts
|
|
1562
|
+
function ownedText(target, content) {
|
|
1563
|
+
if (strategyFor(target) !== "append-block")
|
|
1564
|
+
return content;
|
|
1565
|
+
if (!carriesConstructBlock(target, content))
|
|
1566
|
+
return "";
|
|
1567
|
+
const [begin, end] = blockMarkers(target);
|
|
1568
|
+
return withoutDiscoveryBodies(content.slice(content.indexOf(begin) + begin.length, content.indexOf(end)));
|
|
1569
|
+
}
|
|
1570
|
+
function blockSpansDocument(target, content) {
|
|
1571
|
+
const [begin, end] = blockMarkers(target);
|
|
1572
|
+
const before = content.slice(0, content.indexOf(begin));
|
|
1573
|
+
const after = content.slice(content.indexOf(end) + end.length);
|
|
1574
|
+
return `${before}${after}`.trim() === "";
|
|
1575
|
+
}
|
|
1576
|
+
function carriesConstructBlock(target, content) {
|
|
1577
|
+
const [begin, end] = blockMarkers(target);
|
|
1578
|
+
const start = content.indexOf(begin);
|
|
1579
|
+
const stop = content.indexOf(end);
|
|
1580
|
+
return start !== -1 && stop > start;
|
|
1581
|
+
}
|
|
1582
|
+
function ownedSha(target, content) {
|
|
1583
|
+
return sha256(ownedText(target, content));
|
|
1584
|
+
}
|
|
1585
|
+
function matchesRecordedSha(recorded, target, content) {
|
|
1586
|
+
const asOwnedViewWrittenBySync = ownedSha(target, content);
|
|
1587
|
+
const asWholeFileWrittenByInit = sha256(content);
|
|
1588
|
+
return recorded === asOwnedViewWrittenBySync || recorded === asWholeFileWrittenByInit;
|
|
1589
|
+
}
|
|
1590
|
+
function parseObject(content) {
|
|
1591
|
+
try {
|
|
1592
|
+
const parsed = JSON.parse(content);
|
|
1593
|
+
return isJsonObject(parsed) ? parsed : null;
|
|
1594
|
+
} catch {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
function compareKeys(present, produced, prefix) {
|
|
1599
|
+
const owned = [];
|
|
1600
|
+
for (const [key, value] of Object.entries(produced)) {
|
|
1601
|
+
const at = prefix === "" ? key : `${prefix}.${key}`;
|
|
1602
|
+
if (!(key in present)) {
|
|
1603
|
+
owned.push({ key: at, class: "add" });
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
const current = present[key];
|
|
1607
|
+
if (isJsonObject(current) && isJsonObject(value)) {
|
|
1608
|
+
owned.push(...compareKeys(current, value, at));
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
owned.push({ key: at, class: JSON.stringify(current) === JSON.stringify(value) ? "keep" : "conflict" });
|
|
1612
|
+
}
|
|
1613
|
+
return owned;
|
|
1614
|
+
}
|
|
1615
|
+
function ownedKeys(present, produced) {
|
|
1616
|
+
const current = parseObject(present);
|
|
1617
|
+
const template = parseObject(produced);
|
|
1618
|
+
if (current == null || template == null)
|
|
1619
|
+
return null;
|
|
1620
|
+
return compareKeys(current, template, "");
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// src/sync/classify.ts
|
|
1624
|
+
var PATH_CLASSES = ["add", "keep", "update", "conflict", "unknown", "removed", "orphaned", "foreign"];
|
|
1625
|
+
var BLOCK_REPLACED_WHOLE_DISCOVERY_BODIES_CARRIED_OVER = "block-replaced-whole-discovery-bodies-carried-over";
|
|
1626
|
+
function classFromKeys(keys) {
|
|
1627
|
+
if (keys.some((key) => key.class === "conflict"))
|
|
1628
|
+
return "conflict";
|
|
1629
|
+
return keys.some((key) => key.class === "add") ? "update" : "keep";
|
|
1630
|
+
}
|
|
1631
|
+
function compareDeclaredBlock(target, present, produced, variant) {
|
|
1632
|
+
if (!carriesConstructBlock(target, present))
|
|
1633
|
+
return "conflict";
|
|
1634
|
+
if (variant == null)
|
|
1635
|
+
return "unknown";
|
|
1636
|
+
return ownedText(target, present) === ownedText(target, produced) ? "keep" : "update";
|
|
1637
|
+
}
|
|
1638
|
+
function shapeSuggests(target, present) {
|
|
1639
|
+
return blockSpansDocument(target, present) ? "default" : "existing";
|
|
1640
|
+
}
|
|
1641
|
+
function compareOwnedView(target, recordedSha, present, produced) {
|
|
1642
|
+
if (ownedText(target, present) === ownedText(target, produced))
|
|
1643
|
+
return "keep";
|
|
1644
|
+
if (matchesRecordedSha(recordedSha, target, present))
|
|
1645
|
+
return "update";
|
|
1646
|
+
return "conflict";
|
|
1647
|
+
}
|
|
1648
|
+
function classifyPath(state) {
|
|
1649
|
+
const { target, recordedSha, present, produced } = state;
|
|
1650
|
+
const variant = state.variant ?? null;
|
|
1651
|
+
const strategy = strategyFor(target);
|
|
1652
|
+
const classified = (value, keys = []) => ({
|
|
1653
|
+
target,
|
|
1654
|
+
strategy,
|
|
1655
|
+
class: value,
|
|
1656
|
+
keys,
|
|
1657
|
+
writeEffect: writeEffectFor(strategy, value),
|
|
1658
|
+
...strategy === "append-block" ? { variant } : {},
|
|
1659
|
+
...value === "unknown" && present != null ? { shape: shapeSuggests(target, present) } : {}
|
|
1660
|
+
});
|
|
1661
|
+
if (present == null) {
|
|
1662
|
+
if (recordedSha != null)
|
|
1663
|
+
return classified("removed");
|
|
1664
|
+
return produced == null ? null : classified("add");
|
|
1665
|
+
}
|
|
1666
|
+
if (produced == null)
|
|
1667
|
+
return classified(recordedSha == null ? "foreign" : "orphaned");
|
|
1668
|
+
if (strategy === "merge-json") {
|
|
1669
|
+
const keys = ownedKeys(present, produced);
|
|
1670
|
+
if (keys == null)
|
|
1671
|
+
return classified("conflict");
|
|
1672
|
+
return classified(recordedSha == null ? "conflict" : classFromKeys(keys), keys);
|
|
1673
|
+
}
|
|
1674
|
+
if (recordedSha == null)
|
|
1675
|
+
return classified("conflict");
|
|
1676
|
+
if (strategy === "append-block")
|
|
1677
|
+
return classified(compareDeclaredBlock(target, present, produced, variant));
|
|
1678
|
+
return classified(compareOwnedView(target, recordedSha, present, produced));
|
|
1679
|
+
}
|
|
1680
|
+
var WRITABLE_CLASSES = /* @__PURE__ */ new Set(["add", "update"]);
|
|
1681
|
+
var WRITABLE_STRATEGIES = /* @__PURE__ */ new Set(["create", "append-block"]);
|
|
1682
|
+
function willBeWritten(strategy, value) {
|
|
1683
|
+
return WRITABLE_CLASSES.has(value) && WRITABLE_STRATEGIES.has(strategy);
|
|
1684
|
+
}
|
|
1685
|
+
function writeEffectFor(strategy, value) {
|
|
1686
|
+
return strategy === "append-block" && willBeWritten(strategy, value) ? BLOCK_REPLACED_WHOLE_DISCOVERY_BODIES_CARRIED_OVER : null;
|
|
1687
|
+
}
|
|
1688
|
+
function isWritable(classification) {
|
|
1689
|
+
return willBeWritten(classification.strategy, classification.class);
|
|
1690
|
+
}
|
|
1691
|
+
function classifyRepository(state) {
|
|
1692
|
+
const targets = [.../* @__PURE__ */ new Set([...Object.keys(state.recorded), ...Object.keys(state.present), ...Object.keys(state.produced)])].sort();
|
|
1693
|
+
return targets.flatMap((target) => {
|
|
1694
|
+
const classification = classifyPath({
|
|
1695
|
+
target,
|
|
1696
|
+
recordedSha: state.recorded[target] ?? null,
|
|
1697
|
+
present: state.present[target] ?? null,
|
|
1698
|
+
produced: state.produced[target] ?? null,
|
|
1699
|
+
variant: state.variants?.[target] ?? null
|
|
1700
|
+
});
|
|
1701
|
+
return classification == null ? [] : [classification];
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// src/sync/variant.ts
|
|
1706
|
+
function existingForm(target, template) {
|
|
1707
|
+
return appendBlock("", template, target);
|
|
1708
|
+
}
|
|
1709
|
+
function shasOfDefaultForm(target, producedDefault) {
|
|
1710
|
+
return [sha256(producedDefault), ownedSha(target, producedDefault)];
|
|
1711
|
+
}
|
|
1712
|
+
function shasOfExistingForm(target, template, present) {
|
|
1713
|
+
return [sha256(appendBlock(present, template, target)), ownedSha(target, existingForm(target, template))];
|
|
1714
|
+
}
|
|
1715
|
+
function establishVariant(input) {
|
|
1716
|
+
if (input.recordedVariant != null)
|
|
1717
|
+
return { variant: input.recordedVariant, evidence: "recorded" };
|
|
1718
|
+
if (input.existingTemplate == null)
|
|
1719
|
+
return { variant: "default", evidence: "sole-variant" };
|
|
1720
|
+
if (input.recordedSha == null)
|
|
1721
|
+
return null;
|
|
1722
|
+
const reconstructed = [
|
|
1723
|
+
["default", shasOfDefaultForm(input.target, input.producedDefault)],
|
|
1724
|
+
["existing", shasOfExistingForm(input.target, input.existingTemplate, input.present)]
|
|
1725
|
+
];
|
|
1726
|
+
const matched = reconstructed.filter(([, shas]) => shas.includes(input.recordedSha)).map(([variant]) => variant);
|
|
1727
|
+
return matched.length === 1 ? { variant: matched[0], evidence: "reconstructed" } : null;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/sync/replay.ts
|
|
1731
|
+
var NO_TREE_TO_PLAN_AGAINST = path15.join(tmpdir(), "mikoshi-construct-replay-renders-against-no-tree");
|
|
1732
|
+
function replayedGroups(manifest) {
|
|
1733
|
+
const preset = getPreset(manifest.preset);
|
|
1734
|
+
return [...preset.groups, ...aiGroups(manifest.ai), ...reviewGroups(manifest.review?.provider ?? "none")];
|
|
1735
|
+
}
|
|
1736
|
+
function varsRecordingMisses(manifest, version, facts, missed) {
|
|
1737
|
+
const asRecordedExceptTheRunningVersion = { ...manifest.vars, constructVersion: version };
|
|
1738
|
+
return new Proxy(asRecordedExceptTheRunningVersion, {
|
|
1739
|
+
get(target, key) {
|
|
1740
|
+
if (typeof key !== "string")
|
|
1741
|
+
return Reflect.get(target, key);
|
|
1742
|
+
const value = target[key];
|
|
1743
|
+
if (value != null)
|
|
1744
|
+
return value;
|
|
1745
|
+
const established = facts[key];
|
|
1746
|
+
if (established != null)
|
|
1747
|
+
return established;
|
|
1748
|
+
missed.add(key);
|
|
1749
|
+
return "";
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
function missingVariables(manifest, missed) {
|
|
1754
|
+
const names = [...missed].sort().join(", ");
|
|
1755
|
+
return `construct.json was written by construct ${manifest.construct} and carries no value for ${names}, which today's ${manifest.preset} templates render. Add ${missed.size === 1 ? "it" : "each of them"} under "vars" in construct.json and run the sync again.`;
|
|
1756
|
+
}
|
|
1757
|
+
function contentByTarget(ops) {
|
|
1758
|
+
return Object.fromEntries(ops.map((op) => [op.target, op.content]));
|
|
1759
|
+
}
|
|
1760
|
+
function producedByTemplates(manifest, vars, recorded) {
|
|
1761
|
+
const groups = replayedGroups(manifest);
|
|
1762
|
+
const plan = (emptyTarget) => planMaterialize(NO_TREE_TO_PLAN_AGAINST, groups, vars, { emptyTarget, ai: manifest.ai });
|
|
1763
|
+
const withoutSamples = plan(false);
|
|
1764
|
+
const kept = contentByTarget(withoutSamples.ops);
|
|
1765
|
+
if (withoutSamples.omittedGroups.length === 0)
|
|
1766
|
+
return { produced: kept, existingVariants: withoutSamples.existingVariants };
|
|
1767
|
+
const withSamples = plan(true);
|
|
1768
|
+
const sampled = contentByTarget(withSamples.ops);
|
|
1769
|
+
const sampleWasMaterialized = Object.keys(sampled).some((target) => !(target in kept) && recorded[target] != null);
|
|
1770
|
+
return sampleWasMaterialized ? { produced: sampled, existingVariants: withSamples.existingVariants } : { produced: kept, existingVariants: withoutSamples.existingVariants };
|
|
1771
|
+
}
|
|
1772
|
+
function establishedVariants(input) {
|
|
1773
|
+
const recordedVariant = recordedVariants(input.manifest);
|
|
1774
|
+
const established = {};
|
|
1775
|
+
for (const [target, producedDefault] of Object.entries(input.templates.produced)) {
|
|
1776
|
+
const present = input.present[target];
|
|
1777
|
+
if (strategyFor(target) !== "append-block" || present == null)
|
|
1778
|
+
continue;
|
|
1779
|
+
const variant = establishVariant({
|
|
1780
|
+
target,
|
|
1781
|
+
recordedVariant: recordedVariant[target] ?? null,
|
|
1782
|
+
recordedSha: input.recorded[target] ?? null,
|
|
1783
|
+
present,
|
|
1784
|
+
producedDefault,
|
|
1785
|
+
existingTemplate: input.templates.existingVariants[target] ?? null
|
|
1786
|
+
});
|
|
1787
|
+
if (variant != null)
|
|
1788
|
+
established[target] = variant;
|
|
1789
|
+
}
|
|
1790
|
+
return established;
|
|
1791
|
+
}
|
|
1792
|
+
function producedInTheVariantThatWroteIt(templates, variants) {
|
|
1793
|
+
const produced = { ...templates.produced };
|
|
1794
|
+
for (const [target, established] of Object.entries(variants)) {
|
|
1795
|
+
const template = templates.existingVariants[target];
|
|
1796
|
+
if (established.variant === "existing" && template != null)
|
|
1797
|
+
produced[target] = existingForm(target, template);
|
|
1798
|
+
}
|
|
1799
|
+
return produced;
|
|
1800
|
+
}
|
|
1801
|
+
function presentInTree(root, targets) {
|
|
1802
|
+
const present = {};
|
|
1803
|
+
for (const target of new Set(targets)) {
|
|
1804
|
+
const absolute = path15.join(root, target);
|
|
1805
|
+
if (existsSync13(absolute))
|
|
1806
|
+
present[target] = readFileSync13(absolute, "utf8");
|
|
1807
|
+
}
|
|
1808
|
+
return present;
|
|
1809
|
+
}
|
|
1810
|
+
function replay(input) {
|
|
1811
|
+
const missed = /* @__PURE__ */ new Set();
|
|
1812
|
+
const vars = varsRecordingMisses(input.manifest, input.version, input.facts, missed);
|
|
1813
|
+
const recorded = recordedShas(input.manifest);
|
|
1814
|
+
const templates = producedByTemplates(input.manifest, vars, recorded);
|
|
1815
|
+
if (missed.size > 0)
|
|
1816
|
+
throw new Error(missingVariables(input.manifest, missed));
|
|
1817
|
+
const present = presentInTree(input.root, [...Object.keys(recorded), ...Object.keys(templates.produced)]);
|
|
1818
|
+
const variants = establishedVariants({ manifest: input.manifest, recorded, present, templates });
|
|
1819
|
+
const produced = producedInTheVariantThatWroteIt(templates, variants);
|
|
1820
|
+
return {
|
|
1821
|
+
fromVersion: input.manifest.construct,
|
|
1822
|
+
toVersion: input.version,
|
|
1823
|
+
present,
|
|
1824
|
+
produced,
|
|
1825
|
+
variants,
|
|
1826
|
+
classifications: classifyRepository({ recorded, present, produced, variants })
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// src/sync/write.ts
|
|
1831
|
+
var PENDING_CLASSES = ["add", "update"];
|
|
1832
|
+
function isPending(classification) {
|
|
1833
|
+
return PENDING_CLASSES.includes(classification.class);
|
|
1834
|
+
}
|
|
1835
|
+
function contentToWrite(classification, input) {
|
|
1836
|
+
const produced = input.produced[classification.target] ?? "";
|
|
1837
|
+
const present = input.present[classification.target];
|
|
1838
|
+
if (classification.strategy !== "append-block" || present == null)
|
|
1839
|
+
return produced;
|
|
1840
|
+
return substituteBlock(present, produced, classification.target);
|
|
1841
|
+
}
|
|
1842
|
+
function variantWritten(classification, input) {
|
|
1843
|
+
if (classification.strategy !== "append-block")
|
|
1844
|
+
return null;
|
|
1845
|
+
return classification.variant?.variant ?? (input.present[classification.target] == null ? "default" : null);
|
|
1846
|
+
}
|
|
1847
|
+
function plannedWrite(classification, input) {
|
|
1848
|
+
const content = contentToWrite(classification, input);
|
|
1849
|
+
return {
|
|
1850
|
+
target: classification.target,
|
|
1851
|
+
strategy: classification.strategy,
|
|
1852
|
+
content,
|
|
1853
|
+
ownedSha: ownedSha(classification.target, content),
|
|
1854
|
+
variant: variantWritten(classification, input),
|
|
1855
|
+
writeEffect: classification.writeEffect
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
function planWrites(input) {
|
|
1859
|
+
const writes = [];
|
|
1860
|
+
const refused = [];
|
|
1861
|
+
for (const classification of input.classifications) {
|
|
1862
|
+
if (isWritable(classification))
|
|
1863
|
+
writes.push(plannedWrite(classification, input));
|
|
1864
|
+
else if (isPending(classification))
|
|
1865
|
+
refused.push(classification);
|
|
1866
|
+
}
|
|
1867
|
+
return { writes, refused };
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// src/commands/doctor/version-gap.ts
|
|
1871
|
+
function versionGap(root, manifest, version) {
|
|
1872
|
+
try {
|
|
1873
|
+
const { fromVersion, toVersion, classifications } = replay({ root, manifest, version, facts: factsTheRepositoryEstablishes(root) });
|
|
1874
|
+
return { materializedBy: fromVersion, readBy: toVersion, pending: classifications.filter(isPending).length };
|
|
1875
|
+
} catch {
|
|
1876
|
+
return { materializedBy: manifest.construct, readBy: version, pending: null };
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// src/commands/doctor/report.ts
|
|
1881
|
+
function checkLine(ui2, check) {
|
|
1882
|
+
return ` ${check.id.padEnd(16)} ${check.level} ${check.state.padEnd(8)} ${ui2.theme.dim(check.evidence)}`;
|
|
1883
|
+
}
|
|
1884
|
+
function printChecks(ui2, checks) {
|
|
1885
|
+
ui2.line();
|
|
1886
|
+
ui2.line(ui2.theme.accent(ui2.lore.enforcement));
|
|
1887
|
+
for (const check of checks)
|
|
1888
|
+
ui2.line(checkLine(ui2, check));
|
|
1889
|
+
}
|
|
1890
|
+
function printProvenance(ui2, provenance) {
|
|
1891
|
+
const authored = constructAuthored(provenance);
|
|
1892
|
+
if (authored.length === 0)
|
|
1893
|
+
return;
|
|
1894
|
+
ui2.line();
|
|
1895
|
+
ui2.line(ui2.theme.accent(ui2.lore.provenance));
|
|
1896
|
+
for (const reading of authored)
|
|
1897
|
+
ui2.line(` ${reading.marker.padEnd(20)} ${ui2.theme.dim(reading.file)}`);
|
|
1898
|
+
ui2.line(ui2.theme.dim(` ${ui2.lore.stillConstructAuthored(authored.length)}`));
|
|
1899
|
+
}
|
|
1900
|
+
function gapReading(ui2, gap) {
|
|
1901
|
+
if (gap.pending == null)
|
|
1902
|
+
return ui2.lore.baselineGapUnknown;
|
|
1903
|
+
return gap.pending === 0 ? ui2.lore.baselineCurrent : ui2.lore.baselineMoved(gap.pending);
|
|
1904
|
+
}
|
|
1905
|
+
function printVersionGap(ui2, gap) {
|
|
1906
|
+
ui2.line(ui2.theme.dim(` ${ui2.lore.syncVersionGap(gap.materializedBy, gap.readBy)}`));
|
|
1907
|
+
ui2.line(ui2.theme.dim(` ${gapReading(ui2, gap)}`));
|
|
1908
|
+
}
|
|
1909
|
+
function printWeakestLink(ui2, weakest) {
|
|
1910
|
+
ui2.line();
|
|
1911
|
+
if (weakest == null) {
|
|
1912
|
+
ui2.line(ui2.theme.bold(ui2.lore.weakestLinkNone));
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
const lift = ui2.lore.levelLift[weakest.level];
|
|
1916
|
+
ui2.line(`${ui2.theme.bold(ui2.lore.weakestLink(weakest.id, weakest.level))}${lift == null ? "" : ui2.theme.dim(` \u2014 ${lift}`)}`);
|
|
1917
|
+
}
|
|
1918
|
+
function printDoctor(ui2, result) {
|
|
1919
|
+
if (result == null) {
|
|
1920
|
+
ui2.flatline("No construct.json here. Run `construct init` first.");
|
|
1921
|
+
return 1;
|
|
1922
|
+
}
|
|
1923
|
+
if (result.harnessProblems.length > 0)
|
|
1924
|
+
ui2.glitch("Harness is broken.", result.harnessProblems);
|
|
1925
|
+
if (result.missingFiles.length > 0)
|
|
1926
|
+
ui2.glitch("Baseline files are missing.", result.missingFiles);
|
|
1927
|
+
if (result.missingDiscovery.length > 0)
|
|
1928
|
+
ui2.glitch(ui2.lore.discoveryIncomplete, ["", "Missing:", ...result.missingDiscovery.map((marker) => ` ${marker}`), "", "Run: claude \u2192 /construct-discover"]);
|
|
1929
|
+
if (result.warnings.length > 0)
|
|
1930
|
+
ui2.glitch(ui2.lore.typecheckCaveat, result.warnings);
|
|
1931
|
+
if (result.modifiedFiles.length > 0)
|
|
1932
|
+
ui2.line(ui2.theme.dim(` ${result.modifiedFiles.length} baseline files modified since init (expected once the project evolves).`));
|
|
1933
|
+
printVersionGap(ui2, result.versionGap);
|
|
1934
|
+
if (result.ok)
|
|
1935
|
+
ui2.ok(ui2.lore.stable);
|
|
1936
|
+
printProvenance(ui2, result.provenance);
|
|
1937
|
+
printChecks(ui2, result.checks);
|
|
1938
|
+
printWeakestLink(ui2, result.weakestLink);
|
|
1939
|
+
return result.ok ? 0 : 1;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// src/commands/doctor/index.ts
|
|
1943
|
+
function runDoctor(root, version = VERSION) {
|
|
1944
|
+
const manifest = readManifest(root);
|
|
1945
|
+
if (manifest == null)
|
|
1946
|
+
return null;
|
|
1947
|
+
const evidence = gatherEvidence(root, manifest);
|
|
1948
|
+
const baseline = baselineVerdict(root, manifest);
|
|
1949
|
+
const problems = harnessProblems(root, manifest, evidence.harness);
|
|
1950
|
+
const checks = [
|
|
1951
|
+
lintPolicyCheck(evidence),
|
|
1952
|
+
constructTestsCheck(evidence),
|
|
1953
|
+
ciCheck(evidence),
|
|
1954
|
+
hookCheck(evidence),
|
|
1955
|
+
redGateCheck(evidence)
|
|
1956
|
+
];
|
|
1957
|
+
return {
|
|
1958
|
+
ok: baseline.missingFiles.length === 0 && problems.length === 0,
|
|
1959
|
+
missingFiles: baseline.missingFiles,
|
|
1960
|
+
modifiedFiles: baseline.modifiedFiles,
|
|
1961
|
+
missingDiscovery: missingDiscovery(root, manifest),
|
|
1962
|
+
provenance: discoveryProvenance(root, manifest),
|
|
1963
|
+
harnessProblems: problems,
|
|
1964
|
+
warnings: typecheckWarnings(manifest.preset, evidence),
|
|
1965
|
+
checks,
|
|
1966
|
+
weakestLink: weakestLink(checks),
|
|
1967
|
+
versionGap: versionGap(root, manifest, version)
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// src/commands/init.ts
|
|
1972
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
1973
|
+
import path21 from "path";
|
|
1974
|
+
|
|
1975
|
+
// src/detect/index.ts
|
|
1976
|
+
import { existsSync as existsSync17 } from "fs";
|
|
1977
|
+
import path19 from "path";
|
|
1978
|
+
import process3 from "process";
|
|
1979
|
+
|
|
1980
|
+
// src/detect/layout.ts
|
|
1981
|
+
import { existsSync as existsSync15, readdirSync as readdirSync7, readFileSync as readFileSync15, statSync as statSync4 } from "fs";
|
|
1982
|
+
import path17 from "path";
|
|
1983
|
+
|
|
1984
|
+
// src/detect/workspaces.ts
|
|
1985
|
+
import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
|
|
1986
|
+
import path16 from "path";
|
|
1987
|
+
var TOP_LEVEL_PACKAGES_KEY = /^packages:(.*)$/m;
|
|
1988
|
+
var EMPTY_FLOW_SEQUENCE = /^\[\s*\]$/;
|
|
1989
|
+
var BLOCK_SEQUENCE_ENTRY = /^[ \t]+-[ \t]*\S/;
|
|
1990
|
+
function readIfPresent(file) {
|
|
1991
|
+
if (!existsSync14(file))
|
|
1992
|
+
return null;
|
|
1993
|
+
try {
|
|
1994
|
+
return readFileSync14(file, "utf8");
|
|
1995
|
+
} catch {
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
function startsBlockSequence(rest) {
|
|
2000
|
+
for (const line of rest.split("\n")) {
|
|
2001
|
+
if (line.trim() === "" || line.trimStart().startsWith("#"))
|
|
2002
|
+
continue;
|
|
2003
|
+
return BLOCK_SEQUENCE_ENTRY.test(line);
|
|
2004
|
+
}
|
|
2005
|
+
return false;
|
|
2006
|
+
}
|
|
2007
|
+
function declaresPnpmPackages(dir) {
|
|
2008
|
+
const content = readIfPresent(path16.join(dir, "pnpm-workspace.yaml"));
|
|
2009
|
+
if (content == null)
|
|
2010
|
+
return false;
|
|
2011
|
+
const match = TOP_LEVEL_PACKAGES_KEY.exec(content);
|
|
2012
|
+
if (match == null)
|
|
2013
|
+
return false;
|
|
2014
|
+
const inline = match[1].trim();
|
|
2015
|
+
if (inline.startsWith("["))
|
|
2016
|
+
return !EMPTY_FLOW_SEQUENCE.test(inline);
|
|
2017
|
+
if (inline !== "")
|
|
2018
|
+
return false;
|
|
2019
|
+
return startsBlockSequence(content.slice(match.index + match[0].length));
|
|
2020
|
+
}
|
|
2021
|
+
function declaresNpmWorkspaces(dir) {
|
|
2022
|
+
const content = readIfPresent(path16.join(dir, "package.json"));
|
|
2023
|
+
if (content == null)
|
|
2024
|
+
return false;
|
|
2025
|
+
let workspaces;
|
|
2026
|
+
try {
|
|
2027
|
+
({ workspaces } = JSON.parse(content));
|
|
2028
|
+
} catch {
|
|
2029
|
+
return false;
|
|
2030
|
+
}
|
|
2031
|
+
if (Array.isArray(workspaces))
|
|
2032
|
+
return workspaces.length > 0;
|
|
2033
|
+
const packages = workspaces?.packages;
|
|
2034
|
+
return Array.isArray(packages) && packages.length > 0;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// src/detect/layout.ts
|
|
2038
|
+
var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
|
|
2039
|
+
function isEmptyDir(dir) {
|
|
2040
|
+
if (!existsSync15(dir))
|
|
2041
|
+
return true;
|
|
2042
|
+
return readdirSync7(dir).every((entry) => IGNORED_ENTRIES.has(entry));
|
|
2043
|
+
}
|
|
2044
|
+
function detectMonorepoTools(dir) {
|
|
2045
|
+
const tools = [];
|
|
2046
|
+
if (declaresPnpmPackages(dir))
|
|
2047
|
+
tools.push("pnpm-workspace");
|
|
2048
|
+
if (declaresNpmWorkspaces(dir))
|
|
2049
|
+
tools.push("npm-workspaces");
|
|
2050
|
+
if (existsSync15(path17.join(dir, "turbo.json")))
|
|
2051
|
+
tools.push("turbo");
|
|
2052
|
+
if (existsSync15(path17.join(dir, "nx.json")))
|
|
2053
|
+
tools.push("nx");
|
|
2054
|
+
return tools;
|
|
2055
|
+
}
|
|
2056
|
+
function detectWorkspaceDirs(dir) {
|
|
2057
|
+
return ["apps", "packages", "libs", "services"].filter((name) => existsSync15(path17.join(dir, name)) && statSync4(path17.join(dir, name)).isDirectory());
|
|
2058
|
+
}
|
|
2059
|
+
function packageName(dir) {
|
|
2060
|
+
try {
|
|
2061
|
+
const parsed = JSON.parse(readFileSync15(path17.join(dir, "package.json"), "utf8"));
|
|
2062
|
+
return typeof parsed.name === "string" && parsed.name !== "" ? parsed.name : null;
|
|
2063
|
+
} catch {
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
function detectWorkspacePackages(root, workspaceDirs) {
|
|
2068
|
+
return workspaceDirs.flatMap((parent) => readdirSync7(path17.join(root, parent)).sort().map((entry) => `${parent}/${entry}`).filter((dir) => existsSync15(path17.join(root, dir, "package.json"))).map((dir) => ({ dir, name: packageName(path17.join(root, dir)) ?? dir.split("/").at(-1) ?? dir })));
|
|
2069
|
+
}
|
|
2070
|
+
function detectLayout(dir, monorepoTools, workspaceDirs, hasSrc) {
|
|
2071
|
+
if (isEmptyDir(dir))
|
|
2072
|
+
return "empty";
|
|
2073
|
+
if (monorepoTools.length > 0 || workspaceDirs.length > 0)
|
|
2074
|
+
return "monorepo";
|
|
2075
|
+
if (hasSrc || existsSync15(path17.join(dir, "package.json")))
|
|
2076
|
+
return "single";
|
|
2077
|
+
return "unknown";
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// src/detect/package-manager.ts
|
|
2081
|
+
import { execFileSync } from "child_process";
|
|
2082
|
+
import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
|
|
2083
|
+
import path18 from "path";
|
|
2084
|
+
var LOCKFILES = [
|
|
2085
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
2086
|
+
["bun.lockb", "bun"],
|
|
2087
|
+
["bun.lock", "bun"],
|
|
2088
|
+
["yarn.lock", "yarn"],
|
|
2089
|
+
["package-lock.json", "npm"]
|
|
2090
|
+
];
|
|
2091
|
+
function fromPackageManagerField(dir) {
|
|
2092
|
+
const manifest = path18.join(dir, "package.json");
|
|
2093
|
+
if (!existsSync16(manifest))
|
|
2094
|
+
return null;
|
|
2095
|
+
try {
|
|
2096
|
+
const parsed = JSON.parse(readFileSync16(manifest, "utf8"));
|
|
2097
|
+
const name = parsed.packageManager?.split("@")[0];
|
|
2098
|
+
return name === "pnpm" || name === "npm" || name === "yarn" || name === "bun" ? name : null;
|
|
2099
|
+
} catch {
|
|
2100
|
+
return null;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
function detectPackageManager(dir) {
|
|
2104
|
+
const declared = fromPackageManagerField(dir);
|
|
2105
|
+
if (declared != null)
|
|
2106
|
+
return declared;
|
|
2107
|
+
for (const [lockfile, manager] of LOCKFILES) {
|
|
2108
|
+
if (existsSync16(path18.join(dir, lockfile)))
|
|
2109
|
+
return manager;
|
|
2110
|
+
}
|
|
2111
|
+
return existsSync16(path18.join(dir, "package.json")) ? "npm" : "none";
|
|
842
2112
|
}
|
|
843
|
-
|
|
844
|
-
|
|
2113
|
+
var pnpmVersionCache;
|
|
2114
|
+
function detectPnpmVersion() {
|
|
2115
|
+
if (pnpmVersionCache !== void 0)
|
|
2116
|
+
return pnpmVersionCache;
|
|
2117
|
+
try {
|
|
2118
|
+
pnpmVersionCache = execFileSync("pnpm", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
2119
|
+
} catch {
|
|
2120
|
+
pnpmVersionCache = null;
|
|
2121
|
+
}
|
|
2122
|
+
return pnpmVersionCache;
|
|
845
2123
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const lines = packages.map((pkg) => ` ${quote(pkg.dir)}: [${allowedFor(pkg).map(quote).join(", ")}],`);
|
|
2124
|
+
|
|
2125
|
+
// src/detect/index.ts
|
|
2126
|
+
function detect(dir) {
|
|
2127
|
+
const root = path19.resolve(dir);
|
|
2128
|
+
const monorepoTools = detectMonorepoTools(root);
|
|
2129
|
+
const workspaceDirs = detectWorkspaceDirs(root);
|
|
2130
|
+
const hasSrc = existsSync17(path19.join(root, "src"));
|
|
854
2131
|
return {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
2132
|
+
dir: root,
|
|
2133
|
+
packageManager: detectPackageManager(root),
|
|
2134
|
+
pnpmVersion: detectPnpmVersion(),
|
|
2135
|
+
layout: detectLayout(root, monorepoTools, workspaceDirs, hasSrc),
|
|
2136
|
+
monorepoTools,
|
|
2137
|
+
workspaceDirs,
|
|
2138
|
+
workspacePackages: detectWorkspacePackages(root, workspaceDirs),
|
|
2139
|
+
hasSrc,
|
|
2140
|
+
nodeMajor: Number(process3.versions.node.split(".")[0]),
|
|
2141
|
+
existing: detectExisting(root)
|
|
859
2142
|
};
|
|
860
2143
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
],
|
|
875
|
-
contracts: true,
|
|
876
|
-
available: true,
|
|
877
|
-
vars: () => ({
|
|
878
|
-
contractPath: "contracts/api/openapi.yaml",
|
|
879
|
-
contractTypesOutput: "src/contracts/openapi.ts",
|
|
880
|
-
contractTypesImport: "./openapi.js",
|
|
881
|
-
contractPathFromConfig: "../contracts/api/openapi.yaml",
|
|
882
|
-
appRoot: ""
|
|
883
|
-
})
|
|
884
|
-
},
|
|
885
|
-
"node-frontend": {
|
|
886
|
-
id: "node-frontend",
|
|
887
|
-
label: "Node.js frontend",
|
|
888
|
-
description: "Vite + TypeScript, platform CSS rules, composition root, harness; no API contract",
|
|
889
|
-
groups: [
|
|
890
|
-
"base",
|
|
891
|
-
"harness",
|
|
892
|
-
{ group: "presets/node-frontend/sample", onlyWhenEmpty: true },
|
|
893
|
-
"presets/node-frontend/baseline"
|
|
894
|
-
],
|
|
895
|
-
contracts: false,
|
|
896
|
-
available: true,
|
|
897
|
-
vars: () => ({
|
|
898
|
-
contractPath: "",
|
|
899
|
-
contractTypesOutput: ""
|
|
900
|
-
})
|
|
901
|
-
},
|
|
902
|
-
"node-library": {
|
|
903
|
-
id: "node-library",
|
|
904
|
-
label: "Node.js library or CLI",
|
|
905
|
-
description: "TypeScript package with no HTTP contract: architecture policy, composition models, harness",
|
|
906
|
-
groups: ["base", "harness"],
|
|
907
|
-
contracts: false,
|
|
908
|
-
available: true,
|
|
909
|
-
vars: () => ({
|
|
910
|
-
contractPath: "",
|
|
911
|
-
contractTypesOutput: ""
|
|
912
|
-
})
|
|
913
|
-
},
|
|
914
|
-
"monorepo": {
|
|
915
|
-
id: "monorepo",
|
|
916
|
-
label: "pnpm monorepo",
|
|
917
|
-
description: "apps/* + packages/*, catalog:, contract types in packages/shared, dependency policy in lint",
|
|
918
|
-
groups: [
|
|
919
|
-
"base",
|
|
920
|
-
"harness",
|
|
921
|
-
HTTP_CONTRACT,
|
|
922
|
-
{ group: EXPRESS_APP, into: "apps/api", onlyWhenEmpty: true },
|
|
923
|
-
{ group: EXPRESS_REPO, onlyWhenEmpty: true },
|
|
924
|
-
{ group: "presets/monorepo/sample", onlyWhenEmpty: true },
|
|
925
|
-
"presets/monorepo/baseline"
|
|
926
|
-
],
|
|
927
|
-
contracts: true,
|
|
928
|
-
available: true,
|
|
929
|
-
vars: (report, projectName) => {
|
|
930
|
-
const detected = report.workspacePackages;
|
|
931
|
-
const packages = detected.length > 0 ? detected : sampleWorkspace(`@${projectName}`);
|
|
932
|
-
return {
|
|
933
|
-
contractPath: "contracts/api/openapi.yaml",
|
|
934
|
-
contractTypesOutput: "packages/shared/src/api/openapi.ts",
|
|
935
|
-
contractTypesImport: `@${projectName}/shared`,
|
|
936
|
-
contractPathFromConfig: "../../../contracts/api/openapi.yaml",
|
|
937
|
-
appRoot: "apps/api/",
|
|
938
|
-
...renderWorkspacePolicy(packages, detected.length === 0)
|
|
939
|
-
};
|
|
940
|
-
}
|
|
2144
|
+
|
|
2145
|
+
// src/materialize/apply.ts
|
|
2146
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2147
|
+
import path20 from "path";
|
|
2148
|
+
function applyPlan(root, ops) {
|
|
2149
|
+
const written = [];
|
|
2150
|
+
for (const op of ops) {
|
|
2151
|
+
if (op.action === "skip")
|
|
2152
|
+
continue;
|
|
2153
|
+
const absolute = path20.join(root, op.target);
|
|
2154
|
+
mkdirSync(path20.dirname(absolute), { recursive: true });
|
|
2155
|
+
writeFileSync2(absolute, op.content);
|
|
2156
|
+
written.push(op);
|
|
941
2157
|
}
|
|
942
|
-
|
|
943
|
-
var PRESET_IDS = Object.keys(PRESETS);
|
|
944
|
-
var PRESET_LIST = Object.values(PRESETS);
|
|
945
|
-
function isPresetId(value) {
|
|
946
|
-
return value in PRESETS;
|
|
947
|
-
}
|
|
948
|
-
function getPreset(id) {
|
|
949
|
-
return PRESETS[id];
|
|
950
|
-
}
|
|
951
|
-
function aiGroups(target) {
|
|
952
|
-
return target === "both" ? ["ai/shared", "ai/claude", "ai/cursor"] : ["ai/shared", `ai/${target}`];
|
|
953
|
-
}
|
|
954
|
-
var DEFAULT_REVIEW_MODEL = "claude-sonnet-5";
|
|
955
|
-
function reviewGroups(provider) {
|
|
956
|
-
return provider === "claude" ? ["ai/review"] : [];
|
|
957
|
-
}
|
|
958
|
-
function defaultProjectName(dir) {
|
|
959
|
-
const base = dir.split(/[\\/]/).filter(Boolean).at(-1) ?? "project";
|
|
960
|
-
return base.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
2158
|
+
return written;
|
|
961
2159
|
}
|
|
962
2160
|
|
|
963
2161
|
// src/ui/prompts.ts
|
|
@@ -1030,24 +2228,6 @@ function createClackPrompter(lore, streams = {}) {
|
|
|
1030
2228
|
};
|
|
1031
2229
|
}
|
|
1032
2230
|
|
|
1033
|
-
// src/version.ts
|
|
1034
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
1035
|
-
import path12 from "path";
|
|
1036
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1037
|
-
var HERE2 = path12.dirname(fileURLToPath2(import.meta.url));
|
|
1038
|
-
function readVersion() {
|
|
1039
|
-
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
1040
|
-
try {
|
|
1041
|
-
const parsed = JSON.parse(readFileSync8(path12.resolve(HERE2, candidate), "utf8"));
|
|
1042
|
-
if (parsed.name === "mikoshi-construct" && parsed.version != null)
|
|
1043
|
-
return parsed.version;
|
|
1044
|
-
} catch {
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
return "0.0.0";
|
|
1048
|
-
}
|
|
1049
|
-
var VERSION = readVersion();
|
|
1050
|
-
|
|
1051
2231
|
// src/commands/soulkill.ts
|
|
1052
2232
|
function yesNo(value) {
|
|
1053
2233
|
return value ? "yes" : "no";
|
|
@@ -1071,6 +2251,13 @@ function printDetectReport(ui2, report) {
|
|
|
1071
2251
|
|
|
1072
2252
|
// src/commands/init.ts
|
|
1073
2253
|
var CANCELLED = /* @__PURE__ */ Symbol("cancelled");
|
|
2254
|
+
var RESTATED_BY_THE_RUNNING_BINARY = ["constructVersion"];
|
|
2255
|
+
function varsThisRunChanged(previous, vars) {
|
|
2256
|
+
return Object.entries(vars).filter(([name]) => !RESTATED_BY_THE_RUNNING_BINARY.includes(name)).flatMap(([name, to]) => {
|
|
2257
|
+
const from = previous.vars[name];
|
|
2258
|
+
return from == null || from === to ? [] : [{ name, from, to }];
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
1074
2261
|
function aborted(skipped = [], conflicts = []) {
|
|
1075
2262
|
return { status: "aborted", written: [], skipped, conflicts };
|
|
1076
2263
|
}
|
|
@@ -1138,7 +2325,7 @@ async function askChoices(ui2, options, report, root, prompter) {
|
|
|
1138
2325
|
return { presetId, ai, projectName, review };
|
|
1139
2326
|
}
|
|
1140
2327
|
async function runInit(ui2, options, prompter) {
|
|
1141
|
-
const root =
|
|
2328
|
+
const root = path21.resolve(options.dir);
|
|
1142
2329
|
mkdirSync2(root, { recursive: true });
|
|
1143
2330
|
if (!options.yes && prompter == null) {
|
|
1144
2331
|
ui2.glitch(ui2.lore.needsTerminal);
|
|
@@ -1176,7 +2363,7 @@ async function runInit(ui2, options, prompter) {
|
|
|
1176
2363
|
contracts: preset.contracts ? "true" : "false",
|
|
1177
2364
|
contractPath: "contracts/api/openapi.yaml",
|
|
1178
2365
|
contractTypesOutput: "src/contracts/openapi.ts",
|
|
1179
|
-
compositionDir: report.existing.compositionDir ??
|
|
2366
|
+
compositionDir: report.existing.compositionDir ?? DEFAULT_COMPOSITION_DIR,
|
|
1180
2367
|
harnessCommand: "pnpm run quality",
|
|
1181
2368
|
packageManager: "pnpm",
|
|
1182
2369
|
pnpmVersion: report.pnpmVersion ?? "",
|
|
@@ -1202,7 +2389,7 @@ async function runInit(ui2, options, prompter) {
|
|
|
1202
2389
|
ui2.line();
|
|
1203
2390
|
if (plan.omittedGroups.length > 0)
|
|
1204
2391
|
ui2.line(ui2.theme.dim(` ${ui2.lore.sampleOmitted}`));
|
|
1205
|
-
if (report.existing.compositionDir != null && report.existing.compositionDir !==
|
|
2392
|
+
if (report.existing.compositionDir != null && report.existing.compositionDir !== DEFAULT_COMPOSITION_DIR)
|
|
1206
2393
|
ui2.line(ui2.theme.dim(` Existing composition models found at ${report.existing.compositionDir}/ \u2014 kept there, not moved.`));
|
|
1207
2394
|
if (plan.conflicts.length > 0)
|
|
1208
2395
|
ui2.glitch("Existing values kept; review these keys by hand:", plan.conflicts);
|
|
@@ -1215,7 +2402,17 @@ async function runInit(ui2, options, prompter) {
|
|
|
1215
2402
|
if (interactive != null && await interactive.confirm(ui2.lore.confirm) !== true)
|
|
1216
2403
|
return aborted(skipped, plan.conflicts);
|
|
1217
2404
|
const written = applyPlan(root, plan.ops);
|
|
1218
|
-
|
|
2405
|
+
const previous = readManifest(root);
|
|
2406
|
+
const manifest = buildManifest({ version: VERSION, preset: presetId, ai, review, vars, written, contracts: preset.contracts, previous });
|
|
2407
|
+
writeManifest(root, manifest);
|
|
2408
|
+
if (previous != null) {
|
|
2409
|
+
const carriedOver = Object.keys(previous.files).filter((target) => !written.some((op) => op.target === target)).length;
|
|
2410
|
+
const added = written.filter((op) => previous.files[op.target] == null).length;
|
|
2411
|
+
ui2.line(ui2.theme.dim(` ${ui2.lore.recordCarriedOver(carriedOver, added)}`));
|
|
2412
|
+
const changed = varsThisRunChanged(previous, vars);
|
|
2413
|
+
if (changed.length > 0)
|
|
2414
|
+
ui2.line(ui2.theme.dim(` ${ui2.lore.recordVarsChanged(changed)}`));
|
|
2415
|
+
}
|
|
1219
2416
|
ui2.phase(4, 4, "\u2705", ui2.lore.phaseOnline);
|
|
1220
2417
|
ui2.tree([
|
|
1221
2418
|
["Written", `${written.length} files`],
|
|
@@ -1225,10 +2422,202 @@ async function runInit(ui2, options, prompter) {
|
|
|
1225
2422
|
return { status: "done", written: written.map((op) => op.target), skipped, conflicts: plan.conflicts };
|
|
1226
2423
|
}
|
|
1227
2424
|
|
|
2425
|
+
// src/commands/sync/report.ts
|
|
2426
|
+
var SYNC_EXIT = {
|
|
2427
|
+
upToDate: 0,
|
|
2428
|
+
noManifest: 1,
|
|
2429
|
+
pending: 2
|
|
2430
|
+
};
|
|
2431
|
+
var SYNC_APPLY_EXIT = {
|
|
2432
|
+
written: 0,
|
|
2433
|
+
noManifest: 1,
|
|
2434
|
+
refused: 2
|
|
2435
|
+
};
|
|
2436
|
+
var LISTED_CLASSES = ["add", "update", "conflict", "unknown", "removed", "orphaned"];
|
|
2437
|
+
var CLASS_COLUMN = Math.max(...PATH_CLASSES.map((value) => value.length)) + 2;
|
|
2438
|
+
function pendingCount(report) {
|
|
2439
|
+
return PENDING_CLASSES.reduce((total, value) => total + report.counts[value], 0);
|
|
2440
|
+
}
|
|
2441
|
+
function syncExit(report) {
|
|
2442
|
+
if (report == null)
|
|
2443
|
+
return SYNC_EXIT.noManifest;
|
|
2444
|
+
return pendingCount(report) === 0 ? SYNC_EXIT.upToDate : SYNC_EXIT.pending;
|
|
2445
|
+
}
|
|
2446
|
+
function actionableKeys(entry) {
|
|
2447
|
+
return entry.keys.filter((key) => key.class !== "keep").map((key) => `${key.key} (${key.class})`);
|
|
2448
|
+
}
|
|
2449
|
+
function syncJson(report) {
|
|
2450
|
+
return {
|
|
2451
|
+
fromVersion: report.fromVersion,
|
|
2452
|
+
toVersion: report.toVersion,
|
|
2453
|
+
counts: report.counts,
|
|
2454
|
+
paths: report.classifications.map((entry) => ({
|
|
2455
|
+
target: entry.target,
|
|
2456
|
+
class: entry.class,
|
|
2457
|
+
strategy: entry.strategy,
|
|
2458
|
+
...entry.keys.length === 0 ? {} : { keys: entry.keys },
|
|
2459
|
+
...entry.variant == null ? {} : { variant: entry.variant.variant, variantEvidence: entry.variant.evidence },
|
|
2460
|
+
...entry.shape == null ? {} : { shape: entry.shape },
|
|
2461
|
+
...entry.writeEffect == null ? {} : { writeEffect: entry.writeEffect }
|
|
2462
|
+
}))
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
function note(ui2, entry) {
|
|
2466
|
+
if (entry.strategy === "merge-json") {
|
|
2467
|
+
const keys = actionableKeys(entry);
|
|
2468
|
+
return keys.length === 0 ? "" : ui2.lore.syncMergedKeys(keys);
|
|
2469
|
+
}
|
|
2470
|
+
if (entry.class === "unknown")
|
|
2471
|
+
return ui2.lore.syncVariantUnknown(entry.shape ?? "");
|
|
2472
|
+
return entry.writeEffect == null ? "" : ui2.lore.syncWriteEffect[entry.writeEffect] ?? "";
|
|
2473
|
+
}
|
|
2474
|
+
var COUNT_ORDER = ["add", "update", "conflict", "unknown", "removed", "orphaned", "keep", "foreign"];
|
|
2475
|
+
function printCounts(ui2, report) {
|
|
2476
|
+
ui2.line(ui2.theme.accent(ui2.lore.syncClasses));
|
|
2477
|
+
for (const value of COUNT_ORDER) {
|
|
2478
|
+
if (report.counts[value] === 0)
|
|
2479
|
+
continue;
|
|
2480
|
+
const meaning = ui2.lore.syncClassMeaning[value] ?? "";
|
|
2481
|
+
ui2.line(` ${value.padEnd(CLASS_COLUMN)}${String(report.counts[value]).padStart(3)} ${ui2.theme.dim(meaning)}`);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
function printPaths(ui2, report) {
|
|
2485
|
+
for (const value of LISTED_CLASSES) {
|
|
2486
|
+
const entries = report.classifications.filter((entry) => entry.class === value);
|
|
2487
|
+
if (entries.length === 0)
|
|
2488
|
+
continue;
|
|
2489
|
+
ui2.line();
|
|
2490
|
+
ui2.line(`${ui2.theme.accent(value)} ${ui2.theme.dim(`\u2014 ${ui2.lore.syncClassMeaning[value] ?? ""}`)}`);
|
|
2491
|
+
for (const entry of entries) {
|
|
2492
|
+
const detail = note(ui2, entry);
|
|
2493
|
+
ui2.line(` ${entry.target}${detail === "" ? "" : ui2.theme.dim(` \u2014 ${detail}`)}`);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
function printMergedNote(ui2, report) {
|
|
2498
|
+
const listed = report.classifications.filter((entry) => LISTED_CLASSES.includes(entry.class));
|
|
2499
|
+
if (!listed.some((entry) => entry.strategy === "merge-json"))
|
|
2500
|
+
return;
|
|
2501
|
+
ui2.line();
|
|
2502
|
+
ui2.line(ui2.theme.dim(ui2.lore.syncMergedNotWritten));
|
|
2503
|
+
}
|
|
2504
|
+
function printSync(ui2, report) {
|
|
2505
|
+
if (report == null) {
|
|
2506
|
+
ui2.flatline(ui2.lore.syncNoManifest);
|
|
2507
|
+
return SYNC_EXIT.noManifest;
|
|
2508
|
+
}
|
|
2509
|
+
ui2.line(ui2.theme.accent(ui2.theme.bold(ui2.lore.syncTitle)));
|
|
2510
|
+
ui2.line(ui2.theme.bold(ui2.lore.syncVersionGap(report.fromVersion, report.toVersion)));
|
|
2511
|
+
ui2.line();
|
|
2512
|
+
printCounts(ui2, report);
|
|
2513
|
+
printPaths(ui2, report);
|
|
2514
|
+
printMergedNote(ui2, report);
|
|
2515
|
+
const pending = pendingCount(report);
|
|
2516
|
+
ui2.line();
|
|
2517
|
+
ui2.line(pending === 0 ? ui2.lore.syncNothingToWrite : ui2.lore.syncPending(pending));
|
|
2518
|
+
return syncExit(report);
|
|
2519
|
+
}
|
|
2520
|
+
function syncApplyExit(result) {
|
|
2521
|
+
if (result == null)
|
|
2522
|
+
return SYNC_APPLY_EXIT.noManifest;
|
|
2523
|
+
return result.refused.length === 0 ? SYNC_APPLY_EXIT.written : SYNC_APPLY_EXIT.refused;
|
|
2524
|
+
}
|
|
2525
|
+
function syncApplyJson(result) {
|
|
2526
|
+
return {
|
|
2527
|
+
...syncJson(result.report),
|
|
2528
|
+
written: result.written,
|
|
2529
|
+
pending: result.refused.map((entry) => entry.target),
|
|
2530
|
+
ranAt: result.ranAt
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
function printWritten(ui2, result) {
|
|
2534
|
+
if (result.written.length === 0)
|
|
2535
|
+
return;
|
|
2536
|
+
const byTarget = new Map(result.report.classifications.map((entry) => [entry.target, entry]));
|
|
2537
|
+
ui2.line();
|
|
2538
|
+
ui2.line(ui2.theme.accent(ui2.lore.syncApplyWritten));
|
|
2539
|
+
for (const target of result.written) {
|
|
2540
|
+
const entry = byTarget.get(target);
|
|
2541
|
+
const detail = entry == null ? "" : note(ui2, entry);
|
|
2542
|
+
ui2.line(` ${target}${detail === "" ? "" : ui2.theme.dim(` \u2014 ${detail}`)}`);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
function printUnknownVariants(ui2, result) {
|
|
2546
|
+
const unknown = result.report.classifications.filter((entry) => entry.class === "unknown");
|
|
2547
|
+
if (unknown.length === 0)
|
|
2548
|
+
return;
|
|
2549
|
+
ui2.line();
|
|
2550
|
+
ui2.line(ui2.theme.accent(ui2.lore.syncApplyUnknown));
|
|
2551
|
+
for (const entry of unknown)
|
|
2552
|
+
ui2.line(` ${entry.target}${ui2.theme.dim(` \u2014 ${note(ui2, entry)}`)}`);
|
|
2553
|
+
}
|
|
2554
|
+
function printRefused(ui2, result) {
|
|
2555
|
+
if (result.refused.length === 0)
|
|
2556
|
+
return;
|
|
2557
|
+
ui2.line();
|
|
2558
|
+
ui2.line(ui2.theme.accent(ui2.lore.syncApplyRefused));
|
|
2559
|
+
for (const entry of result.refused) {
|
|
2560
|
+
const detail = note(ui2, entry);
|
|
2561
|
+
ui2.line(` ${entry.target}${detail === "" ? "" : ui2.theme.dim(` \u2014 ${detail}`)}`);
|
|
2562
|
+
}
|
|
2563
|
+
ui2.line(ui2.theme.dim(ui2.lore.syncMergedNotWritten));
|
|
2564
|
+
}
|
|
2565
|
+
function printSyncApply(ui2, result) {
|
|
2566
|
+
if (result == null) {
|
|
2567
|
+
ui2.flatline(ui2.lore.syncNoManifest);
|
|
2568
|
+
return SYNC_APPLY_EXIT.noManifest;
|
|
2569
|
+
}
|
|
2570
|
+
ui2.line(ui2.theme.accent(ui2.theme.bold(ui2.lore.syncApplyTitle)));
|
|
2571
|
+
ui2.line(ui2.theme.bold(ui2.lore.syncVersionGap(result.report.fromVersion, result.report.toVersion)));
|
|
2572
|
+
printWritten(ui2, result);
|
|
2573
|
+
printUnknownVariants(ui2, result);
|
|
2574
|
+
printRefused(ui2, result);
|
|
2575
|
+
ui2.line();
|
|
2576
|
+
ui2.line(result.written.length === 0 ? ui2.lore.syncApplyNothingWritten : ui2.lore.syncApplyWrote(result.written.length));
|
|
2577
|
+
if (result.refused.length > 0)
|
|
2578
|
+
ui2.line(ui2.lore.syncApplyLeftToYou(result.refused.length));
|
|
2579
|
+
return syncApplyExit(result);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// src/commands/sync/index.ts
|
|
2583
|
+
function countByClass(classifications) {
|
|
2584
|
+
const counts = Object.fromEntries(PATH_CLASSES.map((value) => [value, 0]));
|
|
2585
|
+
for (const entry of classifications)
|
|
2586
|
+
counts[entry.class] += 1;
|
|
2587
|
+
return counts;
|
|
2588
|
+
}
|
|
2589
|
+
function runSync(root, version) {
|
|
2590
|
+
const manifest = readManifest(root);
|
|
2591
|
+
if (manifest == null)
|
|
2592
|
+
return null;
|
|
2593
|
+
const { fromVersion, toVersion, classifications } = replay({ root, manifest, version, facts: factsTheRepositoryEstablishes(root) });
|
|
2594
|
+
return { fromVersion, toVersion, counts: countByClass(classifications), classifications };
|
|
2595
|
+
}
|
|
2596
|
+
function applySync(root, version) {
|
|
2597
|
+
const manifest = readManifest(root);
|
|
2598
|
+
if (manifest == null)
|
|
2599
|
+
return null;
|
|
2600
|
+
const { fromVersion, toVersion, present, produced, classifications } = replay({ root, manifest, version, facts: factsTheRepositoryEstablishes(root) });
|
|
2601
|
+
const report = { fromVersion, toVersion, counts: countByClass(classifications), classifications };
|
|
2602
|
+
const { writes, refused } = planWrites({ classifications, present, produced });
|
|
2603
|
+
const written = applyPlan(root, writes.map((write) => ({ target: write.target, strategy: write.strategy, action: "create", content: write.content })));
|
|
2604
|
+
const ranAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2605
|
+
if (writes.length > 0) {
|
|
2606
|
+
writeManifest(root, recordSync(manifest, {
|
|
2607
|
+
ranAt,
|
|
2608
|
+
toVersion: version,
|
|
2609
|
+
files: Object.fromEntries(writes.map((write) => [write.target, write.ownedSha])),
|
|
2610
|
+
variants: Object.fromEntries(writes.flatMap((write) => write.variant == null ? [] : [[write.target, write.variant]]))
|
|
2611
|
+
}));
|
|
2612
|
+
}
|
|
2613
|
+
return { report, written: written.map((op) => op.target), refused, ranAt };
|
|
2614
|
+
}
|
|
2615
|
+
|
|
1228
2616
|
// src/ui/console.ts
|
|
1229
|
-
import
|
|
2617
|
+
import process4 from "process";
|
|
1230
2618
|
|
|
1231
2619
|
// src/ui/lore.ts
|
|
2620
|
+
var BLOCK_REPLACED_WHOLE = "block-replaced-whole-discovery-bodies-carried-over";
|
|
1232
2621
|
var BANNER = String.raw`
|
|
1233
2622
|
███╗ ███╗██╗██╗ ██╗ ██████╗ ███████╗██╗ ██╗██╗
|
|
1234
2623
|
████╗ ████║██║██║ ██╔╝██╔═══██╗██╔════╝██║ ██║██║
|
|
@@ -1264,12 +2653,68 @@ var LORE = {
|
|
|
1264
2653
|
flatlined: "FLATLINED",
|
|
1265
2654
|
stable: "CONSTRUCT STABLE",
|
|
1266
2655
|
discoveryIncomplete: "Discovery incomplete.",
|
|
2656
|
+
provenance: "AUTHORSHIP TRACE",
|
|
2657
|
+
stillConstructAuthored: (count) => `Still the construct's own words: ${count} marker${count === 1 ? "" : "s"} nobody has stood behind yet.`,
|
|
2658
|
+
baselineCurrent: "The baseline reads back what today's templates produce.",
|
|
2659
|
+
baselineMoved: (count) => `THE BASELINE MOVED ON: ${count} recorded path${count === 1 ? "" : "s"} a sync would add or update \u2014 run \`construct sync\`.`,
|
|
2660
|
+
baselineGapUnknown: "What a sync would add or update cannot be established from this manifest: run `construct sync`.",
|
|
2661
|
+
enforcement: "ENFORCEMENT TRACE",
|
|
2662
|
+
typecheckCaveat: "Typecheck cannot carry this stack alone.",
|
|
2663
|
+
weakestLink: (id, level) => `WEAKEST LINK: ${id} at ${level}`,
|
|
2664
|
+
levelLift: {
|
|
2665
|
+
L0: "wire it to a hook or a workflow step and it climbs",
|
|
2666
|
+
L1: "only a reviewer stands behind it; a hook or a workflow step raises it",
|
|
2667
|
+
L2: "a hook is bypassable with --no-verify; running it in CI too raises it",
|
|
2668
|
+
L3: "L3 is the ceiling doctor can read: branch protection is what makes it blocking, and that lives in the API",
|
|
2669
|
+
L4: "nothing above this"
|
|
2670
|
+
},
|
|
2671
|
+
weakestLinkNone: "WEAKEST LINK: nothing is claimed",
|
|
1267
2672
|
wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
|
|
1268
2673
|
wireHarnessSteps: [
|
|
1269
2674
|
"eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
|
|
1270
2675
|
"tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
|
|
1271
2676
|
"package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
|
|
1272
|
-
]
|
|
2677
|
+
],
|
|
2678
|
+
costUnsupported: (runtime) => `No usage feed: the ${runtime} runtime does not expose per-run token usage.`,
|
|
2679
|
+
costEmpty: "No /implement runs recorded here yet.",
|
|
2680
|
+
costKeyMismatch: (key) => `Runs for this repository were recorded under another path. Looked up: ${key}`,
|
|
2681
|
+
costKeyUnknown: (key) => `Runs may be recorded under another path \u2014 the evidence is not conclusive. Looked up: ${key}`,
|
|
2682
|
+
ledgerCounts: (runs, agents, failures, tokens2) => `Ledger, kept by hand and trusted by nobody: ${runs} runs, ${agents} agents, ${failures} unfinished, ${tokens2} tokens.`,
|
|
2683
|
+
ledgerMalformed: (count) => `${count} ledger line${count === 1 ? "" : "s"} could not be read as a run record.`,
|
|
2684
|
+
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.`,
|
|
2685
|
+
ledgerEntryWithoutSession: "logged as a run, no session behind it",
|
|
2686
|
+
ledgerSessionWithoutEntry: "ran, never logged",
|
|
2687
|
+
syncTitle: "BRAINDANCE \u2014 ENGRAM REPLAY",
|
|
2688
|
+
syncClasses: "PATH CLASSES",
|
|
2689
|
+
syncClassMeaning: {
|
|
2690
|
+
add: "not in the tree; the templates produce it",
|
|
2691
|
+
update: "the construct owns this and the template moved on",
|
|
2692
|
+
conflict: "yours \u2014 you wrote or changed it; sync never touches these",
|
|
2693
|
+
unknown: "which template variant wrote this block cannot be established; you changed nothing, and sync writes nothing here",
|
|
2694
|
+
removed: "you deleted it; sync never puts it back",
|
|
2695
|
+
orphaned: "the construct wrote it once and no longer produces it; it is yours now",
|
|
2696
|
+
keep: "already what the templates produce",
|
|
2697
|
+
foreign: "never ours"
|
|
2698
|
+
},
|
|
2699
|
+
syncMergedKeys: (keys) => `keys: ${keys.join(", ")}`,
|
|
2700
|
+
syncMergedNotWritten: "A merged target is reported by its keys and never rewritten: no merge-json file is written in this version.",
|
|
2701
|
+
syncWriteEffect: {
|
|
2702
|
+
[BLOCK_REPLACED_WHOLE]: "the construct block is replaced whole \u2014 edits between the delimiters do not survive; the discovery marker bodies are carried over"
|
|
2703
|
+
},
|
|
2704
|
+
syncVariantUnknown: (shape) => `no record of the variant that wrote it and no rendering matches the recorded hash; the shape reads like the ${shape} variant, which is a guess and never enough to write on`,
|
|
2705
|
+
syncApplyUnknown: "BEYOND THE BLACKWALL",
|
|
2706
|
+
syncNothingToWrite: "NOTHING TO WRITE \u2014 the replay reads back what the tree already carries.",
|
|
2707
|
+
syncPending: (count) => `${count} path${count === 1 ? "" : "s"} can be written: run \`construct sync --apply\`.`,
|
|
2708
|
+
syncApplyTitle: "RELIC WRITE",
|
|
2709
|
+
syncApplyWritten: "WRITTEN",
|
|
2710
|
+
syncApplyRefused: "LEFT TO YOU",
|
|
2711
|
+
syncApplyWrote: (count) => `${count} path${count === 1 ? "" : "s"} written. The manifest records the owned view of each of them.`,
|
|
2712
|
+
syncApplyNothingWritten: "NOTHING WRITTEN \u2014 the tree already carries what the construct owns.",
|
|
2713
|
+
syncApplyLeftToYou: (count) => `${count} path${count === 1 ? "" : "s"} the record cannot prove the construct owns. Yours to carry across.`,
|
|
2714
|
+
syncVersionGap: (from, to) => `ENGRAM CUT BY v${from} // REPLAYED BY v${to}`,
|
|
2715
|
+
syncNoManifest: "No construct.json here. Run `construct init` first.",
|
|
2716
|
+
recordCarriedOver: (carried, added) => `ENGRAM EXTENDED: ${carried} record${carried === 1 ? "" : "s"} carried over from the construct.json already here, ${added} added.`,
|
|
2717
|
+
recordVarsChanged: (changed) => `ENGRAM REWRITTEN: this run changed ${changed.map((entry) => `${entry.name} (${entry.from} \u2192 ${entry.to})`).join(", ")} in the record; the recorded hashes were taken with the old value${changed.length === 1 ? "" : "s"}.`
|
|
1273
2718
|
};
|
|
1274
2719
|
var PLAIN_LORE = {
|
|
1275
2720
|
subtitle: (version) => `mikoshi-construct v${version}`,
|
|
@@ -1299,17 +2744,73 @@ var PLAIN_LORE = {
|
|
|
1299
2744
|
flatlined: "ERROR",
|
|
1300
2745
|
stable: "OK",
|
|
1301
2746
|
discoveryIncomplete: "Discovery incomplete.",
|
|
2747
|
+
provenance: "Discovery provenance",
|
|
2748
|
+
stillConstructAuthored: (count) => `Unchanged since discovery wrote them: ${count} marker${count === 1 ? "" : "s"} nobody has stood behind yet.`,
|
|
2749
|
+
baselineCurrent: "The baseline reads back what today's templates produce.",
|
|
2750
|
+
baselineMoved: (count) => `The baseline moved on: ${count} recorded path${count === 1 ? "" : "s"} a sync would add or update \u2014 run \`construct sync\`.`,
|
|
2751
|
+
baselineGapUnknown: "What a sync would add or update cannot be established from this manifest: run `construct sync`.",
|
|
2752
|
+
enforcement: "Enforcement",
|
|
2753
|
+
typecheckCaveat: "Typecheck cannot carry this stack alone.",
|
|
2754
|
+
weakestLink: (id, level) => `Weakest link: ${id} at ${level}`,
|
|
2755
|
+
levelLift: {
|
|
2756
|
+
L0: "wire it to a hook or a workflow step and it climbs",
|
|
2757
|
+
L1: "only a reviewer stands behind it; a hook or a workflow step raises it",
|
|
2758
|
+
L2: "a hook is bypassable with --no-verify; running it in CI too raises it",
|
|
2759
|
+
L3: "L3 is the ceiling doctor can read: branch protection is what makes it blocking, and that lives in the API",
|
|
2760
|
+
L4: "nothing above this"
|
|
2761
|
+
},
|
|
2762
|
+
weakestLinkNone: "Weakest link: nothing is claimed",
|
|
1302
2763
|
wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
|
|
1303
2764
|
wireHarnessSteps: [
|
|
1304
2765
|
"eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
|
|
1305
2766
|
"tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
|
|
1306
2767
|
"package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
|
|
1307
|
-
]
|
|
2768
|
+
],
|
|
2769
|
+
costUnsupported: (runtime) => `The ${runtime} runtime does not expose per-run token usage.`,
|
|
2770
|
+
costEmpty: "No /implement runs recorded here yet.",
|
|
2771
|
+
costKeyMismatch: (key) => `Runs for this repository were recorded under another path. Looked up: ${key}`,
|
|
2772
|
+
costKeyUnknown: (key) => `Runs may be recorded under another path \u2014 the evidence is not conclusive. Looked up: ${key}`,
|
|
2773
|
+
ledgerCounts: (runs, agents, failures, tokens2) => `Ledger (a skill step writes it, nothing enforces it): ${runs} runs, ${agents} agents, ${failures} unfinished, ${tokens2} tokens.`,
|
|
2774
|
+
ledgerMalformed: (count) => `${count} ledger line${count === 1 ? "" : "s"} could not be read as a run record.`,
|
|
2775
|
+
ledgerDrift: (entriesWithoutSession, sessionsWithoutEntry, unjoinable) => `Ledger against the runtime: ${entriesWithoutSession} entries with no session, ${sessionsWithoutEntry} sessions with no entry, ${unjoinable} entries with no run id.`,
|
|
2776
|
+
ledgerEntryWithoutSession: "logged as a run, no session behind it",
|
|
2777
|
+
ledgerSessionWithoutEntry: "ran, never logged",
|
|
2778
|
+
syncTitle: "Sync report",
|
|
2779
|
+
syncClasses: "Classes",
|
|
2780
|
+
syncClassMeaning: {
|
|
2781
|
+
add: "not in the tree; the templates produce it",
|
|
2782
|
+
update: "the construct owns this and the template moved on",
|
|
2783
|
+
conflict: "yours \u2014 you wrote or changed it; sync never touches these",
|
|
2784
|
+
unknown: "which template variant wrote this block cannot be established; you changed nothing, and sync writes nothing here",
|
|
2785
|
+
removed: "you deleted it; sync never puts it back",
|
|
2786
|
+
orphaned: "the construct wrote it once and no longer produces it; it is yours now",
|
|
2787
|
+
keep: "already what the templates produce",
|
|
2788
|
+
foreign: "never ours"
|
|
2789
|
+
},
|
|
2790
|
+
syncMergedKeys: (keys) => `keys: ${keys.join(", ")}`,
|
|
2791
|
+
syncMergedNotWritten: "A merged target is reported by its keys and never rewritten: no merge-json file is written in this version.",
|
|
2792
|
+
syncWriteEffect: {
|
|
2793
|
+
[BLOCK_REPLACED_WHOLE]: "the construct block is replaced whole \u2014 edits between the delimiters do not survive; the discovery marker bodies are carried over"
|
|
2794
|
+
},
|
|
2795
|
+
syncVariantUnknown: (shape) => `no record of the variant that wrote it and no rendering matches the recorded hash; the shape reads like the ${shape} variant, which is a guess and never enough to write on`,
|
|
2796
|
+
syncApplyUnknown: "Variant unknown",
|
|
2797
|
+
syncNothingToWrite: "Nothing to write: the replay reads back what the tree already carries.",
|
|
2798
|
+
syncPending: (count) => `${count} path${count === 1 ? "" : "s"} can be written: run \`construct sync --apply\`.`,
|
|
2799
|
+
syncApplyTitle: "Sync apply",
|
|
2800
|
+
syncApplyWritten: "Written",
|
|
2801
|
+
syncApplyRefused: "Left to you",
|
|
2802
|
+
syncApplyWrote: (count) => `${count} path${count === 1 ? "" : "s"} written. The manifest records the owned view of each of them.`,
|
|
2803
|
+
syncApplyNothingWritten: "Nothing written: the tree already carries what the construct owns.",
|
|
2804
|
+
syncApplyLeftToYou: (count) => `${count} path${count === 1 ? "" : "s"} the record cannot prove the construct owns. Yours to carry across.`,
|
|
2805
|
+
syncVersionGap: (from, to) => `Materialized by construct ${from}, read by ${to}.`,
|
|
2806
|
+
syncNoManifest: "No construct.json here. Run `construct init` first.",
|
|
2807
|
+
recordCarriedOver: (carried, added) => `Carried over ${carried} record${carried === 1 ? "" : "s"} from the construct.json already here; added ${added}.`,
|
|
2808
|
+
recordVarsChanged: (changed) => `This run changed ${changed.map((entry) => `${entry.name} (${entry.from} -> ${entry.to})`).join(", ")} in the record; the recorded hashes were taken with the old value${changed.length === 1 ? "" : "s"}.`
|
|
1308
2809
|
};
|
|
1309
2810
|
|
|
1310
2811
|
// src/ui/console.ts
|
|
1311
2812
|
var stdoutWriter = (text2) => {
|
|
1312
|
-
|
|
2813
|
+
process4.stdout.write(text2);
|
|
1313
2814
|
};
|
|
1314
2815
|
function createUi(theme, write = stdoutWriter) {
|
|
1315
2816
|
const plain = theme.name === "plain";
|
|
@@ -1368,21 +2869,21 @@ function createUi(theme, write = stdoutWriter) {
|
|
|
1368
2869
|
}
|
|
1369
2870
|
|
|
1370
2871
|
// src/ui/theme.ts
|
|
1371
|
-
import
|
|
2872
|
+
import process5 from "process";
|
|
1372
2873
|
import pc from "picocolors";
|
|
1373
2874
|
function rgb(r, g, b) {
|
|
1374
2875
|
return (text2) => `\x1B[38;2;${r};${g};${b}m${text2}\x1B[39m`;
|
|
1375
2876
|
}
|
|
1376
2877
|
var identity = (text2) => text2;
|
|
1377
2878
|
function supportsColor() {
|
|
1378
|
-
if (
|
|
2879
|
+
if (process5.env.NO_COLOR != null && process5.env.NO_COLOR !== "")
|
|
1379
2880
|
return false;
|
|
1380
|
-
if (
|
|
2881
|
+
if (process5.env.FORCE_COLOR != null && process5.env.FORCE_COLOR !== "0")
|
|
1381
2882
|
return true;
|
|
1382
2883
|
return pc.isColorSupported;
|
|
1383
2884
|
}
|
|
1384
2885
|
function truecolor() {
|
|
1385
|
-
const term =
|
|
2886
|
+
const term = process5.env.COLORTERM ?? "";
|
|
1386
2887
|
return term === "truecolor" || term === "24bit";
|
|
1387
2888
|
}
|
|
1388
2889
|
var PLAIN = {
|
|
@@ -1451,12 +2952,12 @@ var init = defineCommand({
|
|
|
1451
2952
|
const console = ui(args);
|
|
1452
2953
|
console.banner(VERSION, args.johnny);
|
|
1453
2954
|
try {
|
|
1454
|
-
const prompter = isTTY(
|
|
2955
|
+
const prompter = isTTY(process6.stdout) && process6.stdin.isTTY === true ? createClackPrompter(console.lore) : void 0;
|
|
1455
2956
|
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
|
-
|
|
2957
|
+
process6.exitCode = result.status === "aborted" ? 1 : 0;
|
|
1457
2958
|
} catch (error) {
|
|
1458
2959
|
console.flatline(error instanceof Error ? error.message : String(error));
|
|
1459
|
-
|
|
2960
|
+
process6.exitCode = 1;
|
|
1460
2961
|
}
|
|
1461
2962
|
}
|
|
1462
2963
|
});
|
|
@@ -1469,7 +2970,7 @@ var soulkill = defineCommand({
|
|
|
1469
2970
|
run({ args }) {
|
|
1470
2971
|
const report = detect(args.dir);
|
|
1471
2972
|
if (args.json) {
|
|
1472
|
-
|
|
2973
|
+
process6.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
1473
2974
|
`);
|
|
1474
2975
|
return;
|
|
1475
2976
|
}
|
|
@@ -1489,13 +2990,13 @@ var doctor = defineCommand({
|
|
|
1489
2990
|
run({ args }) {
|
|
1490
2991
|
const result = runDoctor(args.dir);
|
|
1491
2992
|
if (args.json) {
|
|
1492
|
-
|
|
2993
|
+
process6.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1493
2994
|
`);
|
|
1494
|
-
|
|
2995
|
+
process6.exitCode = result?.ok === true ? 0 : 1;
|
|
1495
2996
|
return;
|
|
1496
2997
|
}
|
|
1497
2998
|
const console = ui(args);
|
|
1498
|
-
|
|
2999
|
+
process6.exitCode = printDoctor(console, result);
|
|
1499
3000
|
}
|
|
1500
3001
|
});
|
|
1501
3002
|
var cost = defineCommand({
|
|
@@ -1506,16 +3007,49 @@ var cost = defineCommand({
|
|
|
1506
3007
|
json: { type: "boolean", description: "Machine-readable report", default: false }
|
|
1507
3008
|
},
|
|
1508
3009
|
run({ args }) {
|
|
1509
|
-
const
|
|
3010
|
+
const report = costReport(path22.resolve(args.dir));
|
|
1510
3011
|
if (args.json) {
|
|
1511
|
-
|
|
1512
|
-
process4.stdout.write(`${JSON.stringify(selected, null, 2)}
|
|
3012
|
+
process6.stdout.write(`${JSON.stringify(costJson(report, args.last), null, 2)}
|
|
1513
3013
|
`);
|
|
1514
|
-
|
|
3014
|
+
process6.exitCode = COST_EXIT[report.status];
|
|
1515
3015
|
return;
|
|
1516
3016
|
}
|
|
3017
|
+
process6.exitCode = printCost(ui(args), report, args.last);
|
|
3018
|
+
}
|
|
3019
|
+
});
|
|
3020
|
+
var sync = defineCommand({
|
|
3021
|
+
meta: { name: "sync", description: "Classify what today's construct would change in this repository; --apply writes what it owns" },
|
|
3022
|
+
args: {
|
|
3023
|
+
...commonArgs,
|
|
3024
|
+
json: { type: "boolean", description: "Machine-readable report", default: false },
|
|
3025
|
+
apply: { type: "boolean", description: "Write the paths the construct owns \u2014 the only way sync writes; never a conflict, a removal or a merged file", default: false }
|
|
3026
|
+
},
|
|
3027
|
+
run({ args }) {
|
|
1517
3028
|
const console = ui(args);
|
|
1518
|
-
|
|
3029
|
+
if (args.apply) {
|
|
3030
|
+
try {
|
|
3031
|
+
const result = applySync(args.dir, VERSION);
|
|
3032
|
+
if (args.json) {
|
|
3033
|
+
process6.stdout.write(`${JSON.stringify(result == null ? null : syncApplyJson(result), null, 2)}
|
|
3034
|
+
`);
|
|
3035
|
+
process6.exitCode = syncApplyExit(result);
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
process6.exitCode = printSyncApply(console, result);
|
|
3039
|
+
} catch (error) {
|
|
3040
|
+
console.flatline(error instanceof Error ? error.message : String(error));
|
|
3041
|
+
process6.exitCode = 1;
|
|
3042
|
+
}
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
const report = runSync(args.dir, VERSION);
|
|
3046
|
+
if (args.json) {
|
|
3047
|
+
process6.stdout.write(`${JSON.stringify(report == null ? null : syncJson(report), null, 2)}
|
|
3048
|
+
`);
|
|
3049
|
+
process6.exitCode = syncExit(report);
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
process6.exitCode = printSync(console, report);
|
|
1519
3053
|
}
|
|
1520
3054
|
});
|
|
1521
3055
|
var main = defineCommand({
|
|
@@ -1530,6 +3064,7 @@ var main = defineCommand({
|
|
|
1530
3064
|
inspect: soulkill,
|
|
1531
3065
|
capture: soulkill,
|
|
1532
3066
|
doctor,
|
|
3067
|
+
sync,
|
|
1533
3068
|
cost
|
|
1534
3069
|
}
|
|
1535
3070
|
});
|