codecartographer-pi 0.19.6 → 0.21.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/.codecarto/GUIDE.md +2 -2
- package/.codecarto/broadside/SKILL.md +21 -3
- package/.codecarto/broadside/config.yaml +35 -9
- package/.codecarto/findings/contracts/SKILL.md +4 -1
- package/.codecarto/findings/defect-scan/SKILL.md +10 -0
- package/.codecarto/findings/defect-scan-mechanical/SKILL.md +6 -0
- package/.codecarto/findings/defect-scan-semantic/SKILL.md +8 -1
- package/.codecarto/findings/porting/SKILL.md +4 -0
- package/.codecarto/findings/protocols/SKILL.md +4 -0
- package/.codecarto/templates/mechanical-defects.md +15 -0
- package/.codecarto/templates/reimplementation-spec.md +5 -3
- package/.codecarto/templates/reverse-engineering-bundle.md +10 -1
- package/.codecarto/templates/semantic-defects.md +15 -0
- package/.codecarto/workflow/VALIDATE.md +3 -2
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +13 -9
- package/dist/core/amendment.js +9 -4
- package/dist/core/broadside.d.ts +121 -2
- package/dist/core/broadside.js +478 -92
- package/dist/core/completion.js +88 -23
- package/dist/core/dashboard-writer.d.ts +8 -0
- package/dist/core/dashboard-writer.js +159 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/library.js +9 -4
- package/dist/core/orchestrator-config.d.ts +32 -7
- package/dist/core/orchestrator-config.js +124 -44
- package/dist/core/pipeline.d.ts +73 -0
- package/dist/core/pipeline.js +134 -10
- package/dist/core/prompts.d.ts +20 -0
- package/dist/core/prompts.js +53 -13
- package/dist/core/secrets.d.ts +16 -0
- package/dist/core/secrets.js +98 -0
- package/dist/core/status.d.ts +16 -0
- package/dist/core/status.js +47 -20
- package/dist/core/synthesis.js +5 -2
- package/dist/core/utils.d.ts +7 -0
- package/dist/core/utils.js +7 -0
- package/dist/core/workspace.d.ts +55 -8
- package/dist/core/workspace.js +116 -8
- package/dist/core/yaml.js +181 -15
- package/dist/extensions/codecarto/agent-rewriter.js +21 -14
- package/dist/extensions/codecarto/agent-runner.d.ts +6 -2
- package/dist/extensions/codecarto/agent-runner.js +27 -9
- package/dist/extensions/codecarto/agent-state.d.ts +0 -2
- package/dist/extensions/codecarto/auto-runner.d.ts +1 -1
- package/dist/extensions/codecarto/auto-runner.js +27 -8
- package/dist/extensions/codecarto/broadside-flags.d.ts +3 -1
- package/dist/extensions/codecarto/broadside-flags.js +12 -0
- package/dist/extensions/codecarto/dashboard-narrator.js +9 -2
- package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -8
- package/dist/extensions/codecarto/dashboard-writer.js +5 -154
- package/dist/extensions/codecarto/index.js +158 -47
- package/dist/extensions/codecarto/phase-compaction.js +5 -1
- package/dist/mcp-server/server.d.ts +3 -1
- package/dist/mcp-server/server.js +205 -77
- package/package.json +3 -2
package/dist/core/completion.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFile, copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { applyHandoff, autoAssignIds,
|
|
3
|
+
import { recomputeCursor, resolvePhase, validatePhaseOutput } from "./pipeline.js";
|
|
4
|
+
import { applyHandoff, autoAssignIds, loadHandoffFile, normalizeStatus } from "./status.js";
|
|
5
5
|
import { compareDottedVersions, dateOnly, newlineIfUnterminated, pathExists, uniqueStrings } from "./utils.js";
|
|
6
6
|
import { getWorkspaceState, updateStatusAtomically } from "./workspace.js";
|
|
7
7
|
/**
|
|
@@ -110,7 +110,13 @@ async function appendDecisionLog(workspaceDir, phaseId, closeoutFile, decisions)
|
|
|
110
110
|
const number = String(nextNumber + index).padStart(3, "0");
|
|
111
111
|
return `D${number} | ${decision.trim()} | ${source} | closeouts/${closeoutFile} §Decisions Beyond Prompt (${phaseId})`;
|
|
112
112
|
});
|
|
113
|
-
|
|
113
|
+
// A row appended straight after the section's explanatory paragraph is
|
|
114
|
+
// rendered as part of that paragraph by most Markdown renderers; a blank
|
|
115
|
+
// line makes the rows their own block (self-audit F4). Rows already
|
|
116
|
+
// present stay contiguous with the new ones.
|
|
117
|
+
const trailing = content.replace(/\n+$/, "").split("\n").pop() ?? "";
|
|
118
|
+
const separator = /^D\d+\s*\|/.test(trailing) || trailing.trim() === "" ? "" : "\n";
|
|
119
|
+
content += `${content.endsWith("\n") ? "" : "\n"}${separator}${rows.join("\n")}\n`;
|
|
114
120
|
await writeFile(join(workspaceDir, "DECISIONS.md"), content, "utf8");
|
|
115
121
|
return fresh.length;
|
|
116
122
|
}
|
|
@@ -253,6 +259,80 @@ export function closureEvidenceGateActive(scaffoldVersion) {
|
|
|
253
259
|
const comparison = compareDottedVersions(scaffoldVersion, CLOSURE_EVIDENCE_GATE_SCAFFOLD_VERSION);
|
|
254
260
|
return comparison !== null && comparison >= 0;
|
|
255
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Whether `text` names one of `ids` as a whole token: the id must not run
|
|
264
|
+
* straight into another id character on either side, so `arch-CF2` matches
|
|
265
|
+
* "routed as `arch-CF2`" and not "arch-CF20".
|
|
266
|
+
*/
|
|
267
|
+
function mentionsAnyId(text, ids) {
|
|
268
|
+
for (const id of ids) {
|
|
269
|
+
let from = 0;
|
|
270
|
+
while (true) {
|
|
271
|
+
const at = text.indexOf(id, from);
|
|
272
|
+
if (at === -1)
|
|
273
|
+
break;
|
|
274
|
+
const before = at === 0 ? "" : text[at - 1];
|
|
275
|
+
const after = text[at + id.length] ?? "";
|
|
276
|
+
if (!/[A-Za-z0-9_-]/.test(before) && !/[A-Za-z0-9_-]/.test(after))
|
|
277
|
+
return true;
|
|
278
|
+
from = at + 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Turn each PARTIAL validation row into a `needs-maintainer-decision`
|
|
285
|
+
* question on the phase, unless the row's criterion or evidence cell names an
|
|
286
|
+
* entry the workspace already tracks: an open question, carry-forward, or
|
|
287
|
+
* post-pipeline item, from this handoff or an earlier phase. VALIDATE.md asks
|
|
288
|
+
* the phase to name the tracking entry in the evidence cell; honouring it is
|
|
289
|
+
* what stops a routed gap from also becoming a duplicate question that every
|
|
290
|
+
* later phase re-triages and someone must close (#239).
|
|
291
|
+
*
|
|
292
|
+
* Runs after the handoff has been applied. That is also what keeps the auto
|
|
293
|
+
* ids distinct from the handoff's: both `autoAssignIds` from `oq-<phase>-1`,
|
|
294
|
+
* and assigning the gaps first let a later id-less handoff question replace a
|
|
295
|
+
* gap under the same id.
|
|
296
|
+
*/
|
|
297
|
+
function addPartialRowQuestions(status, phaseId, rows) {
|
|
298
|
+
const phase = status.phases[phaseId];
|
|
299
|
+
if (!phase)
|
|
300
|
+
return;
|
|
301
|
+
const trackedIds = new Set();
|
|
302
|
+
for (const phaseState of Object.values(status.phases)) {
|
|
303
|
+
for (const entry of [...(phaseState.open_questions ?? []), ...(phaseState.carry_forward ?? [])]) {
|
|
304
|
+
if (entry.id)
|
|
305
|
+
trackedIds.add(entry.id);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const entry of status.post_pipeline) {
|
|
309
|
+
if (entry.id)
|
|
310
|
+
trackedIds.add(entry.id);
|
|
311
|
+
}
|
|
312
|
+
const gapEntries = [];
|
|
313
|
+
for (const row of rows) {
|
|
314
|
+
if (!row.result.toUpperCase().includes("PARTIAL"))
|
|
315
|
+
continue;
|
|
316
|
+
if (mentionsAnyId(`${row.criterion} ${row.evidence}`, trackedIds))
|
|
317
|
+
continue;
|
|
318
|
+
const candidate = {
|
|
319
|
+
kind: "needs-maintainer-decision",
|
|
320
|
+
description: row.criterion || "Partial validation gap",
|
|
321
|
+
deferred_reason: row.evidence || "Marked PARTIAL by validation",
|
|
322
|
+
};
|
|
323
|
+
// Idempotent across re-runs of completion: the same gap is one question.
|
|
324
|
+
if (phase.open_questions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason))
|
|
325
|
+
continue;
|
|
326
|
+
gapEntries.push(candidate);
|
|
327
|
+
}
|
|
328
|
+
if (gapEntries.length === 0)
|
|
329
|
+
return;
|
|
330
|
+
// Reserve every id already on the phase so the new ones skip them; the
|
|
331
|
+
// placeholders keep autoAssignIds from touching the existing entries.
|
|
332
|
+
const reserved = phase.open_questions.filter((entry) => entry.id).map((entry) => ({ id: entry.id }));
|
|
333
|
+
autoAssignIds([...reserved, ...gapEntries], "oq", phaseId);
|
|
334
|
+
phase.open_questions.push(...gapEntries);
|
|
335
|
+
}
|
|
256
336
|
export async function completeValidatedPhase(cwd, validation, sourceLabel) {
|
|
257
337
|
const initialState = await getWorkspaceState(cwd);
|
|
258
338
|
if (!initialState)
|
|
@@ -391,20 +471,6 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
|
|
|
391
471
|
open_questions: [],
|
|
392
472
|
carry_forward: [],
|
|
393
473
|
};
|
|
394
|
-
const gapEntries = lockedValidation.rows
|
|
395
|
-
.filter((row) => row.result.toUpperCase().includes("PARTIAL"))
|
|
396
|
-
.map((row) => ({
|
|
397
|
-
kind: "needs-maintainer-decision",
|
|
398
|
-
description: row.criterion || "Partial validation gap",
|
|
399
|
-
deferred_reason: row.evidence || "Marked PARTIAL by validation",
|
|
400
|
-
}));
|
|
401
|
-
autoAssignIds(gapEntries, "oq", validation.phaseId);
|
|
402
|
-
const mergedOpenQuestions = [...existingPhase.open_questions];
|
|
403
|
-
for (const candidate of gapEntries) {
|
|
404
|
-
if (!mergedOpenQuestions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason)) {
|
|
405
|
-
mergedOpenQuestions.push(candidate);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
474
|
nextStatus.phases[validation.phaseId] = {
|
|
409
475
|
status: "complete",
|
|
410
476
|
owner_notes: uniqueStrings([
|
|
@@ -414,18 +480,17 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
|
|
|
414
480
|
`Validation: ${lockedValidation.overall}`,
|
|
415
481
|
]),
|
|
416
482
|
outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
|
|
417
|
-
open_questions:
|
|
483
|
+
open_questions: [...existingPhase.open_questions],
|
|
418
484
|
carry_forward: existingPhase.carry_forward ?? [],
|
|
419
485
|
};
|
|
420
486
|
if (handoff)
|
|
421
487
|
applyHandoff(nextStatus, handoff);
|
|
488
|
+
// After the handoff, so a gap the handoff routed is recognised as
|
|
489
|
+
// tracked and the ids the handoff introduced are known (#239).
|
|
490
|
+
addPartialRowQuestions(nextStatus, validation.phaseId, lockedValidation.rows);
|
|
422
491
|
nextStatus.last_updated = completionTimestamp;
|
|
423
492
|
const nextWorkspace = { ...lockedState, status: nextStatus };
|
|
424
|
-
|
|
425
|
-
nextStatus.current_phase = nextEligible?.id ?? "complete";
|
|
426
|
-
nextStatus.next_actions = nextEligible
|
|
427
|
-
? [`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`]
|
|
428
|
-
: buildTerminalNextActions(nextStatus);
|
|
493
|
+
recomputeCursor(nextWorkspace);
|
|
429
494
|
return {
|
|
430
495
|
state: { ...nextWorkspace, status: nextStatus },
|
|
431
496
|
// The closeout, THREAD_LOG line, decision rows, and staged proposals
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render and atomically replace `.codecarto/dashboard.html`.
|
|
3
|
+
* @returns true when a fresh dashboard landed on disk; false when the
|
|
4
|
+
* workspace is missing or any gather/render/write step failed (swallowed —
|
|
5
|
+
* lifecycle callers must never fail on a dashboard problem, but they may
|
|
6
|
+
* report truthfully whether a refresh happened).
|
|
7
|
+
*/
|
|
8
|
+
export declare function writeDashboard(cwd: string, packageVersion: string): Promise<boolean>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// I/O wrapper that gathers all dashboard inputs and writes the rendered
|
|
2
|
+
// HTML to `.codecarto/dashboard.html`. Best-effort — failures are swallowed
|
|
3
|
+
// and never escalate to a phase error the user sees, mirroring the
|
|
4
|
+
// recordUsage discipline at extensions/codecarto/index.ts.
|
|
5
|
+
//
|
|
6
|
+
// Lived in the Pi extension until #254, with the MCP server reaching across
|
|
7
|
+
// to import it; both surfaces refresh the dashboard at the same lifecycle
|
|
8
|
+
// points (init, completion, amendment, pipeline switch, on demand), so it is
|
|
9
|
+
// a core primitive like everything else they share.
|
|
10
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { atomicWriteFile, DASHBOARD_RELATIVE_PATH, getWorkspaceState, loadUsage, NARRATION_CACHE_RELATIVE_PATH, parseSimpleYaml, pathExists, renderDashboard, } from "./index.js";
|
|
13
|
+
const CLOSEOUT_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
|
14
|
+
/**
|
|
15
|
+
* Render and atomically replace `.codecarto/dashboard.html`.
|
|
16
|
+
* @returns true when a fresh dashboard landed on disk; false when the
|
|
17
|
+
* workspace is missing or any gather/render/write step failed (swallowed —
|
|
18
|
+
* lifecycle callers must never fail on a dashboard problem, but they may
|
|
19
|
+
* report truthfully whether a refresh happened).
|
|
20
|
+
*/
|
|
21
|
+
export async function writeDashboard(cwd, packageVersion) {
|
|
22
|
+
try {
|
|
23
|
+
const state = await getWorkspaceState(cwd);
|
|
24
|
+
if (!state)
|
|
25
|
+
return false;
|
|
26
|
+
const workspaceDir = state.workspaceDir;
|
|
27
|
+
const [usage, closeouts, outputsPresent, narration] = await Promise.all([
|
|
28
|
+
loadUsage(workspaceDir),
|
|
29
|
+
listCloseouts(workspaceDir),
|
|
30
|
+
buildOutputsPresent(state.workspaceDir, state.pipeline),
|
|
31
|
+
loadNarration(workspaceDir),
|
|
32
|
+
]);
|
|
33
|
+
const inputs = {
|
|
34
|
+
status: state.status,
|
|
35
|
+
pipeline: state.pipeline,
|
|
36
|
+
usage,
|
|
37
|
+
closeouts,
|
|
38
|
+
outputsPresent,
|
|
39
|
+
packageVersion,
|
|
40
|
+
generatedAt: new Date().toISOString(),
|
|
41
|
+
narration,
|
|
42
|
+
};
|
|
43
|
+
const html = renderDashboard(inputs);
|
|
44
|
+
await atomicWriteFile(join(workspaceDir, DASHBOARD_RELATIVE_PATH), html);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Best-effort: a failed dashboard write must not surface as a phase
|
|
49
|
+
// error. The user's pipeline state is unaffected; the next state
|
|
50
|
+
// change will trigger another render attempt.
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function listCloseouts(workspaceDir) {
|
|
55
|
+
const dir = join(workspaceDir, "closeouts");
|
|
56
|
+
if (!(await pathExists(dir)))
|
|
57
|
+
return [];
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = await readdir(dir);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const name of entries) {
|
|
67
|
+
const m = CLOSEOUT_FILENAME_RE.exec(name);
|
|
68
|
+
if (!m)
|
|
69
|
+
continue;
|
|
70
|
+
out.push({ date: m[1], phaseOrModule: m[2], fileName: name, summary: await readCloseoutSummary(join(dir, name)) });
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
async function readCloseoutSummary(path) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = await readFile(path, "utf8");
|
|
77
|
+
const lines = raw.split(/\r?\n/);
|
|
78
|
+
const summaryStart = lines.findIndex((line) => /^##\s+Summary\s*$/i.test(line.trim()));
|
|
79
|
+
if (summaryStart === -1)
|
|
80
|
+
return undefined;
|
|
81
|
+
const body = [];
|
|
82
|
+
for (const line of lines.slice(summaryStart + 1)) {
|
|
83
|
+
if (/^##\s+/.test(line.trim()))
|
|
84
|
+
break;
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed || trimmed === "-")
|
|
87
|
+
continue;
|
|
88
|
+
body.push(trimmed.replace(/^[-*]\s+/, ""));
|
|
89
|
+
if (body.join(" ").length > 280)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
const summary = body.join(" ").trim();
|
|
93
|
+
return summary ? `${summary.slice(0, 280)}${summary.length > 280 ? "…" : ""}` : undefined;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function buildOutputsPresent(workspaceDir, pipeline) {
|
|
100
|
+
const out = new Map();
|
|
101
|
+
for (const phaseId of pipeline.phase_order) {
|
|
102
|
+
const phaseDef = pipeline.phases.find((p) => p.id === phaseId);
|
|
103
|
+
if (!phaseDef)
|
|
104
|
+
continue;
|
|
105
|
+
const entry = { secondary: [] };
|
|
106
|
+
if (phaseDef.primary_output) {
|
|
107
|
+
entry.primary = {
|
|
108
|
+
path: phaseDef.primary_output,
|
|
109
|
+
exists: await pathExists(join(workspaceDir, phaseDef.primary_output)),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
for (const sec of phaseDef.secondary_outputs ?? []) {
|
|
113
|
+
entry.secondary.push({
|
|
114
|
+
path: sec.path,
|
|
115
|
+
exists: await pathExists(join(workspaceDir, sec.path)),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
out.set(phaseId, entry);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
async function loadNarration(workspaceDir) {
|
|
123
|
+
const path = join(workspaceDir, NARRATION_CACHE_RELATIVE_PATH);
|
|
124
|
+
if (!(await pathExists(path)))
|
|
125
|
+
return undefined;
|
|
126
|
+
try {
|
|
127
|
+
const raw = await readFile(path, "utf8");
|
|
128
|
+
const { frontmatter, body } = splitFrontmatter(raw);
|
|
129
|
+
if (!frontmatter)
|
|
130
|
+
return undefined;
|
|
131
|
+
const generatedAt = typeof frontmatter.generatedAt === "string" ? frontmatter.generatedAt : "";
|
|
132
|
+
const phaseCountAtGeneration = typeof frontmatter.phaseCountAtGeneration === "number" ? frontmatter.phaseCountAtGeneration : 0;
|
|
133
|
+
if (!generatedAt)
|
|
134
|
+
return undefined;
|
|
135
|
+
return { content: body.trim(), generatedAt, phaseCountAtGeneration };
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function splitFrontmatter(raw) {
|
|
142
|
+
if (!raw.startsWith("---\n"))
|
|
143
|
+
return { frontmatter: null, body: raw };
|
|
144
|
+
const end = raw.indexOf("\n---\n", 4);
|
|
145
|
+
if (end === -1)
|
|
146
|
+
return { frontmatter: null, body: raw };
|
|
147
|
+
const yamlText = raw.slice(4, end);
|
|
148
|
+
const body = raw.slice(end + 5);
|
|
149
|
+
try {
|
|
150
|
+
const parsed = parseSimpleYaml(yamlText);
|
|
151
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
152
|
+
return { frontmatter: parsed, body };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// fall through
|
|
157
|
+
}
|
|
158
|
+
return { frontmatter: null, body };
|
|
159
|
+
}
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
package/dist/core/library.js
CHANGED
|
@@ -33,7 +33,7 @@ import { spawn } from "node:child_process";
|
|
|
33
33
|
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
34
34
|
import { basename, join, resolve } from "node:path";
|
|
35
35
|
import { acquireLock } from "./status.js";
|
|
36
|
-
import { atomicWriteFile, canonicalPath, isPlainObject, normalizeForComparison, pathExists, uniqueTempSuffix } from "./utils.js";
|
|
36
|
+
import { atomicWriteFile, canonicalPath, GIT_TIMEOUT_MS, isPlainObject, normalizeForComparison, pathExists, uniqueTempSuffix } from "./utils.js";
|
|
37
37
|
import { parseSimpleYaml, stringifySimpleYaml } from "./yaml.js";
|
|
38
38
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
39
39
|
export const LIBRARY_MARKER_FILE = ".codecarto-library";
|
|
@@ -676,7 +676,10 @@ export async function listEntries(libraryRoot, filter = {}) {
|
|
|
676
676
|
return false;
|
|
677
677
|
if (filter.slug !== undefined && e.slug !== filter.slug)
|
|
678
678
|
return false;
|
|
679
|
-
|
|
679
|
+
// Same equivalence the publish guard applies: `.git`, scheme, userinfo,
|
|
680
|
+
// default port, and forge-host case do not make two references two
|
|
681
|
+
// repositories, so they must not make a filter miss one either (#257).
|
|
682
|
+
if (filter.source_repo !== undefined && !sameSourceRepo(e.source_repo, filter.source_repo))
|
|
680
683
|
return false;
|
|
681
684
|
if (filter.tag !== undefined && !e.tags.includes(filter.tag))
|
|
682
685
|
return false;
|
|
@@ -827,7 +830,7 @@ async function writeIndexMarkdown(libraryRoot, index, marker) {
|
|
|
827
830
|
const lines = [];
|
|
828
831
|
lines.push(`# ${escapeMd(marker.name)} — Library Index`);
|
|
829
832
|
lines.push("");
|
|
830
|
-
lines.push(`_Generated ${index.generated_at}. Do not edit by hand — regenerate with \`
|
|
833
|
+
lines.push(`_Generated ${index.generated_at}. Do not edit by hand — regenerate with the \`codecarto_library_reindex\` MCP tool._`);
|
|
831
834
|
lines.push("");
|
|
832
835
|
// A single-tenant library has no namespaces but is still one namespace's
|
|
833
836
|
// worth of entries; count once so the noun agrees with the number shown.
|
|
@@ -1038,7 +1041,9 @@ export async function commitPublish(libraryRoot, message, opts = {}) {
|
|
|
1038
1041
|
}
|
|
1039
1042
|
function runGit(cwd, args) {
|
|
1040
1043
|
return new Promise((resolvePromise) => {
|
|
1041
|
-
|
|
1044
|
+
// Bounded like every fetch: a credential helper waiting on a prompt
|
|
1045
|
+
// used to hang publish or source-repo resolution for good (sem 3.12).
|
|
1046
|
+
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: GIT_TIMEOUT_MS });
|
|
1042
1047
|
let stdout = "";
|
|
1043
1048
|
let stderr = "";
|
|
1044
1049
|
child.stdout.on("data", (b) => {
|
|
@@ -24,9 +24,24 @@ export interface LibraryConfig {
|
|
|
24
24
|
* to hosts that actually configured the key. */
|
|
25
25
|
publish_confirm_configured: boolean;
|
|
26
26
|
}
|
|
27
|
+
/** One thing a config file said that the loader could not use. */
|
|
28
|
+
export interface ConfigProblem {
|
|
29
|
+
/** The config file the problem was found in. */
|
|
30
|
+
path: string;
|
|
31
|
+
/** What was wrong and what the loader did about it. */
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
27
34
|
export interface CodecartoConfig {
|
|
28
35
|
orchestrator: OrchestratorConfig;
|
|
29
36
|
library: LibraryConfig;
|
|
37
|
+
/**
|
|
38
|
+
* Faults in the config files that were read, in layer order (user-global
|
|
39
|
+
* first). Empty when every file that exists was used in full. The loader
|
|
40
|
+
* never throws on a bad file — a phase run should not be blocked by a
|
|
41
|
+
* typo in a toggle — but publish refuses while this is non-empty, since
|
|
42
|
+
* the library path and the confirm gate come from here.
|
|
43
|
+
*/
|
|
44
|
+
problems: ConfigProblem[];
|
|
30
45
|
}
|
|
31
46
|
export declare const CONFIG_RELATIVE_PATH = "workflow/config.yaml";
|
|
32
47
|
export declare const USER_CONFIG_DIR: string;
|
|
@@ -56,15 +71,25 @@ export declare function loadCodecartoConfig(workspaceDir: PathLike): Promise<Cod
|
|
|
56
71
|
*/
|
|
57
72
|
export declare function loadUserConfig(): Promise<CodecartoConfig>;
|
|
58
73
|
/**
|
|
59
|
-
* Apply one raw config layer over
|
|
74
|
+
* Apply one raw config layer over the defaults. Public so tests can
|
|
60
75
|
* exercise layering without filesystem fixtures, and so wrappers can mock
|
|
61
|
-
* a layer in memory (e.g. "what if library_path were X").
|
|
76
|
+
* a layer in memory (e.g. "what if library_path were X"). `sourcePath`
|
|
77
|
+
* names the layer in any problem it produces.
|
|
78
|
+
*/
|
|
79
|
+
export declare function mergeConfig(raw: RawConfig | null | undefined, sourcePath?: string): CodecartoConfig;
|
|
80
|
+
/**
|
|
81
|
+
* The lines both surfaces print for a config with problems: one header,
|
|
82
|
+
* then one line per fault naming its file. Empty when there are none.
|
|
62
83
|
*/
|
|
63
|
-
export declare function
|
|
84
|
+
export declare function describeConfigProblems(config: CodecartoConfig): string[];
|
|
64
85
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
86
|
+
* Record a library in a config file (user-global or workspace): set
|
|
87
|
+
* `library.path`, and `library.namespace` when one is given. Every other key
|
|
88
|
+
* in the file, `library.publish_confirm` included, is left exactly as it was
|
|
89
|
+
* — library-init writes only what it was asked for, so it cannot switch the
|
|
90
|
+
* MCP confirm gate on behind the user's back (#244). Creates the file and
|
|
91
|
+
* parent directories if needed. A file that exists but does not parse is
|
|
92
|
+
* never overwritten: the caller is told to fix it first.
|
|
68
93
|
*/
|
|
69
|
-
export declare function writeLibraryConfig(configPath: string, libraryPath: string, namespace?: string | null
|
|
94
|
+
export declare function writeLibraryConfig(configPath: string, libraryPath: string, namespace?: string | null): Promise<void>;
|
|
70
95
|
export {};
|