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.
- package/.codecarto/GUIDE.md +16 -3
- package/.codecarto/README.md +3 -0
- package/.codecarto/broadside/SKILL.md +143 -0
- package/.codecarto/broadside/config.yaml +104 -0
- package/.codecarto/findings/architecture/SKILL.md +1 -0
- package/.codecarto/findings/broadside-scout/README.md +20 -0
- package/.codecarto/findings/broadside-scout/SKILL.md +101 -0
- package/.codecarto/findings/contracts/SKILL.md +1 -0
- package/.codecarto/findings/defect-scan/SKILL.md +15 -1
- package/.codecarto/findings/defect-scan/passes/01-logic-and-correctness.md +1 -1
- package/.codecarto/findings/defect-scan/passes/02-error-handling.md +1 -1
- package/.codecarto/findings/defect-scan/passes/03-concurrency-and-resources.md +1 -1
- package/.codecarto/findings/defect-scan/passes/04-security-and-trust.md +1 -1
- package/.codecarto/findings/defect-scan/passes/05-api-contract-violations.md +2 -1
- package/.codecarto/findings/defect-scan/passes/06-config-and-environment.md +1 -1
- package/.codecarto/findings/defect-scan-mechanical/SKILL.md +3 -2
- package/.codecarto/findings/defect-scan-semantic/SKILL.md +2 -0
- package/.codecarto/findings/porting/SKILL.md +2 -1
- package/.codecarto/findings/protocols/SKILL.md +1 -0
- package/.codecarto/findings/reimplementation-spec/SKILL.md +1 -1
- package/.codecarto/skills/spec-delta-application/SKILL.md +3 -1
- package/.codecarto/templates/architecture-map.md +1 -1
- package/.codecarto/templates/backlog-project.md +51 -0
- package/.codecarto/templates/broadside-scout-brief.md +97 -0
- package/.codecarto/templates/defect-report.md +23 -0
- package/.codecarto/templates/mechanical-defects.md +22 -0
- package/.codecarto/templates/reverse-engineering-bundle.md +2 -2
- package/.codecarto/templates/semantic-defects.md +26 -0
- package/.codecarto/{THREAD_LOG.md → templates/thread-log.md} +2 -5
- package/.codecarto/workflow/VALIDATE.md +1 -1
- package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
- package/.codecarto/workflow/pipeline-full-with-audit.yaml +3 -1
- package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +5 -1
- package/.codecarto/workflow/pipeline-scout-first.yaml +275 -0
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +51 -6
- package/agent-skill/codecartographer/SKILL.md +3 -1
- package/agent-skill/codecartographer/references/broadside.md +115 -0
- package/agent-skill/codecartographer/references/deep-audit-synthesis.md +4 -1
- package/agent-skill/codecartographer/references/library.md +2 -2
- package/agent-skill/codecartographer/references/orchestration.md +1 -1
- package/agent-skill/codecartographer/references/pipeline-selection.md +14 -0
- package/dist/core/amendment.js +2 -2
- package/dist/core/broadside.d.ts +421 -0
- package/dist/core/broadside.js +2349 -0
- package/dist/core/completion.d.ts +5 -0
- package/dist/core/completion.js +38 -6
- package/dist/core/dashboard.js +5 -3
- package/dist/core/findings.d.ts +59 -0
- package/dist/core/findings.js +145 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/library.d.ts +88 -1
- package/dist/core/library.js +260 -7
- package/dist/core/orchestrator-config.js +5 -2
- package/dist/core/pipeline.js +16 -0
- package/dist/core/prompts.js +1 -1
- package/dist/core/status.js +23 -7
- package/dist/core/types.d.ts +6 -0
- package/dist/core/utils.d.ts +14 -0
- package/dist/core/utils.js +37 -1
- package/dist/core/workspace.d.ts +17 -0
- package/dist/core/workspace.js +79 -20
- package/dist/core/yaml.js +19 -4
- package/dist/extensions/codecarto/agent-runner.js +6 -0
- package/dist/extensions/codecarto/auto-runner.d.ts +2 -0
- package/dist/extensions/codecarto/broadside-flags.d.ts +26 -0
- package/dist/extensions/codecarto/broadside-flags.js +129 -0
- package/dist/extensions/codecarto/dashboard-narrator.js +1 -1
- package/dist/extensions/codecarto/index.js +270 -18
- package/dist/extensions/codecarto/phase-compaction.js +4 -0
- package/dist/mcp-server/server.d.ts +22 -0
- package/dist/mcp-server/server.js +282 -17
- package/package.json +11 -2
- package/.codecarto/BACKLOG.md +0 -184
- package/.codecarto/CHANGELOG-2026-05-02-feedback-pass.md +0 -118
- 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
|
package/dist/core/completion.js
CHANGED
|
@@ -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 =
|
|
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: ${
|
|
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,
|
|
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
|
}
|
package/dist/core/dashboard.js
CHANGED
|
@@ -452,7 +452,9 @@ function usagePhaseNote(phaseId, status) {
|
|
|
452
452
|
function renderActivityTimeline(runs) {
|
|
453
453
|
if (runs.length === 0)
|
|
454
454
|
return "";
|
|
455
|
-
|
|
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
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/core/index.js
CHANGED
|
@@ -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";
|
package/dist/core/library.d.ts
CHANGED
|
@@ -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<
|
|
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
|
}
|