codecartographer-pi 0.16.0 → 0.17.1

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.
Files changed (77) hide show
  1. package/.codecarto/GUIDE.md +16 -3
  2. package/.codecarto/README.md +3 -0
  3. package/.codecarto/broadside/SKILL.md +143 -0
  4. package/.codecarto/broadside/config.yaml +104 -0
  5. package/.codecarto/findings/architecture/SKILL.md +1 -0
  6. package/.codecarto/findings/broadside-scout/README.md +20 -0
  7. package/.codecarto/findings/broadside-scout/SKILL.md +101 -0
  8. package/.codecarto/findings/contracts/SKILL.md +1 -0
  9. package/.codecarto/findings/defect-scan/SKILL.md +15 -1
  10. package/.codecarto/findings/defect-scan/passes/01-logic-and-correctness.md +1 -1
  11. package/.codecarto/findings/defect-scan/passes/02-error-handling.md +1 -1
  12. package/.codecarto/findings/defect-scan/passes/03-concurrency-and-resources.md +1 -1
  13. package/.codecarto/findings/defect-scan/passes/04-security-and-trust.md +1 -1
  14. package/.codecarto/findings/defect-scan/passes/05-api-contract-violations.md +2 -1
  15. package/.codecarto/findings/defect-scan/passes/06-config-and-environment.md +1 -1
  16. package/.codecarto/findings/defect-scan-mechanical/SKILL.md +3 -2
  17. package/.codecarto/findings/defect-scan-semantic/SKILL.md +2 -0
  18. package/.codecarto/findings/porting/SKILL.md +2 -1
  19. package/.codecarto/findings/protocols/SKILL.md +1 -0
  20. package/.codecarto/findings/reimplementation-spec/SKILL.md +1 -1
  21. package/.codecarto/skills/spec-delta-application/SKILL.md +3 -1
  22. package/.codecarto/templates/architecture-map.md +1 -1
  23. package/.codecarto/templates/backlog-project.md +51 -0
  24. package/.codecarto/templates/broadside-scout-brief.md +97 -0
  25. package/.codecarto/templates/defect-report.md +23 -0
  26. package/.codecarto/templates/mechanical-defects.md +22 -0
  27. package/.codecarto/templates/reverse-engineering-bundle.md +2 -2
  28. package/.codecarto/templates/semantic-defects.md +26 -0
  29. package/.codecarto/{THREAD_LOG.md → templates/thread-log.md} +2 -5
  30. package/.codecarto/workflow/VALIDATE.md +1 -1
  31. package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
  32. package/.codecarto/workflow/pipeline-full-with-audit.yaml +3 -1
  33. package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +5 -1
  34. package/.codecarto/workflow/pipeline-scout-first.yaml +275 -0
  35. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  36. package/README.md +51 -6
  37. package/agent-skill/codecartographer/SKILL.md +3 -1
  38. package/agent-skill/codecartographer/references/broadside.md +115 -0
  39. package/agent-skill/codecartographer/references/deep-audit-synthesis.md +4 -1
  40. package/agent-skill/codecartographer/references/library.md +2 -2
  41. package/agent-skill/codecartographer/references/orchestration.md +1 -1
  42. package/agent-skill/codecartographer/references/pipeline-selection.md +14 -0
  43. package/dist/core/amendment.js +2 -2
  44. package/dist/core/broadside.d.ts +421 -0
  45. package/dist/core/broadside.js +2349 -0
  46. package/dist/core/completion.d.ts +5 -0
  47. package/dist/core/completion.js +38 -6
  48. package/dist/core/dashboard.js +5 -3
  49. package/dist/core/findings.d.ts +59 -0
  50. package/dist/core/findings.js +145 -0
  51. package/dist/core/index.d.ts +2 -0
  52. package/dist/core/index.js +2 -0
  53. package/dist/core/library.d.ts +88 -1
  54. package/dist/core/library.js +260 -7
  55. package/dist/core/orchestrator-config.js +5 -2
  56. package/dist/core/pipeline.js +16 -0
  57. package/dist/core/prompts.js +1 -1
  58. package/dist/core/status.js +23 -7
  59. package/dist/core/types.d.ts +6 -0
  60. package/dist/core/utils.d.ts +14 -0
  61. package/dist/core/utils.js +37 -1
  62. package/dist/core/workspace.d.ts +17 -0
  63. package/dist/core/workspace.js +79 -20
  64. package/dist/core/yaml.js +19 -4
  65. package/dist/extensions/codecarto/agent-runner.js +6 -0
  66. package/dist/extensions/codecarto/auto-runner.d.ts +2 -0
  67. package/dist/extensions/codecarto/broadside-flags.d.ts +26 -0
  68. package/dist/extensions/codecarto/broadside-flags.js +129 -0
  69. package/dist/extensions/codecarto/dashboard-narrator.js +1 -1
  70. package/dist/extensions/codecarto/index.js +270 -18
  71. package/dist/extensions/codecarto/phase-compaction.js +4 -0
  72. package/dist/mcp-server/server.d.ts +22 -0
  73. package/dist/mcp-server/server.js +282 -17
  74. package/package.json +11 -2
  75. package/.codecarto/BACKLOG.md +0 -184
  76. package/.codecarto/CHANGELOG-2026-05-02-feedback-pass.md +0 -118
  77. package/.codecarto/closeouts/2026-05-02-framework-feedback-pass.md +0 -111
@@ -2,6 +2,11 @@ import type { ValidationResult, WorkspaceState } from "./types.ts";
2
2
  export type CompletionResult = {
3
3
  updatedState: WorkspaceState;
4
4
  closeoutNotice?: string;
5
+ /**
6
+ * Non-gating closure-integrity observations (#122): closures the handoff
7
+ * claims that the primary output never mentions. Empty when clean.
8
+ */
9
+ warnings: string[];
5
10
  /**
6
11
  * One-line phase-boundary reminder covering what completion just mechanized
7
12
  * (decisions appended, proposals staged) and what still needs orchestrator
@@ -1,8 +1,8 @@
1
1
  import { appendFile, copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { getNextEligiblePhase, resolvePhase } from "./pipeline.js";
3
+ import { getNextEligiblePhase, resolvePhase, validatePhaseOutput } from "./pipeline.js";
4
4
  import { applyHandoff, autoAssignIds, buildTerminalNextActions, loadHandoffFile, normalizeStatus } from "./status.js";
5
- import { dateOnly, pathExists, uniqueStrings } from "./utils.js";
5
+ import { dateOnly, newlineIfUnterminated, pathExists, uniqueStrings } from "./utils.js";
6
6
  import { getWorkspaceState, updateStatusAtomically } from "./workspace.js";
7
7
  /**
8
8
  * The Markdown a reader sees: content inside `<!-- -->` blocks removed by a
@@ -223,7 +223,7 @@ async function writeCompletionArtifacts(workspaceDir, phaseId, validation, times
223
223
  }
224
224
  const link = `[closeout](closeouts/${closeoutFile})`;
225
225
  if (!current.split(/\r?\n/).some((line) => line.includes(link))) {
226
- await appendFile(threadLogPath, `${entry}\n`, "utf8");
226
+ await appendFile(threadLogPath, `${newlineIfUnterminated(current)}${entry}\n`, "utf8");
227
227
  }
228
228
  // Mechanize the orchestrator loop's bookkeeping half (issue #98): decisions
229
229
  // reach DECISIONS.md and proposals reach CONVENTIONS.md at completion, so a
@@ -268,6 +268,21 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
268
268
  throw new Error("Invalid handoff: post_pipeline entries require a canonical id");
269
269
  }
270
270
  }
271
+ // Closure integrity (#122, warning only): a handoff can close a carry-forward
272
+ // or open question the report never addressed — "closed in the handoff,
273
+ // resolved nowhere." The id of every claimed closure should appear somewhere
274
+ // in the primary output that claims to resolve it.
275
+ const warnings = [];
276
+ if (handoff && validation.outputPath) {
277
+ const closures = [...handoff.carry_forward_closures, ...handoff.open_question_closures].filter((id) => id?.trim());
278
+ if (closures.length > 0) {
279
+ const output = await readFile(validation.outputPath, "utf8").catch(() => "");
280
+ const unmentioned = closures.filter((id) => !output.includes(id));
281
+ if (unmentioned.length > 0) {
282
+ warnings.push(`The handoff closes ${unmentioned.join(", ")} but .codecarto/${validation.primaryOutput} never mentions ${unmentioned.length === 1 ? "that id" : "those ids"} — a closure should be visible in the report that claims to resolve it, not only in the handoff.`);
283
+ }
284
+ }
285
+ }
271
286
  const completionTimestamp = new Date().toISOString();
272
287
  let closeoutPath;
273
288
  let orchestratorCheckpoint;
@@ -275,6 +290,22 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
275
290
  const phase = resolvePhase(lockedState, validation.phaseId);
276
291
  if (!phase?.primary_output)
277
292
  throw new Error(`Phase ${validation.phaseId} is missing primary_output.`);
293
+ // Re-validate the output under the lock (#132). The caller's
294
+ // validation snapshot can predate a concurrent edit or another
295
+ // session's completion; a stale PASS must not complete a phase whose
296
+ // output no longer validates. The locked recheck is the authoritative
297
+ // one and is what every artifact below is written from.
298
+ // A validation that never touched a file on disk (no outputPath)
299
+ // has nothing to race against, so the caller's result stands — the
300
+ // real surfaces (MCP, Pi) always validate real files.
301
+ const authoritative = validation.outputPath
302
+ ? await validatePhaseOutput(lockedState, validation.phaseId)
303
+ : validation;
304
+ if (authoritative.overall === "FAIL" || authoritative.overall === "MISSING") {
305
+ throw new Error(`Refusing to complete ${validation.phaseId}: the output no longer validates under the status lock ` +
306
+ `(now ${authoritative.overall}). It changed since the last validation — re-run validation and fix the output first.`);
307
+ }
308
+ const lockedValidation = authoritative;
278
309
  const nextStatus = normalizeStatus(lockedState.status, lockedState.pipeline, lockedState.status.pipeline, lockedState.cwd);
279
310
  const existingPhase = nextStatus.phases[validation.phaseId] ?? {
280
311
  status: "pending",
@@ -283,7 +314,7 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
283
314
  open_questions: [],
284
315
  carry_forward: [],
285
316
  };
286
- const gapEntries = validation.rows
317
+ const gapEntries = lockedValidation.rows
287
318
  .filter((row) => row.result.toUpperCase().includes("PARTIAL"))
288
319
  .map((row) => ({
289
320
  kind: "needs-maintainer-decision",
@@ -303,7 +334,7 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
303
334
  ...existingPhase.owner_notes,
304
335
  `Completed via ${sourceLabel}.`,
305
336
  `Primary output: .codecarto/${validation.primaryOutput}`,
306
- `Validation: ${validation.overall}`,
337
+ `Validation: ${lockedValidation.overall}`,
307
338
  ]),
308
339
  outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
309
340
  open_questions: mergedOpenQuestions,
@@ -318,7 +349,7 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
318
349
  nextStatus.next_actions = nextEligible
319
350
  ? [`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`]
320
351
  : buildTerminalNextActions(nextStatus);
321
- const artifacts = await writeCompletionArtifacts(lockedState.workspaceDir, validation.phaseId, validation, completionTimestamp, handoff);
352
+ const artifacts = await writeCompletionArtifacts(lockedState.workspaceDir, validation.phaseId, lockedValidation, completionTimestamp, handoff);
322
353
  closeoutPath = artifacts.closeoutPath;
323
354
  orchestratorCheckpoint = buildOrchestratorCheckpoint(artifacts.decisionsAppended, artifacts.totalPendingProposals, nextStatus);
324
355
  return { state: { ...nextWorkspace, status: nextStatus } };
@@ -327,5 +358,6 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
327
358
  updatedState,
328
359
  closeoutNotice: closeoutPath ? `Closeout: ${closeoutPath}` : undefined,
329
360
  orchestratorCheckpoint,
361
+ warnings,
330
362
  };
331
363
  }
@@ -452,7 +452,9 @@ function usagePhaseNote(phaseId, status) {
452
452
  function renderActivityTimeline(runs) {
453
453
  if (runs.length === 0)
454
454
  return "";
455
- const sorted = [...runs].sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1));
455
+ // Newest first. The comparator must return 0 for equal keys: returning -1
456
+ // in both directions left same-timestamp order implementation-defined (#134).
457
+ const sorted = [...runs].sort((a, b) => (a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : 0));
456
458
  const visible = sorted.slice(0, TIMELINE_VISIBLE_COUNT);
457
459
  const overflow = sorted.slice(TIMELINE_VISIBLE_COUNT);
458
460
  const hasSessionLinks = sorted.some((run) => Boolean(run.session_file && safeRelativeHref(run.session_file)));
@@ -507,7 +509,7 @@ function renderCloseoutsList(inputs) {
507
509
  const closeouts = inputs.closeouts;
508
510
  if (closeouts.length === 0)
509
511
  return [`<section class="cc-card cc-closeouts" id="closeouts" aria-label="Closeouts" data-section>`, `<h2>Closeouts</h2>`, `<p class="cc-empty">No closeouts yet.</p>`, `</section>`].join("\n");
510
- const sorted = [...closeouts].sort((a, b) => (a.date < b.date ? 1 : -1));
512
+ const sorted = [...closeouts].sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
511
513
  const rows = sorted.map((c) => {
512
514
  const phase = getPhase(inputs.pipeline, c.phaseOrModule);
513
515
  const outputs = inputs.outputsPresent.get(c.phaseOrModule);
@@ -646,7 +648,7 @@ function getPhase(pipeline, phaseId) {
646
648
  return pipeline.phases.find((p) => p.id === phaseId);
647
649
  }
648
650
  function closeoutForPhase(closeouts, phaseId) {
649
- return [...closeouts].filter((c) => c.phaseOrModule === phaseId).sort((a, b) => (a.date < b.date ? 1 : -1))[0];
651
+ return [...closeouts].filter((c) => c.phaseOrModule === phaseId).sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0))[0];
650
652
  }
651
653
  function phaseAnchor(phaseId) {
652
654
  return `phase-${phaseId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * First scaffold version whose defect templates offer `verify at runtime`.
3
+ * A workspace scaffolded before it had no honest action for an unsettled
4
+ * finding, so the pairing violation is reported as a warning there instead
5
+ * of failing a phase mid-run.
6
+ */
7
+ export declare const FINDINGS_PAIRING_GATE_SCAFFOLD_VERSION = "0.17.1";
8
+ /** Evidence levels that mean "not settled by reading this source". */
9
+ export declare const UNSETTLED_EVIDENCE_LEVELS: ReadonlySet<string>;
10
+ /** Actions that assert the diagnosis is settled enough to act on. */
11
+ export declare const SETTLED_FIX_ACTIONS: ReadonlySet<string>;
12
+ /** The pre-porting action an unsettled finding takes. */
13
+ export declare const RUNTIME_VERIFY_ACTION = "verify at runtime";
14
+ export type FindingRow = {
15
+ /** The `## Pass N` heading the table sits under, when there is one. */
16
+ pass: string | null;
17
+ /** The `#` cell, verbatim. */
18
+ number: string;
19
+ /** Normalized Evidence Level cell (lowercase, markup stripped). */
20
+ evidence: string;
21
+ /** Normalized Action cell. */
22
+ action: string;
23
+ /** 1-based line of the row in the document. */
24
+ line: number;
25
+ };
26
+ export type FindingsCrossCheck = {
27
+ /** Violations that fail validation on a current scaffold. */
28
+ errors: string[];
29
+ /** Non-gating observations, rendered as NOTE lines. */
30
+ warnings: string[];
31
+ findings: FindingRow[];
32
+ };
33
+ /**
34
+ * Every data row of every table whose header carries both an Evidence Level
35
+ * and an Action column. Placeholder rows (both cells empty) are skipped so an
36
+ * untouched template section contributes nothing.
37
+ */
38
+ export declare function parseFindingsTables(content: string): FindingRow[];
39
+ /** Whether the report has an `## Open Questions` table, and how many filled rows it holds. */
40
+ export declare function parseOpenQuestionsTable(content: string): {
41
+ present: boolean;
42
+ rows: number;
43
+ };
44
+ /**
45
+ * Whether the pairing violation fails validation for this workspace. True from
46
+ * the scaffold version that introduced `verify at runtime`; an unversioned or
47
+ * older scaffold only warns.
48
+ */
49
+ export declare function findingsPairingGateActive(scaffoldVersion: string | undefined | null): boolean;
50
+ /**
51
+ * Run the cross-checks over a phase output. Returns empty results for any
52
+ * document without findings tables, so non-defect phases are untouched.
53
+ *
54
+ * @param content - The primary output's markdown.
55
+ * @param opts.gate - Whether the pairing violation is an error (current scaffold) or a warning.
56
+ */
57
+ export declare function crossCheckFindings(content: string, opts: {
58
+ gate: boolean;
59
+ }): FindingsCrossCheck;
@@ -0,0 +1,145 @@
1
+ // Mechanical cross-checks over a defect report's findings tables (issue #122).
2
+ //
3
+ // A run can register an open question saying "source alone cannot determine
4
+ // which" and, in the same run, ship one of that question's candidates as
5
+ // `strong inference` / `fix before porting`. Both artifacts validate on their
6
+ // own criteria because nothing reads what the evidence and action cells SAY.
7
+ // The checks here do: they are deterministic reads of two cells the model
8
+ // wrote itself, so the gating one cannot wedge an --auto run on a heuristic.
9
+ //
10
+ // The findings tables are header-identical across the three defect templates
11
+ // (`| # | Location | Defect | Severity | Evidence Level | Action |`, with an
12
+ // optional trailing Spec Reference), which is what makes a header-driven parse
13
+ // a parse and not a guess. Any table without both an Evidence Level and an
14
+ // Action column is left alone.
15
+ import { compareDottedVersions } from "./utils.js";
16
+ /**
17
+ * First scaffold version whose defect templates offer `verify at runtime`.
18
+ * A workspace scaffolded before it had no honest action for an unsettled
19
+ * finding, so the pairing violation is reported as a warning there instead
20
+ * of failing a phase mid-run.
21
+ */
22
+ export const FINDINGS_PAIRING_GATE_SCAFFOLD_VERSION = "0.17.1";
23
+ /** Evidence levels that mean "not settled by reading this source". */
24
+ export const UNSETTLED_EVIDENCE_LEVELS = new Set(["open question", "external-behavior claim"]);
25
+ /** Actions that assert the diagnosis is settled enough to act on. */
26
+ export const SETTLED_FIX_ACTIONS = new Set(["fix before porting", "fix now"]);
27
+ /** The pre-porting action an unsettled finding takes. */
28
+ export const RUNTIME_VERIFY_ACTION = "verify at runtime";
29
+ function normalizeCell(cell) {
30
+ return cell.replace(/[`*_]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
31
+ }
32
+ function isTableRow(line) {
33
+ return line.trim().startsWith("|");
34
+ }
35
+ function isSeparatorRow(line) {
36
+ return /^\s*\|?\s*:?-{3,}/.test(line);
37
+ }
38
+ function splitRow(line) {
39
+ const trimmed = line.trim();
40
+ const inner = trimmed.slice(1, trimmed.endsWith("|") ? -1 : undefined);
41
+ return inner.split("|").map((cell) => cell.trim());
42
+ }
43
+ /**
44
+ * Every data row of every table whose header carries both an Evidence Level
45
+ * and an Action column. Placeholder rows (both cells empty) are skipped so an
46
+ * untouched template section contributes nothing.
47
+ */
48
+ export function parseFindingsTables(content) {
49
+ const lines = content.split(/\r?\n/);
50
+ const rows = [];
51
+ let pass = null;
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const heading = /^##\s+Pass\s+(\d+)\b/i.exec(lines[i]);
54
+ if (heading) {
55
+ pass = heading[1];
56
+ continue;
57
+ }
58
+ if (!isTableRow(lines[i]) || !isSeparatorRow(lines[i + 1] ?? ""))
59
+ continue;
60
+ const header = splitRow(lines[i]).map(normalizeCell);
61
+ const evidenceIdx = header.indexOf("evidence level");
62
+ const actionIdx = header.indexOf("action");
63
+ const numberIdx = header.indexOf("#");
64
+ if (evidenceIdx < 0 || actionIdx < 0)
65
+ continue;
66
+ for (let j = i + 2; j < lines.length && isTableRow(lines[j]); j++) {
67
+ const cells = splitRow(lines[j]);
68
+ const evidence = normalizeCell(cells[evidenceIdx] ?? "");
69
+ const action = normalizeCell(cells[actionIdx] ?? "");
70
+ if (!evidence && !action)
71
+ continue;
72
+ rows.push({ pass, number: numberIdx >= 0 ? (cells[numberIdx] ?? "").trim() : "", evidence, action, line: j + 1 });
73
+ i = j;
74
+ }
75
+ }
76
+ return rows;
77
+ }
78
+ /** Whether the report has an `## Open Questions` table, and how many filled rows it holds. */
79
+ export function parseOpenQuestionsTable(content) {
80
+ const lines = content.split(/\r?\n/);
81
+ const start = lines.findIndex((line) => /^##\s+Open Questions\s*$/i.test(line));
82
+ if (start < 0)
83
+ return { present: false, rows: 0 };
84
+ for (let i = start + 1; i < lines.length; i++) {
85
+ if (/^##\s/.test(lines[i]))
86
+ break; // next section, no table
87
+ if (!isTableRow(lines[i]) || !isSeparatorRow(lines[i + 1] ?? ""))
88
+ continue;
89
+ let rows = 0;
90
+ for (let j = i + 2; j < lines.length && isTableRow(lines[j]); j++) {
91
+ if (splitRow(lines[j]).some((cell) => cell.length > 0))
92
+ rows++;
93
+ }
94
+ return { present: true, rows };
95
+ }
96
+ return { present: true, rows: 0 };
97
+ }
98
+ /**
99
+ * Whether the pairing violation fails validation for this workspace. True from
100
+ * the scaffold version that introduced `verify at runtime`; an unversioned or
101
+ * older scaffold only warns.
102
+ */
103
+ export function findingsPairingGateActive(scaffoldVersion) {
104
+ if (!scaffoldVersion)
105
+ return false;
106
+ const comparison = compareDottedVersions(scaffoldVersion, FINDINGS_PAIRING_GATE_SCAFFOLD_VERSION);
107
+ return comparison !== null && comparison >= 0;
108
+ }
109
+ function describe(row) {
110
+ const where = row.pass ? `Pass ${row.pass} finding #${row.number || "?"}` : `finding #${row.number || "?"}`;
111
+ return `${where} (line ${row.line})`;
112
+ }
113
+ /**
114
+ * Run the cross-checks over a phase output. Returns empty results for any
115
+ * document without findings tables, so non-defect phases are untouched.
116
+ *
117
+ * @param content - The primary output's markdown.
118
+ * @param opts.gate - Whether the pairing violation is an error (current scaffold) or a warning.
119
+ */
120
+ export function crossCheckFindings(content, opts) {
121
+ const findings = parseFindingsTables(content);
122
+ const errors = [];
123
+ const warnings = [];
124
+ if (findings.length === 0)
125
+ return { errors, warnings, findings };
126
+ for (const row of findings) {
127
+ if (UNSETTLED_EVIDENCE_LEVELS.has(row.evidence) && SETTLED_FIX_ACTIONS.has(row.action)) {
128
+ const message = `${describe(row)}: evidence level "${row.evidence}" cannot carry the settled action "${row.action}" — ` +
129
+ `use "${RUNTIME_VERIFY_ACTION}" or "port differently" ("investigate" on maintenance pipelines), and list the finding under ## Open Questions.`;
130
+ if (opts.gate)
131
+ errors.push(message);
132
+ else
133
+ warnings.push(`${message} Warning only: this workspace's scaffold predates the verify-at-runtime vocabulary — refresh it to make this gating.`);
134
+ }
135
+ if (row.evidence === "observed fact" && row.action === RUNTIME_VERIFY_ACTION) {
136
+ warnings.push(`${describe(row)}: "observed fact" paired with "${RUNTIME_VERIFY_ACTION}" contradicts itself — a settled label with an unsettled action. Pick the one that is true.`);
137
+ }
138
+ }
139
+ const unsettled = findings.filter((row) => UNSETTLED_EVIDENCE_LEVELS.has(row.evidence) || row.action === RUNTIME_VERIFY_ACTION);
140
+ const openQuestions = parseOpenQuestionsTable(content);
141
+ if (unsettled.length > 0 && openQuestions.present && openQuestions.rows === 0) {
142
+ warnings.push(`${unsettled.length} unsettled finding(s) but the ## Open Questions table is empty — each open question / external-behavior claim finding needs a row there so the hedge travels with the finding, not only with the handoff.`);
143
+ }
144
+ return { errors, warnings, findings };
145
+ }
@@ -4,6 +4,7 @@ export * from "./yaml.ts";
4
4
  export * from "./status.ts";
5
5
  export * from "./amendment.ts";
6
6
  export * from "./pipeline.ts";
7
+ export * from "./findings.ts";
7
8
  export * from "./prompts.ts";
8
9
  export * from "./workspace.ts";
9
10
  export * from "./completion.ts";
@@ -13,3 +14,4 @@ export * from "./guide.ts";
13
14
  export * from "./dashboard.ts";
14
15
  export * from "./library.ts";
15
16
  export * from "./synthesis.ts";
17
+ export * from "./broadside.ts";
@@ -7,6 +7,7 @@ export * from "./yaml.js";
7
7
  export * from "./status.js";
8
8
  export * from "./amendment.js";
9
9
  export * from "./pipeline.js";
10
+ export * from "./findings.js";
10
11
  export * from "./prompts.js";
11
12
  export * from "./workspace.js";
12
13
  export * from "./completion.js";
@@ -16,3 +17,4 @@ export * from "./guide.js";
16
17
  export * from "./dashboard.js";
17
18
  export * from "./library.js";
18
19
  export * from "./synthesis.js";
20
+ export * from "./broadside.js";
@@ -75,9 +75,39 @@ export interface LibraryIndex {
75
75
  namespaces: string[];
76
76
  entries: LibraryIndexEntry[];
77
77
  }
78
+ /** One older version whose recorded source_repo names a different repository than the newest version's. */
79
+ export interface ProvenanceConflictVersion {
80
+ version: number;
81
+ source_repo: string;
82
+ }
83
+ /** An entry whose version history spans more than one repository. */
84
+ export interface ProvenanceConflict {
85
+ slug: string;
86
+ namespace?: string;
87
+ /** The newest version — the one whose metadata the index reports for the whole entry. */
88
+ latest_version: number;
89
+ /** The source_repo recorded on the newest version, i.e. what index.yaml advertises. */
90
+ source_repo: string;
91
+ /** Older versions that disagree with it, ascending. Versions with missing or unreadable metadata are skipped. */
92
+ disagreeing_versions: ProvenanceConflictVersion[];
93
+ }
94
+ /**
95
+ * What `reindex` returns: the index it wrote, plus findings that ride on the
96
+ * return value only. `provenance_conflicts` is never serialized into
97
+ * index.yaml or INDEX.md — both shapes are ABI (docs/library-format.md).
98
+ */
99
+ export interface ReindexResult extends LibraryIndex {
100
+ provenance_conflicts: ProvenanceConflict[];
101
+ }
78
102
  export declare function discoverLibrary(libraryPath: string): Promise<LibraryMarker | null>;
79
103
  export declare function readMarker(libraryRoot: string): Promise<LibraryMarker | null>;
80
104
  export declare function writeMarker(libraryRoot: string, marker: LibraryMarker): Promise<void>;
105
+ /**
106
+ * The level a marker's `visibility` or an entry's `confidentiality` is taken
107
+ * to have when it declares none. It is the default `initLibrary` writes and
108
+ * the default docs/library-format.md gives the entry field.
109
+ */
110
+ export declare const DEFAULT_VISIBILITY: LibraryVisibility;
81
111
  export interface InitLibraryOptions {
82
112
  /** Library name (defaults to basename of the path). */
83
113
  name?: string;
@@ -108,6 +138,20 @@ export declare function isValidSlug(slug: string): boolean;
108
138
  * to ask the user about it.
109
139
  */
110
140
  export declare function deriveSlug(sourceRepo: string): string;
141
+ /**
142
+ * Reduce a repo reference to a comparable form so that spellings of the same
143
+ * repository do not read as different projects. Handles scheme, `git@host:path`
144
+ * SCP syntax, a `www.` host prefix, a trailing `.git`, repeated and trailing
145
+ * slashes, backslash separators, and case.
146
+ *
147
+ * This is deliberately conservative: it only collapses spellings that are
148
+ * unambiguously the same target. Anything it cannot prove equivalent stays
149
+ * distinct, because the caller treats "different" as a hard error. Case is the
150
+ * one place that cuts the other way — see the note above the return.
151
+ */
152
+ export declare function normalizeSourceRepo(sourceRepo: string): string;
153
+ /** True when two repo references denote the same repository. */
154
+ export declare function sameSourceRepo(a: string, b: string): boolean;
111
155
  export interface PublishInput {
112
156
  slug: string;
113
157
  namespace?: string;
@@ -131,6 +175,23 @@ export interface PublishOptions {
131
175
  forceNewVersion?: boolean;
132
176
  /** Skip the regen of index.yaml + INDEX.md (caller will batch). */
133
177
  skipReindex?: boolean;
178
+ /**
179
+ * Permit publishing when the target entry's recorded `source_repo` differs
180
+ * from the incoming one. Off by default: a mismatch usually means two
181
+ * different projects derived the same slug, and continuing would append
182
+ * one project's spec to the other's version history. Set this only when
183
+ * the repository genuinely moved (rename, org transfer, host change).
184
+ */
185
+ allowSourceRepoChange?: boolean;
186
+ /**
187
+ * Permit publishing when the entry's `confidentiality` is more restricted
188
+ * than the library's `visibility` — an `internal` entry into a `shared` or
189
+ * `public` library, a `shared` entry into a `public` one. Off by default:
190
+ * that direction exposes the spec to everyone the library reaches. Set
191
+ * this only when the exposure is intended. It does not change the
192
+ * confidentiality recorded on the entry.
193
+ */
194
+ allowConfidentialityMismatch?: boolean;
134
195
  }
135
196
  export interface PublishResult {
136
197
  slug: string;
@@ -140,6 +201,17 @@ export interface PublishResult {
140
201
  entryDir: string;
141
202
  versionDir: string;
142
203
  }
204
+ /**
205
+ * Thrown by `publishEntry` when the entry is more restricted than the library
206
+ * it is headed for. Nothing has been written when this is raised. It carries
207
+ * the two compared levels so a wrapper with a user to ask (Pi) can pose the
208
+ * question from the values rather than by matching the message.
209
+ */
210
+ export declare class ConfidentialityMismatchError extends Error {
211
+ readonly entryConfidentiality: LibraryVisibility;
212
+ readonly libraryVisibility: LibraryVisibility;
213
+ constructor(message: string, entryConfidentiality: LibraryVisibility, libraryVisibility: LibraryVisibility);
214
+ }
143
215
  export declare function publishEntry(libraryRoot: string, spec: string, input: PublishInput, opts?: PublishOptions): Promise<PublishResult>;
144
216
  export interface EntryRef {
145
217
  slug: string;
@@ -160,7 +232,22 @@ export interface ListEntriesFilter {
160
232
  source_repo?: string;
161
233
  }
162
234
  export declare function listEntries(libraryRoot: string, filter?: ListEntriesFilter): Promise<LibraryIndexEntry[]>;
163
- export declare function reindex(libraryRoot: string): Promise<LibraryIndex>;
235
+ export declare function reindex(libraryRoot: string): Promise<ReindexResult>;
236
+ /**
237
+ * Read-only check over the given entries: does every version of each entry
238
+ * record the same repository as its newest version? Comparison goes through
239
+ * `sameSourceRepo`, so spellings of one repository (scheme, `.git`, SCP
240
+ * syntax, casing where safe) do not count as disagreement. A version whose
241
+ * metadata is missing, unreadable, or lacks `source_repo` is skipped rather
242
+ * than reported — the stance the publish guard takes — and an entry whose
243
+ * newest version is unreadable is skipped entirely, since there is nothing to
244
+ * compare against. Never writes.
245
+ *
246
+ * A repository that genuinely moved and was re-published with
247
+ * `allowSourceRepoChange` leaves the same on-disk shape as a collision and is
248
+ * reported the same way; the history alone cannot tell the two apart.
249
+ */
250
+ export declare function detectProvenanceConflicts(libraryRoot: string, entries: ReadonlyArray<Pick<LibraryIndexEntry, "slug" | "namespace">>): Promise<ProvenanceConflict[]>;
164
251
  export interface CommitOptions {
165
252
  addAll?: boolean;
166
253
  }