@bridge_gpt/mcp-server 0.2.66 → 0.2.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -77
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +43 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/commands.generated.js +11 -11
- package/build/conductor/bridge-api-client.js +143 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-implementer-cli.js +22 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +18 -2
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +181 -29
- package/build/conductor/recovery-cli.js +200 -8
- package/build/conductor/recovery-operations.js +69 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/docs.generated.js +8 -3
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +85 -54
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +22 -20
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/env.js +19 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/job-errors.js +25 -0
- package/build/executor/job-runner.js +225 -41
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +51 -14
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +8 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/worker-finalization.js +82 -29
- package/build/executor/worker-guard-hook.js +137 -33
- package/build/executor/worktree.js +82 -1
- package/build/index.js +403 -429
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +3 -3
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +18 -24
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +120 -1
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +30 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +37 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/setup-epic.js +44 -19
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets.js +179 -31
- package/build/tool-error-envelope.js +61 -0
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +3 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +130 -4
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +1 -1
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
* The command does NOT persist a repository-level review policy: that is
|
|
107
107
|
* BAPI-694's. The policy selected here is per-run and is reported as such.
|
|
108
108
|
*/
|
|
109
|
+
import { PRINTED_INSTALL_COMMAND } from "./printed-remedy-commands.js";
|
|
109
110
|
import { bootstrapConductorSupervisorDefaults, classifySupervisorBootstrapFailure, fetchConductorReadiness, previewConductorSupervisorBootstrap, } from "./conductor/bridge-api-client.js";
|
|
110
111
|
import { CONDUCTOR_OPERATOR_RUNBOOK_POINTER, EXECUTOR_PROVISIONING_GUIDANCE, conductorInstallDoctorExitCode, conductorInstallDoctorHasFatal, formatConductorInstallDoctorReport, } from "./conductor/install-doctor.js";
|
|
111
112
|
import { describeNativeLedgerAvailability, } from "./conductor/doctor.js";
|
|
@@ -138,7 +139,7 @@ export function getInstallBridgeConductorUsage() {
|
|
|
138
139
|
return [
|
|
139
140
|
"Usage: install conductor [options]",
|
|
140
141
|
"",
|
|
141
|
-
"Bootstrap Epic
|
|
142
|
+
"Bootstrap the Epic Implementer for this repository: run the unified read-only",
|
|
142
143
|
"doctor, write the safe project-default supervisor posture, optionally scaffold",
|
|
143
144
|
"the claude-review workflow, report the capability matrix, and re-run the doctor.",
|
|
144
145
|
"",
|
|
@@ -183,9 +184,10 @@ export function getInstallBridgeConductorUsage() {
|
|
|
183
184
|
" approves one named server and has no default.",
|
|
184
185
|
" --execution-mode <mode> local | durable (default: durable with",
|
|
185
186
|
" --executor-id, otherwise local).",
|
|
186
|
-
" --policy-file <path> Run policy the profile references
|
|
187
|
-
" run
|
|
188
|
-
" API server
|
|
187
|
+
" --policy-file <path> Run policy the profile references. An unattended",
|
|
188
|
+
" run escalates to a notify.webhook_url reachable",
|
|
189
|
+
" from the API server or, with none declared, to the",
|
|
190
|
+
" account's email (notify.local_sink is not enough).",
|
|
189
191
|
" --dry-run Diagnose and preview only. Performs NO writes:",
|
|
190
192
|
" no POST, no file write, no host MCP config write,",
|
|
191
193
|
" no Git hook write, no install-state or capability",
|
|
@@ -208,7 +210,7 @@ export function getInstallBridgeConductorUsage() {
|
|
|
208
210
|
"nor changed. No file is ever created: when no supported config carries a",
|
|
209
211
|
"a Bridge entry, the run says so and points at the base `install` command.",
|
|
210
212
|
"The MCP server captures its active groups at startup, so a merge activates on",
|
|
211
|
-
"the next launch — restart your MCP client to see the
|
|
213
|
+
"the next launch — restart your MCP client to see the Epic Implementer tools.",
|
|
212
214
|
"",
|
|
213
215
|
"Local observability (a SEPARATE consent): installs or updates this",
|
|
214
216
|
"repository's managed conductor Git hooks (post-commit,",
|
|
@@ -939,7 +941,7 @@ export function formatConductorRestartAdvisory(mergedProfile) {
|
|
|
939
941
|
"client to gain access to the Epic Implementer tools.");
|
|
940
942
|
}
|
|
941
943
|
/** Remediation printed when no supported host config carries a Bridge entry. */
|
|
942
|
-
export const CONDUCTOR_PROFILE_EMPTY_MERGE_REMEDIATION = `run
|
|
944
|
+
export const CONDUCTOR_PROFILE_EMPTY_MERGE_REMEDIATION = `run \`${PRINTED_INSTALL_COMMAND}\` to create and register the host MCP entry, ` +
|
|
943
945
|
"then re-run `install conductor`.";
|
|
944
946
|
/** Remediation printed whenever the managed conductor hooks were not installed. */
|
|
945
947
|
export const CONDUCTOR_HOOKS_MANUAL_REMEDIATION = "re-run `install conductor` and accept the local-observability consent, or run " +
|
package/build/install-bridge.js
CHANGED
|
@@ -171,7 +171,7 @@ import { runInstallBridgeConductorCli, } from "./install-bridge-conductor.js";
|
|
|
171
171
|
// BAPI-1136: the merge-workflow generate-and-PR installer. Domain logic lives in
|
|
172
172
|
// its own module; this file supplies the parser and the process seams.
|
|
173
173
|
import { runMergeWorkflowInstall, } from "./merge-workflow-install.js";
|
|
174
|
-
import { MERGE_WORKFLOW_REMEDY_COMMAND, } from "./conductor/merge-execution-contract.js";
|
|
174
|
+
import { MERGE_WORKFLOW_CODEOWNER_INSTRUCTION, MERGE_WORKFLOW_REMEDY_COMMAND, } from "./conductor/merge-execution-contract.js";
|
|
175
175
|
import { MERGE_WORKFLOW_API_KEY_SECRET_NAME, } from "./merge-workflow-config.js";
|
|
176
176
|
import { runConductorInstallDoctor, EPIC_IMPLEMENTER_PROFILE_TOKEN, } from "./conductor/install-doctor.js";
|
|
177
177
|
import { resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
|
|
@@ -564,7 +564,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
564
564
|
" -h, --help Show this help.",
|
|
565
565
|
"",
|
|
566
566
|
"Subcommands:",
|
|
567
|
-
" install conductor Bootstrap Epic
|
|
567
|
+
" install conductor Bootstrap the Epic Implementer for this repository",
|
|
568
568
|
" (BAPI-679). Runs a unified READ-ONLY doctor that",
|
|
569
569
|
" composes the conductor, executor-preflight,",
|
|
570
570
|
" install-status, and executor service-unit",
|
|
@@ -1548,7 +1548,7 @@ export function parseMergeWorkflowInstallArgs(argv) {
|
|
|
1548
1548
|
/** Usage text for the merge-workflow installer. */
|
|
1549
1549
|
export function getMergeWorkflowInstallUsage() {
|
|
1550
1550
|
return [
|
|
1551
|
-
`Usage: ${MERGE_WORKFLOW_REMEDY_COMMAND}
|
|
1551
|
+
`Usage: ${MERGE_WORKFLOW_REMEDY_COMMAND} [--dry-run]\n${MERGE_WORKFLOW_CODEOWNER_INSTRUCTION}`,
|
|
1552
1552
|
"",
|
|
1553
1553
|
"Generate the Epic Implementer merge workflow plus its CODEOWNERS fragment and",
|
|
1554
1554
|
"repository-rule guidance, commit them on a managed branch, push with your own",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BAPI-1363 — the bounded git probe behind `get_phase_context`.
|
|
3
|
+
*
|
|
4
|
+
* `phase-result-artifacts.ts` is filesystem-only by contract, so the question "is
|
|
5
|
+
* the recorded checkpoint actually in this worktree's history?" is answered here
|
|
6
|
+
* and passed to it as plain values. Every call is shell-free, rooted at the
|
|
7
|
+
* server-resolved project root, and bounded by a short timeout.
|
|
8
|
+
*
|
|
9
|
+
* Nothing in this module throws on a git problem. A failed probe degrades to
|
|
10
|
+
* `"unknown"` with a closed-vocabulary reason, which the artifact layer treats as
|
|
11
|
+
* "not evidence", so work is redone rather than trusted. Raw stderr, command text,
|
|
12
|
+
* and paths never leave this module.
|
|
13
|
+
*/
|
|
14
|
+
import { execFile } from "child_process";
|
|
15
|
+
/** Bound on each local git call. */
|
|
16
|
+
export const PHASE_CONTEXT_GIT_TIMEOUT_MS = 5000;
|
|
17
|
+
const FULL_HEX_SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
18
|
+
/** Build a runner over `execFileImpl` (the real `execFile` unless a test injects one). */
|
|
19
|
+
export function createPhaseContextGitRunner(execFileImpl = execFile) {
|
|
20
|
+
return (cwd, args) => new Promise((resolve, reject) => {
|
|
21
|
+
execFileImpl("git", args, { cwd, shell: false, timeout: PHASE_CONTEXT_GIT_TIMEOUT_MS, windowsHide: true }, (err, stdout) => {
|
|
22
|
+
if (err)
|
|
23
|
+
reject(err);
|
|
24
|
+
else
|
|
25
|
+
resolve(String(stdout));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Closed-vocabulary classification of a failed git call. Raw error text never escapes. */
|
|
30
|
+
function classifyGitFailure(err) {
|
|
31
|
+
const code = err?.code;
|
|
32
|
+
return code === "ENOENT" ? "git_unavailable" : "probe_failed";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Capture HEAD once, then — only when a checkpoint SHA was recorded — probe its
|
|
36
|
+
* ancestry against that same captured HEAD.
|
|
37
|
+
*
|
|
38
|
+
* Only git's ordinary not-ancestor exit (status 1) is `false`. Anything else that
|
|
39
|
+
* goes wrong is `"unknown"`, never a guess.
|
|
40
|
+
*/
|
|
41
|
+
export async function evaluateCheckpointReachability(options) {
|
|
42
|
+
const run = options.run ?? createPhaseContextGitRunner();
|
|
43
|
+
let head;
|
|
44
|
+
try {
|
|
45
|
+
head = (await run(options.root, ["rev-parse", "HEAD"])).trim();
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const reason = classifyGitFailure(err);
|
|
49
|
+
return { reachable: "unknown", reason: reason === "git_unavailable" ? reason : "head_unavailable" };
|
|
50
|
+
}
|
|
51
|
+
if (!FULL_HEX_SHA.test(head))
|
|
52
|
+
return { reachable: "unknown", reason: "head_unavailable" };
|
|
53
|
+
if (options.checkpointSha === undefined) {
|
|
54
|
+
return { head, reachable: "unknown", reason: "not_evaluated" };
|
|
55
|
+
}
|
|
56
|
+
if (!FULL_HEX_SHA.test(options.checkpointSha)) {
|
|
57
|
+
return { head, reachable: "unknown", reason: "invalid_sha" };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
await run(options.root, ["merge-base", "--is-ancestor", options.checkpointSha, head]);
|
|
61
|
+
return { head, reachable: true };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err?.code === 1)
|
|
65
|
+
return { head, reachable: false };
|
|
66
|
+
return { head, reachable: "unknown", reason: classifyGitFailure(err) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -59,7 +59,7 @@ export const PHASE_RESULT_ARTIFACT_VERSION = 1;
|
|
|
59
59
|
* it reaches the filesystem, rather than being sanitized into something that
|
|
60
60
|
* silently addresses a different ticket's artifact.
|
|
61
61
|
*/
|
|
62
|
-
const TICKET_KEY_PATTERN = /^[A-Za-z][A-Za-z0-
|
|
62
|
+
const TICKET_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]*-\d+$/;
|
|
63
63
|
/** Terminal dispositions that mean "this step is done; do not redo it". */
|
|
64
64
|
const REENTRY_TERMINAL_DISPOSITIONS = new Set([
|
|
65
65
|
"executed",
|
|
@@ -74,6 +74,24 @@ const REENTRY_TERMINAL_DISPOSITIONS = new Set([
|
|
|
74
74
|
*/
|
|
75
75
|
export const INLINE_ROUTING_SOURCE = "inline:get_plan_metadata+phase_result_artifacts";
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
|
+
// Shapes
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Why a checkpoint is not trusted as commit evidence. Closed vocabulary: these
|
|
81
|
+
* tokens are the only diagnostics a response may carry about a failed probe, so
|
|
82
|
+
* no raw git output, path, or credential can ride along.
|
|
83
|
+
*/
|
|
84
|
+
export const CHECKPOINT_STALE_REASONS = [
|
|
85
|
+
"not_ancestor",
|
|
86
|
+
"git_unavailable",
|
|
87
|
+
"invalid_sha",
|
|
88
|
+
"probe_failed",
|
|
89
|
+
"head_unavailable",
|
|
90
|
+
"checkpoint_changed",
|
|
91
|
+
"coverage_mismatch",
|
|
92
|
+
"not_evaluated",
|
|
93
|
+
];
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
77
95
|
// Path safety
|
|
78
96
|
// ---------------------------------------------------------------------------
|
|
79
97
|
function assertValidTicketKey(ticketKey) {
|
|
@@ -265,6 +283,9 @@ export async function loadInlinePhaseContext(options) {
|
|
|
265
283
|
const fingerprint = planFingerprint(metadata);
|
|
266
284
|
let ledger = [];
|
|
267
285
|
const artifactPaths = [];
|
|
286
|
+
// Latest record index per produce step, within the produce artifact's own list.
|
|
287
|
+
const produceLatestIndex = new Map();
|
|
288
|
+
let produceRecordCount = 0;
|
|
268
289
|
// Canonical phase order, not directory order: the ledger is chronological and
|
|
269
290
|
// production always precedes the phases that verify it.
|
|
270
291
|
for (const priorPhase of PLAN_PHASES) {
|
|
@@ -277,21 +298,72 @@ export async function loadInlinePhaseContext(options) {
|
|
|
277
298
|
// Re-check against the CURRENT metadata: an artifact that was valid when
|
|
278
299
|
// written must still describe steps this phase owns under the plan in play now.
|
|
279
300
|
const routable = assertStoredRecordsRoutable(artifact.records, metadata, priorPhase, relativePath);
|
|
301
|
+
if (priorPhase === "produce") {
|
|
302
|
+
produceRecordCount = routable.length;
|
|
303
|
+
routable.forEach((record, index) => produceLatestIndex.set(record.stepId, index));
|
|
304
|
+
}
|
|
280
305
|
ledger = [...ledger, ...sanitizePlanStepRecords(routable)];
|
|
281
306
|
artifactPaths.push(relativePath);
|
|
282
307
|
}
|
|
283
308
|
const checkpoint = await readInlineCheckpoint(docsDir, ticketKey);
|
|
284
309
|
if (checkpoint)
|
|
285
310
|
artifactPaths.push(checkpointRelativePath(ticketKey));
|
|
311
|
+
// Reachability is decided by the caller's git probe. Anything other than a
|
|
312
|
+
// confirmed `true` fails toward redo, and a checkpoint that changed between the
|
|
313
|
+
// probe and this load is not the one that was probed.
|
|
314
|
+
let reachable = options.checkpointReachable ?? "unknown";
|
|
315
|
+
let staleReason;
|
|
316
|
+
if (reachable === "unknown") {
|
|
317
|
+
staleReason = options.checkpointUnknownReason ?? "not_evaluated";
|
|
318
|
+
}
|
|
319
|
+
else if (reachable === false) {
|
|
320
|
+
staleReason = "not_ancestor";
|
|
321
|
+
}
|
|
322
|
+
if (checkpoint &&
|
|
323
|
+
reachable !== "unknown" &&
|
|
324
|
+
options.probedCheckpoint !== undefined &&
|
|
325
|
+
!sameCheckpoint(options.probedCheckpoint, checkpoint)) {
|
|
326
|
+
reachable = "unknown";
|
|
327
|
+
staleReason = "checkpoint_changed";
|
|
328
|
+
}
|
|
329
|
+
// A checkpoint covers the produce prefix only when its binding is intact.
|
|
330
|
+
const coverageIntact = checkpoint !== undefined &&
|
|
331
|
+
checkpoint.producedRecordCount > 0 &&
|
|
332
|
+
checkpoint.producedRecordCount <= produceRecordCount &&
|
|
333
|
+
checkpoint.producePlanFingerprint === fingerprint;
|
|
334
|
+
const coveredCount = checkpoint && reachable === true && coverageIntact
|
|
335
|
+
? checkpoint.producedRecordCount
|
|
336
|
+
: 0;
|
|
337
|
+
if (checkpoint &&
|
|
338
|
+
reachable === true &&
|
|
339
|
+
checkpoint.producedRecordCount > 0 &&
|
|
340
|
+
!coverageIntact) {
|
|
341
|
+
staleReason = "coverage_mismatch";
|
|
342
|
+
}
|
|
286
343
|
const current = currentLedgerState(ledger);
|
|
287
344
|
const currentRecordsByStep = {};
|
|
288
345
|
const terminalStepIds = [];
|
|
346
|
+
const uncommittedStepIds = [];
|
|
289
347
|
for (const [stepId, record] of current) {
|
|
290
348
|
currentRecordsByStep[stepId] = record;
|
|
291
|
-
if (REENTRY_TERMINAL_DISPOSITIONS.has(record.disposition))
|
|
349
|
+
if (!REENTRY_TERMINAL_DISPOSITIONS.has(record.disposition))
|
|
350
|
+
continue;
|
|
351
|
+
const producedAt = produceLatestIndex.get(stepId);
|
|
352
|
+
if (producedAt !== undefined && producedAt >= coveredCount) {
|
|
353
|
+
// Latest produce record is not inside the committed prefix: the work it
|
|
354
|
+
// describes is not evidenced at the evaluated HEAD.
|
|
355
|
+
uncommittedStepIds.push(stepId);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
292
358
|
terminalStepIds.push(stepId);
|
|
359
|
+
}
|
|
293
360
|
}
|
|
294
361
|
terminalStepIds.sort((a, b) => a - b);
|
|
362
|
+
uncommittedStepIds.sort((a, b) => a - b);
|
|
363
|
+
const checkpointIsEvidence = checkpoint !== undefined && reachable === true && staleReason === undefined;
|
|
364
|
+
const ledgerNote = uncommittedStepIds.length > 0
|
|
365
|
+
? "Produce steps listed in uncommittedStepIds are recorded as done but no reachable checkpoint covers them, so redo them (checking for equivalent existing work first) and do not report them as already done."
|
|
366
|
+
: "";
|
|
295
367
|
return {
|
|
296
368
|
phase,
|
|
297
369
|
routingSource: INLINE_ROUTING_SOURCE,
|
|
@@ -306,12 +378,40 @@ export async function loadInlinePhaseContext(options) {
|
|
|
306
378
|
ledger,
|
|
307
379
|
currentRecordsByStep,
|
|
308
380
|
terminalStepIds,
|
|
381
|
+
uncommittedStepIds,
|
|
382
|
+
ledgerNote,
|
|
309
383
|
unresolved: unresolvedLedgerRecords(ledger),
|
|
310
|
-
...(
|
|
384
|
+
...(options.evaluatedHead ? { evaluatedHead: options.evaluatedHead } : {}),
|
|
385
|
+
...(checkpoint && checkpointIsEvidence
|
|
386
|
+
? { checkpoint: { branch: checkpoint.branch, sha: checkpoint.sha } }
|
|
387
|
+
: {}),
|
|
388
|
+
...(checkpoint && !checkpointIsEvidence
|
|
389
|
+
? {
|
|
390
|
+
staleCheckpoint: {
|
|
391
|
+
branch: checkpoint.branch,
|
|
392
|
+
sha: checkpoint.sha,
|
|
393
|
+
reason: staleReason ?? "not_evaluated",
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
: {}),
|
|
311
397
|
artifactPaths,
|
|
312
398
|
};
|
|
313
399
|
}
|
|
314
|
-
|
|
400
|
+
function sameCheckpoint(a, b) {
|
|
401
|
+
return (a.branch === b.branch &&
|
|
402
|
+
a.sha === b.sha &&
|
|
403
|
+
a.producedRecordCount === b.producedRecordCount &&
|
|
404
|
+
a.producePlanFingerprint === b.producePlanFingerprint);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Read and structurally validate the checkpoint artifact.
|
|
408
|
+
*
|
|
409
|
+
* Filesystem only. A legacy checkpoint with no coverage fields reads as covering
|
|
410
|
+
* zero produce records; it is never upgraded by inferring coverage from the
|
|
411
|
+
* current ledger. Malformed coverage, or a mis-versioned artifact, fails closed.
|
|
412
|
+
*/
|
|
413
|
+
export async function readInlineCheckpoint(docsDir, ticketKey) {
|
|
414
|
+
ticketKey = assertValidTicketKey(ticketKey);
|
|
315
415
|
const relativePath = checkpointRelativePath(ticketKey);
|
|
316
416
|
const absolutePath = resolveWithinRoot(docsDir, relativePath);
|
|
317
417
|
const parsed = await readJsonArtifact(absolutePath, relativePath);
|
|
@@ -333,7 +433,69 @@ async function readInlineCheckpoint(docsDir, ticketKey) {
|
|
|
333
433
|
artifact.sha.trim() === "") {
|
|
334
434
|
throw new PlanLedgerError(`${relativePath} is missing its branch or sha`);
|
|
335
435
|
}
|
|
336
|
-
|
|
436
|
+
const hasCount = artifact.produce_record_count !== undefined;
|
|
437
|
+
const fingerprintValue = artifact.produce_plan_fingerprint;
|
|
438
|
+
if (!hasCount) {
|
|
439
|
+
if (fingerprintValue !== undefined && fingerprintValue !== null) {
|
|
440
|
+
throw new PlanLedgerError(`${relativePath} has a produce fingerprint but no record count`);
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
branch: artifact.branch,
|
|
444
|
+
sha: artifact.sha,
|
|
445
|
+
producedRecordCount: 0,
|
|
446
|
+
producePlanFingerprint: null,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const count = artifact.produce_record_count;
|
|
450
|
+
if (typeof count !== "number" || !Number.isInteger(count) || count < 0) {
|
|
451
|
+
throw new PlanLedgerError(`${relativePath} has an invalid produce_record_count`);
|
|
452
|
+
}
|
|
453
|
+
if (count > 0 && (typeof fingerprintValue !== "string" || fingerprintValue === "")) {
|
|
454
|
+
throw new PlanLedgerError(`${relativePath} covers produce records but has no valid produce_plan_fingerprint`);
|
|
455
|
+
}
|
|
456
|
+
if (count === 0 &&
|
|
457
|
+
fingerprintValue !== undefined &&
|
|
458
|
+
fingerprintValue !== null &&
|
|
459
|
+
typeof fingerprintValue !== "string") {
|
|
460
|
+
throw new PlanLedgerError(`${relativePath} has an invalid produce_plan_fingerprint`);
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
branch: artifact.branch,
|
|
464
|
+
sha: artifact.sha,
|
|
465
|
+
producedRecordCount: count,
|
|
466
|
+
producePlanFingerprint: typeof fingerprintValue === "string" && fingerprintValue !== "" ? fingerprintValue : null,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* How much of the produce artifact a checkpoint written now would cover.
|
|
471
|
+
*
|
|
472
|
+
* Structural validation only, with no plan metadata fetch: the count and the
|
|
473
|
+
* artifact's stored fingerprint are read as written. An absent artifact covers
|
|
474
|
+
* zero records, and nothing is created to calculate that.
|
|
475
|
+
*/
|
|
476
|
+
async function readProduceCoverage(docsDir, ticketKey) {
|
|
477
|
+
const relativePath = phaseResultRelativePath(ticketKey, "produce");
|
|
478
|
+
const absolutePath = resolveWithinRoot(docsDir, relativePath);
|
|
479
|
+
const parsed = await readJsonArtifact(absolutePath, relativePath);
|
|
480
|
+
if (parsed === undefined)
|
|
481
|
+
return { count: 0, fingerprint: null };
|
|
482
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
483
|
+
throw new PlanLedgerError(`${relativePath} is not a phase-result artifact object`);
|
|
484
|
+
}
|
|
485
|
+
const artifact = parsed;
|
|
486
|
+
if (artifact.artifact_version !== PHASE_RESULT_ARTIFACT_VERSION) {
|
|
487
|
+
throw new PlanLedgerError(`${relativePath} has unsupported artifact_version ${String(artifact.artifact_version)}; expected ${PHASE_RESULT_ARTIFACT_VERSION}`);
|
|
488
|
+
}
|
|
489
|
+
if (artifact.ticket_key !== ticketKey || artifact.phase !== "produce") {
|
|
490
|
+
throw new PlanLedgerError(`${relativePath} does not describe the produce phase of '${ticketKey}'`);
|
|
491
|
+
}
|
|
492
|
+
if (typeof artifact.plan_fingerprint !== "string" || artifact.plan_fingerprint === "") {
|
|
493
|
+
throw new PlanLedgerError(`${relativePath} has no plan_fingerprint`);
|
|
494
|
+
}
|
|
495
|
+
if (!Array.isArray(artifact.records)) {
|
|
496
|
+
throw new PlanLedgerError(`${relativePath} has no records array`);
|
|
497
|
+
}
|
|
498
|
+
return { count: artifact.records.length, fingerprint: artifact.plan_fingerprint };
|
|
337
499
|
}
|
|
338
500
|
// ---------------------------------------------------------------------------
|
|
339
501
|
// Writing
|
|
@@ -435,6 +597,10 @@ export async function writeInlineCheckpoint(options) {
|
|
|
435
597
|
if (checkpoint.pushed !== true || checkpoint.remoteMatchesHead !== true) {
|
|
436
598
|
throw new PlanLedgerError("checkpoint must report pushed and remoteMatchesHead true; a checkpoint that is not on origin is not a checkpoint");
|
|
437
599
|
}
|
|
600
|
+
// Coverage is derived here from the produce artifact as it stands, never taken
|
|
601
|
+
// from the caller: the push facts above are the caller's claim, but what the
|
|
602
|
+
// push covers is a fact about the ledger this module owns.
|
|
603
|
+
const coverage = await readProduceCoverage(docsDir, ticketKey);
|
|
438
604
|
const relativePath = checkpointRelativePath(ticketKey);
|
|
439
605
|
const absolutePath = resolveWithinRoot(docsDir, relativePath);
|
|
440
606
|
const artifact = {
|
|
@@ -444,6 +610,8 @@ export async function writeInlineCheckpoint(options) {
|
|
|
444
610
|
sha,
|
|
445
611
|
pushed: true,
|
|
446
612
|
remote_matches_head: true,
|
|
613
|
+
produce_record_count: coverage.count,
|
|
614
|
+
produce_plan_fingerprint: coverage.fingerprint,
|
|
447
615
|
};
|
|
448
616
|
await writeAtomically(absolutePath, relativePath, artifact);
|
|
449
617
|
return { branch, sha, artifactPath: relativePath };
|