codecartographer-pi 0.15.0 → 0.16.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.
@@ -3,4 +3,4 @@
3
3
  # workspace's framework-owned files (GUIDE.md, templates/, workflow/ pipelines
4
4
  # and VALIDATE.md) predate the running release. Written at release time and
5
5
  # copied verbatim by init — never edit by hand.
6
- scaffold_version: 0.15.0
6
+ scaffold_version: 0.16.0
@@ -122,6 +122,8 @@ A PARTIAL row's evidence must name what is missing and which `open_questions` or
122
122
 
123
123
  ## When the run drives a rewrite
124
124
 
125
+ When the pipeline completes, the finished spec has a designed destination: publish it to a **library** (`codecarto_publish`; create one with `codecarto_library_init`) so synthesis runs and other projects can consume it — `references/library.md`. A spec that only ever lives in its workspace helps exactly one repository.
126
+
125
127
  If the goal is to rebuild or refactor rather than to understand, two phases carry that weight and both have their own reference:
126
128
 
127
129
  - the defect scans feed `porting` and `reimplementation-spec` as inputs, not as an appendix — `references/deep-audit-synthesis.md`;
@@ -142,5 +144,6 @@ Using what it produces:
142
144
  - `references/deep-audit-synthesis.md` — defect dispositions, hazards as normative rules, reporting
143
145
  - `references/kernel-first-rewrite.md` — rings, build order, acceptance harness, strategic assumptions
144
146
  - `references/carrying-results-forward.md` — starting implementation, autonomy boundaries, publishing findings
147
+ - `references/library.md` — publishing finished specs to a library and consuming them from synthesis runs
145
148
 
146
149
  This guide is also served by the `codecarto_guide` MCP tool, so an agent with the server configured can read it without installing anything.
@@ -0,0 +1,32 @@
1
+ # The library: where finished specs go
2
+
3
+ A CodeCartographer **library** is a directory of published reimplementation-specs with provenance — the bridge between analysis runs and everything downstream. An analysis pipeline ends with a spec in one workspace; publishing it makes it addressable from any other project, and the **synthesis pipeline** consumes exactly these entries ("convert a user vision and explicitly confirmed library specs into a provenance-backed project plan" — see `codecarto_guide` topic `pipeline-selection`). Without a publish step, every analysis is an island.
4
+
5
+ ## Anatomy
6
+
7
+ - A directory holding a `.codecarto-library` marker, `entries/` (optionally namespaced), and a generated `index.yaml` + `INDEX.md`.
8
+ - Discovery: tools take `library_path` (absolute) directly, or resolve the library from a workspace `cwd`'s `config.yaml`; `codecarto_library_init` also writes `library.path` into the user-global config so later calls need no path at all.
9
+ - `codecarto_config` shows the effective merge (`library.path`, `library.namespace`, `publish_confirm`) and whether the marker was found.
10
+
11
+ ## The four tools
12
+
13
+ | Tool | Does | Notes |
14
+ |---|---|---|
15
+ | `codecarto_library_init` | Create the directory, write the marker, record `library.path` in user-global config | Idempotent; pass `namespace` to create a namespaced library |
16
+ | `codecarto_publish` | Publish a spec as a library entry | Required: `source_repo`, `headline`, and `spec` (inline) or `spec_path` (absolute). Content-hash idempotent: identical bytes update metadata in place, no version bump. `slug` derives from `source_repo` if omitted; namespaced libraries require `namespace` (or inherit via `cwd`). Provenance (`source_commit`, `source_branch`, `source_dirty`, `analyzed_at`, `pipeline`, `model_metadata`) is recorded; omitted generation fields default to `unknown` |
17
+ | `codecarto_library_list` | List entries | Filter by `namespace`, `tag`, `slug`, or `source_repo` |
18
+ | `codecarto_library_reindex` | Regenerate `index.yaml` + `INDEX.md` from filesystem state | For manual edits and index merge conflicts |
19
+
20
+ ## When to publish
21
+
22
+ The moment `reimplementation-spec` completes and validates is the publish moment — the spec is finished, the workspace still knows its provenance (`cwd` inherits `pipeline` from `status.yaml`), and the terminal `next_actions` point here. Publish with `cwd` set so provenance rides along:
23
+
24
+ ```
25
+ codecarto_publish cwd:<workspace repo> source_repo:<repo URL or path> headline:"<one line>" spec_path:<abs path to reimplementation-spec.md>
26
+ ```
27
+
28
+ Set `publish_confirm` in config if you want an explicit confirmation gate before writes.
29
+
30
+ ## What this is not
31
+
32
+ Publishing a spec into a library is different from copying findings into a product repository you are about to build — that curated-snapshot flow is `references/carrying-results-forward.md`. The library holds *specs as reusable inputs*; a product repo holds *your implementation of one*.
@@ -8,7 +8,7 @@
8
8
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
9
9
  import { join } from "node:path";
10
10
  import { getNextEligiblePhase } from "./pipeline.js";
11
- import { ensureArray, normalizeStatus } from "./status.js";
11
+ import { buildTerminalNextActions, ensureArray, normalizeStatus } from "./status.js";
12
12
  import { dateOnly, pathExists } from "./utils.js";
13
13
  import { getWorkspaceState, updateStatusAtomically } from "./workspace.js";
14
14
  import { loadYamlFile } from "./yaml.js";
@@ -111,6 +111,9 @@ export async function applyAmendment(cwd, name) {
111
111
  nextStatus.post_pipeline = nextStatus.post_pipeline.filter((entry) => entry.id !== closureId);
112
112
  (nextStatus.post_pipeline.length !== before ? applied.postPipelineClosed : applied.unknownIds).push(closureId);
113
113
  }
114
+ // The amendment changed exactly the counts the terminal routing lines
115
+ // carry (issue #114); rebuild them so status never shows stale numbers.
116
+ nextStatus.next_actions = buildTerminalNextActions(nextStatus);
114
117
  nextStatus.last_updated = timestamp;
115
118
  // Amendment closeout + THREAD_LOG entry, same idempotence rule as
116
119
  // completion: the closeout link appears in THREAD_LOG at most once.
@@ -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
3
  import { getNextEligiblePhase, resolvePhase } from "./pipeline.js";
4
- import { applyHandoff, autoAssignIds, loadHandoffFile, normalizeStatus } from "./status.js";
4
+ import { applyHandoff, autoAssignIds, buildTerminalNextActions, loadHandoffFile, normalizeStatus } from "./status.js";
5
5
  import { dateOnly, pathExists, uniqueStrings } from "./utils.js";
6
6
  import { getWorkspaceState, updateStatusAtomically } from "./workspace.js";
7
7
  /**
@@ -46,6 +46,18 @@ function visibleMarkdown(content) {
46
46
  export const DECISIONS_COMPLETION_LOG_HEADING = "## Completion log";
47
47
  /** Section heading completion stages proposed conventions under. */
48
48
  export const CONVENTIONS_PENDING_HEADING = "## Pending proposals";
49
+ /**
50
+ * True when `heading` exists as its own visible line. Both orchestrator-file
51
+ * templates mention their headings in running prose (issue #111: a raw
52
+ * substring check saw the decisions-template's "…rows under `## Completion
53
+ * log`…" sentence and never inserted the real heading), so presence checks
54
+ * must match a whole trimmed line of comment-stripped content.
55
+ */
56
+ function hasVisibleHeadingLine(content, heading) {
57
+ return visibleMarkdown(content)
58
+ .split(/\r?\n/)
59
+ .some((line) => line.trim() === heading);
60
+ }
49
61
  /**
50
62
  * Ensure an orchestrator file exists: prefer the workspace's template, fall
51
63
  * back to a minimal header for scaffolds that predate the template.
@@ -90,7 +102,7 @@ async function appendDecisionLog(workspaceDir, phaseId, closeoutFile, decisions)
90
102
  if (Number.isFinite(parsed) && parsed >= nextNumber)
91
103
  nextNumber = parsed + 1;
92
104
  }
93
- if (!content.includes(DECISIONS_COMPLETION_LOG_HEADING)) {
105
+ if (!hasVisibleHeadingLine(content, DECISIONS_COMPLETION_LOG_HEADING)) {
94
106
  content += `${content.endsWith("\n") ? "" : "\n"}\n${DECISIONS_COMPLETION_LOG_HEADING}\n\nAppended by completion from each phase handoff's \`decisions\` array. The orchestrator may re-file entries into the category sections above; numbering is shared with them.\n`;
95
107
  }
96
108
  const source = closeoutFile.replace(/\.md$/, "");
@@ -114,13 +126,20 @@ export async function countPendingProposals(workspaceDir, content) {
114
126
  return 0;
115
127
  text = await readFile(filePath, "utf8");
116
128
  }
117
- const start = text.indexOf(CONVENTIONS_PENDING_HEADING);
118
- if (start === -1)
129
+ // Same line-anchored rule as the heading-insertion checks: a prose mention
130
+ // of the heading must not open the section early and miscount.
131
+ const lines = visibleMarkdown(text).split(/\r?\n/);
132
+ const headingIndex = lines.findIndex((line) => line.trim() === CONVENTIONS_PENDING_HEADING);
133
+ if (headingIndex === -1)
119
134
  return 0;
120
- const rest = text.slice(start + CONVENTIONS_PENDING_HEADING.length);
121
- const end = rest.indexOf("\n## ");
122
- const section = end === -1 ? rest : rest.slice(0, end);
123
- return section.split(/\r?\n/).filter((line) => line.startsWith("- **")).length;
135
+ let count = 0;
136
+ for (const line of lines.slice(headingIndex + 1)) {
137
+ if (line.startsWith("## "))
138
+ break;
139
+ if (line.startsWith("- **"))
140
+ count += 1;
141
+ }
142
+ return count;
124
143
  }
125
144
  /**
126
145
  * Stage handoff `proposed_conventions` in CONVENTIONS.md under
@@ -135,7 +154,7 @@ async function stageProposedConventions(workspaceDir, phaseId, timestamp, propos
135
154
  return { staged: 0, totalPending: await countPendingProposals(workspaceDir) };
136
155
  }
137
156
  let content = await ensureOrchestratorFile(workspaceDir, "CONVENTIONS.md", "conventions-template.md", "# Conventions\n\nCross-cutting patterns promoted to project-wide invariants. This scaffold predates templates/conventions-template.md; refresh the framework-owned files for the full format.\n");
138
- if (!content.includes(CONVENTIONS_PENDING_HEADING)) {
157
+ if (!hasVisibleHeadingLine(content, CONVENTIONS_PENDING_HEADING)) {
139
158
  content += `${content.endsWith("\n") ? "" : "\n"}\n${CONVENTIONS_PENDING_HEADING}\n\nStaged by completion from each phase handoff's \`proposed_conventions\`. The orchestrator promotes an entry into a numbered convention above (or removes it with a note) at the phase boundary — see GUIDE.md §Roles.\n`;
140
159
  }
141
160
  // Same visible-content rule as the decision log: template comments must not
@@ -298,7 +317,7 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
298
317
  nextStatus.current_phase = nextEligible?.id ?? "complete";
299
318
  nextStatus.next_actions = nextEligible
300
319
  ? [`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`]
301
- : ["All phases complete. Review findings, open questions, and downstream implementation notes."];
320
+ : buildTerminalNextActions(nextStatus);
302
321
  const artifacts = await writeCompletionArtifacts(lockedState.workspaceDir, validation.phaseId, validation, completionTimestamp, handoff);
303
322
  closeoutPath = artifacts.closeoutPath;
304
323
  orchestratorCheckpoint = buildOrchestratorCheckpoint(artifacts.decisionsAppended, artifacts.totalPendingProposals, nextStatus);
@@ -9,6 +9,14 @@ export declare function autoAssignIds(entries: OpenQuestionEntry[], prefix: stri
9
9
  export declare function ensurePostPipelineArray(value: unknown): PostPipelineEntry[];
10
10
  export declare function ensurePhaseRecord(value: unknown): Record<string, StatusPhase>;
11
11
  export declare function createEmptyStatus(projectName: string, pipelinePath: string, pipeline: PipelineFile): NormalizedStatus;
12
+ /**
13
+ * Route the terminal boundary to the post-pipeline surfaces (issue #114). The
14
+ * moment every phase completes is exactly when skills, amendments, publishing,
15
+ * and the dashboard apply; the prior static sentence left them undiscovered —
16
+ * the 0.15.0 field test finished two full runs with every one of them unused.
17
+ * Amendment recomputes this list so closure counts never go stale.
18
+ */
19
+ export declare function buildTerminalNextActions(status: NormalizedStatus): string[];
12
20
  export declare function normalizeStatus(status: StatusFile, pipeline: PipelineFile, pipelinePath: string, cwd: string): NormalizedStatus;
13
21
  export declare function parseHandoff(value: unknown): PhaseHandoff;
14
22
  /**
@@ -126,6 +126,28 @@ export function createEmptyStatus(projectName, pipelinePath, pipeline) {
126
126
  post_pipeline: [],
127
127
  };
128
128
  }
129
+ /**
130
+ * Route the terminal boundary to the post-pipeline surfaces (issue #114). The
131
+ * moment every phase completes is exactly when skills, amendments, publishing,
132
+ * and the dashboard apply; the prior static sentence left them undiscovered —
133
+ * the 0.15.0 field test finished two full runs with every one of them unused.
134
+ * Amendment recomputes this list so closure counts never go stale.
135
+ */
136
+ export function buildTerminalNextActions(status) {
137
+ const openQuestions = Object.values(status.phases).reduce((sum, phase) => sum + (phase.open_questions?.length ?? 0), 0);
138
+ const postPipeline = status.post_pipeline.length;
139
+ const actions = [
140
+ "All phases complete. Review findings; post-pipeline skills: codecarto_list_skills / codecarto_skill.",
141
+ ];
142
+ if (openQuestions > 0 || postPipeline > 0) {
143
+ actions.push(`${openQuestions} open question(s) and ${postPipeline} post-pipeline item(s) remain — apply resolutions with codecarto_amend (write scratch/amendments/<slug>.yaml from templates/amendment.yaml).`);
144
+ }
145
+ if ("reimplementation-spec" in status.phases) {
146
+ actions.push("Publish the finished spec to a library: codecarto_publish (create one with codecarto_library_init; see the library guide topic).");
147
+ }
148
+ actions.push("Dashboard: .codecarto/dashboard.html (refreshed on completion and amendment; codecarto_dashboard re-renders on demand). Usage totals: codecarto_usage.");
149
+ return actions;
150
+ }
129
151
  export function normalizeStatus(status, pipeline, pipelinePath, cwd) {
130
152
  if (typeof status.schema_version === "number" && status.schema_version > 1) {
131
153
  throw new Error(`Unsupported status schema_version ${status.schema_version}. Supported: 1.`);
@@ -1 +1,8 @@
1
- export declare function writeDashboard(cwd: string, packageVersion: string): Promise<void>;
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>;
@@ -6,11 +6,18 @@ import { readdir, readFile, rename, writeFile } from "node:fs/promises";
6
6
  import { join } from "node:path";
7
7
  import { DASHBOARD_RELATIVE_PATH, getWorkspaceState, loadUsage, NARRATION_CACHE_RELATIVE_PATH, parseSimpleYaml, pathExists, renderDashboard, } from "../../core/index.js";
8
8
  const CLOSEOUT_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
9
+ /**
10
+ * Render and atomically replace `.codecarto/dashboard.html`.
11
+ * @returns true when a fresh dashboard landed on disk; false when the
12
+ * workspace is missing or any gather/render/write step failed (swallowed —
13
+ * lifecycle callers must never fail on a dashboard problem, but they may
14
+ * report truthfully whether a refresh happened).
15
+ */
9
16
  export async function writeDashboard(cwd, packageVersion) {
10
17
  try {
11
18
  const state = await getWorkspaceState(cwd);
12
19
  if (!state)
13
- return;
20
+ return false;
14
21
  const workspaceDir = state.workspaceDir;
15
22
  const [usage, closeouts, outputsPresent, narration] = await Promise.all([
16
23
  loadUsage(workspaceDir),
@@ -33,11 +40,13 @@ export async function writeDashboard(cwd, packageVersion) {
33
40
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
34
41
  await writeFile(tempPath, html, "utf8");
35
42
  await rename(tempPath, path);
43
+ return true;
36
44
  }
37
45
  catch {
38
46
  // Best-effort: a failed dashboard write must not surface as a phase
39
47
  // error. The user's pipeline state is unaffected; the next state
40
48
  // change will trigger another render attempt.
49
+ return false;
41
50
  }
42
51
  }
43
52
  async function listCloseouts(workspaceDir) {
@@ -144,7 +144,12 @@ export async function handleStatus(args) {
144
144
  `Open questions (terminal unresolved): ${terminalOpenQuestions}`,
145
145
  `Carry-forward (pipeline phases): ${totalCarryForward}`,
146
146
  `Post-pipeline work: ${postPipelinePending} pending`,
147
- `Next: ${state.status.next_actions[0] ?? (nextPhase ? `Begin ${nextPhase.id}` : "All phases complete.")}`,
147
+ // Render every stored action: the terminal list routes to several
148
+ // post-pipeline surfaces (issue #114), and a text-reading client that
149
+ // only ever sees actions[0] loses exactly the routing it exists for.
150
+ ...(state.status.next_actions.length > 0
151
+ ? state.status.next_actions.map((action, index) => `${index === 0 ? "Next: " : " "}${action}`)
152
+ : [`Next: ${nextPhase ? `Begin ${nextPhase.id}` : "All phases complete."}`]),
148
153
  ];
149
154
  if (scaffoldNotice)
150
155
  summaryLines.push(`Scaffold: ${scaffoldNotice}`);
@@ -267,6 +272,11 @@ export async function handleComplete(args) {
267
272
  // state is already written, so a usage-log write failure must not fail
268
273
  // the completion result. Nothing else can act on the error here.
269
274
  }
275
+ // Dashboard freshness is a completion side effect (issue #112): the counts
276
+ // it renders change exactly here, and a stale dashboard misreports them
277
+ // confidently. writeDashboard never throws; its boolean says whether a
278
+ // fresh render actually landed, so the result only claims what happened.
279
+ const dashboardPath = (await writeDashboard(cwd, PACKAGE_VERSION)) ? ".codecarto/dashboard.html" : undefined;
270
280
  const lines = [
271
281
  `Marked ${validation.phaseId} complete (validation: ${validation.overall}).`,
272
282
  `Next phase: ${updatedState.status.current_phase}`,
@@ -275,12 +285,15 @@ export async function handleComplete(args) {
275
285
  lines.push(closeoutNotice);
276
286
  if (orchestratorCheckpoint)
277
287
  lines.push(orchestratorCheckpoint);
288
+ if (dashboardPath)
289
+ lines.push(`Dashboard refreshed: ${dashboardPath}`);
278
290
  return textResult(lines.join("\n"), {
279
291
  completedPhase: validation.phaseId,
280
292
  validation: validation.overall,
281
293
  nextPhase: updatedState.status.current_phase,
282
294
  closeoutNotice,
283
295
  orchestratorCheckpoint,
296
+ dashboardPath,
284
297
  });
285
298
  }
286
299
  export async function handleSkill(args) {
@@ -684,7 +697,9 @@ export async function handleUsage(args) {
684
697
  export async function handleDashboard(args) {
685
698
  const cwd = await validateCwd(args.cwd);
686
699
  await requireWorkspace(cwd);
687
- await writeDashboard(cwd, PACKAGE_VERSION);
700
+ if (!(await writeDashboard(cwd, PACKAGE_VERSION))) {
701
+ throw new McpError(ErrorCode.InvalidRequest, "Dashboard render failed: the workspace state could not be gathered or .codecarto/dashboard.html is not writable.");
702
+ }
688
703
  return textResult("Dashboard regenerated: .codecarto/dashboard.html", { path: ".codecarto/dashboard.html" });
689
704
  }
690
705
  export async function handleListSkills(args) {
@@ -725,6 +740,9 @@ export async function handleAmend(args) {
725
740
  const { applied, closeoutNotice } = await applyAmendment(cwd, args.name).catch((error) => {
726
741
  throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
727
742
  });
743
+ // An amendment exists precisely to change the numbers the dashboard shows
744
+ // (issue #112); refresh it, reporting only a render that actually landed.
745
+ const dashboardPath = (await writeDashboard(cwd, PACKAGE_VERSION)) ? ".codecarto/dashboard.html" : undefined;
728
746
  const lines = [
729
747
  `Amendment applied.`,
730
748
  `Open questions closed: ${applied.openQuestionsClosed.length > 0 ? applied.openQuestionsClosed.join(", ") : "none"}`,
@@ -733,11 +751,14 @@ export async function handleAmend(args) {
733
751
  if (applied.unknownIds.length > 0)
734
752
  lines.push(`Ids that matched nothing (already closed or unknown): ${applied.unknownIds.join(", ")}`);
735
753
  lines.push(closeoutNotice);
754
+ if (dashboardPath)
755
+ lines.push(`Dashboard refreshed: ${dashboardPath}`);
736
756
  return textResult(lines.join("\n"), {
737
757
  openQuestionsClosed: applied.openQuestionsClosed,
738
758
  postPipelineClosed: applied.postPipelineClosed,
739
759
  unknownIds: applied.unknownIds,
740
760
  closeoutNotice,
761
+ dashboardPath,
741
762
  });
742
763
  }
743
764
  // ---------- tool registry ----------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "mcpName": "io.github.HuginnIndustries/codecartographer",
5
5
  "description": "Turn an unfamiliar codebase into a validated reimplementation spec, then synthesize confirmed specs and a product vision into a traceable plan.",
6
6
  "type": "module",