@cassiomc1/forgeloop 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/DOCS_INDEX.md +13 -8
  2. package/EXECUTION_STATE.md +20 -0
  3. package/LOOP_ENGINEERING.md +81 -0
  4. package/LOOP_SYSTEM_DESIGN.md +32 -0
  5. package/PROTOCOL_INTEGRATION.md +46 -0
  6. package/QUALITY_SCORECARD.md +2 -0
  7. package/README.md +31 -9
  8. package/THIRD_PARTY_NOTICES.md +15 -0
  9. package/THREAT_MODEL.md +39 -0
  10. package/docs/ARTIFACT_REFERENCE.md +140 -0
  11. package/docs/CLI_REFERENCE.md +294 -3
  12. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  13. package/docs/DOCUMENTATION_GUIDE.md +22 -13
  14. package/docs/EXECUTION_TRACE.md +76 -0
  15. package/docs/MCP.md +33 -0
  16. package/docs/RECIPES.md +67 -0
  17. package/docs/TROUBLESHOOTING.md +106 -2
  18. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  19. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  20. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  21. package/docs/diagrams/README.md +55 -0
  22. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  23. package/docs/diagrams/manifest.json +42 -0
  24. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  25. package/package.json +8 -6
  26. package/schemas/action.schema.json +100 -0
  27. package/schemas/approval.schema.json +51 -0
  28. package/schemas/capability-policy.schema.json +41 -0
  29. package/schemas/diagnostic-case.schema.json +85 -0
  30. package/schemas/execution-receipt.schema.json +16 -0
  31. package/schemas/hypothesis-disposition.schema.json +16 -0
  32. package/schemas/intervention.schema.json +27 -0
  33. package/schemas/policy-lock.schema.json +1 -0
  34. package/schemas/policy-snapshot.schema.json +2 -0
  35. package/schemas/trajectory-evaluation.schema.json +64 -0
  36. package/schemas/trajectory-scenario.schema.json +42 -0
  37. package/src/cli.js +94 -0
  38. package/src/commands/action-authorize.js +41 -0
  39. package/src/commands/action-propose.js +10 -0
  40. package/src/commands/action-reconcile.js +10 -0
  41. package/src/commands/action-record.js +47 -0
  42. package/src/commands/action-show.js +10 -0
  43. package/src/commands/action-verify.js +10 -0
  44. package/src/commands/advance.js +7 -2
  45. package/src/commands/approval-request.js +64 -0
  46. package/src/commands/approval-resolve.js +10 -0
  47. package/src/commands/baseline.js +3 -3
  48. package/src/commands/eval.js +6 -0
  49. package/src/commands/history.js +18 -0
  50. package/src/commands/init.js +2 -2
  51. package/src/commands/inspect.js +49 -0
  52. package/src/commands/metrics.js +7 -0
  53. package/src/commands/next.js +8 -2
  54. package/src/commands/policy-discover.js +2 -2
  55. package/src/commands/record-diagnosis.js +37 -1
  56. package/src/commands/record-hypothesis-disposition.js +45 -0
  57. package/src/commands/record-intervention.js +35 -0
  58. package/src/commands/reflect.js +38 -0
  59. package/src/commands/report.js +9 -1
  60. package/src/commands/run-action.js +18 -0
  61. package/src/commands/trace.js +34 -0
  62. package/src/commands/validate-protocol.js +21 -13
  63. package/src/core/action-authorization.js +106 -0
  64. package/src/core/action-constants.js +86 -0
  65. package/src/core/action-execution.js +105 -0
  66. package/src/core/action-ledger-projection.js +302 -0
  67. package/src/core/action-model.js +581 -0
  68. package/src/core/action-readiness.js +141 -0
  69. package/src/core/action-reconciliation-policy.js +49 -0
  70. package/src/core/action-reconciliation.js +66 -0
  71. package/src/core/action-verification.js +111 -0
  72. package/src/core/actions.js +462 -0
  73. package/src/core/approvals.js +405 -0
  74. package/src/core/artifact-registry.js +48 -0
  75. package/src/core/audit.js +25 -0
  76. package/src/core/bundles.js +15 -0
  77. package/src/core/capability-policy.js +226 -0
  78. package/src/core/cli-command-definitions.js +210 -1
  79. package/src/core/command-executors.js +171 -15
  80. package/src/core/command-runtime.js +12 -1
  81. package/src/core/completion-artifacts.js +37 -12
  82. package/src/core/completion-recovery-rebind.js +194 -0
  83. package/src/core/completion.js +70 -0
  84. package/src/core/continuity-reconciliation.js +24 -5
  85. package/src/core/diagnostic-model.js +396 -0
  86. package/src/core/diagnostic-projection.js +51 -0
  87. package/src/core/diagnostic-record.js +360 -0
  88. package/src/core/error-codes.js +343 -0
  89. package/src/core/events.js +41 -1
  90. package/src/core/execution-prerequisites.js +4 -1
  91. package/src/core/execution.js +26 -188
  92. package/src/core/failure-signature.js +70 -0
  93. package/src/core/failure-surface.js +57 -0
  94. package/src/core/history.js +110 -0
  95. package/src/core/hypothesis-projection.js +85 -0
  96. package/src/core/information-gain-projection.js +283 -0
  97. package/src/core/information-gain.js +138 -0
  98. package/src/core/inspect.js +105 -7
  99. package/src/core/integration-invocation-policy.js +47 -0
  100. package/src/core/integration-resources.js +51 -0
  101. package/src/core/next-action-model.js +35 -1
  102. package/src/core/next-action.js +459 -3
  103. package/src/core/phase.js +40 -21
  104. package/src/core/policy-engine.js +113 -6
  105. package/src/core/preflight-consistency.js +31 -5
  106. package/src/core/preflight.js +19 -2
  107. package/src/core/prepared-execution.js +227 -0
  108. package/src/core/progress.js +41 -4
  109. package/src/core/protocol-info.js +48 -0
  110. package/src/core/protocol.js +14 -0
  111. package/src/core/receipt.js +1 -0
  112. package/src/core/reconcile-closure.js +15 -12
  113. package/src/core/reflection.js +305 -0
  114. package/src/core/resumability.js +57 -3
  115. package/src/core/schema-validation.js +8 -0
  116. package/src/core/strategy-analysis.js +97 -0
  117. package/src/core/task-paths.js +28 -0
  118. package/src/core/task-snapshot.js +53 -0
  119. package/src/core/templates.js +8 -0
  120. package/src/core/trace.js +548 -0
  121. package/src/core/trajectory-evaluation.js +71 -0
  122. package/src/core/trajectory-metrics.js +80 -0
  123. package/src/core/transaction.js +8 -0
  124. package/src/core/work-state.js +10 -5
  125. package/docs/assets/forgeloop-flow.svg +0 -1
  126. package/docs/forgeloop-flow.mmd +0 -51
@@ -1,5 +1,3 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
1
  import { readdir } from "node:fs/promises";
4
2
  import path from "node:path";
5
3
  import { ensureWithin, fileExists } from "./filesystem.js";
@@ -7,17 +5,19 @@ import {
7
5
  ARTIFACT_PATHS,
8
6
  executionArtifactPath,
9
7
  readJsonArtifact,
10
- writeJsonArtifact,
11
8
  } from "./artifacts.js";
12
9
  import { taskArtifactPath, taskExecutionPath } from "./task-paths.js";
13
- import {
14
- resolveExecutionResolution,
15
- validateVerificationAuthority,
16
- E_COMMAND_RESOLUTION_AMBIGUOUS,
17
- } from "./verification-capability.js";
18
10
 
19
- export { E_COMMAND_RESOLUTION_AMBIGUOUS };
20
- export const EXECUTION_KIND = "COMMAND_EXECUTION";
11
+ export { E_COMMAND_RESOLUTION_AMBIGUOUS } from "./verification-capability.js";
12
+ import {
13
+ prepareCommandExecution,
14
+ runPreparedCommandExecution,
15
+ } from "./prepared-execution.js";
16
+ export {
17
+ prepareCommandExecution,
18
+ runPreparedCommandExecution,
19
+ TERMINATION_GRACE_MS_PREPARED as TERMINATION_GRACE_MS,
20
+ } from "./prepared-execution.js";
21
21
 
22
22
  function executionError(code, message, artifacts = []) {
23
23
  const error = new Error(message);
@@ -26,111 +26,6 @@ function executionError(code, message, artifacts = []) {
26
26
  return error;
27
27
  }
28
28
 
29
- function normalizeArgv(argv) {
30
- if (!Array.isArray(argv) || argv.length === 0 || argv.some((item) => typeof item !== "string" || item.trim() === "")) {
31
- throw executionError("E_EXECUTION_INVALID", "Execution argv must contain at least one non-empty string");
32
- }
33
- return [...argv];
34
- }
35
-
36
- function validateAuthorityBeforeLaunch({ target, taskId, argv, resolution, details, authorityContext, runtimeContext }) {
37
- if (!resolution.mayInstall) return;
38
- const check = {
39
- kind: "command",
40
- source: argv[0],
41
- details: {
42
- ...(details ?? {}),
43
- execution: { resolution },
44
- },
45
- };
46
- const authority = validateVerificationAuthority(check, {
47
- target,
48
- taskId,
49
- authorityContext,
50
- runtimeContext,
51
- });
52
- if (!authority.valid) {
53
- throw executionError(authority.error.code ?? "E_INSTALLATION_AUTHORITY_REQUIRED", authority.error.message);
54
- }
55
- }
56
-
57
- const MAX_CAPTURED_OUTPUT_BYTES = 64 * 1024;
58
- export const TERMINATION_GRACE_MS = 1_000;
59
-
60
- function digest(bytes) {
61
- return createHash("sha256").update(bytes).digest("hex");
62
- }
63
-
64
- function executeProcess(argv, cwd, { timeoutMs = null } = {}) {
65
- return new Promise((resolve) => {
66
- let spawnError = null;
67
- let timedOut = false;
68
- let settled = false;
69
- let timeout = null;
70
- let forceTermination = null;
71
- const stdout = [];
72
- const stderr = [];
73
- let stdoutBytes = 0;
74
- let stderrBytes = 0;
75
- let outputTruncated = false;
76
- const capture = (chunks, chunk, total) => {
77
- const available = MAX_CAPTURED_OUTPUT_BYTES - total;
78
- if (available <= 0) {
79
- outputTruncated = true;
80
- return total;
81
- }
82
- if (chunk.length > available) {
83
- chunks.push(chunk.subarray(0, available));
84
- outputTruncated = true;
85
- return total + available;
86
- }
87
- chunks.push(chunk);
88
- return total + chunk.length;
89
- };
90
- const finish = (result) => {
91
- if (settled) return;
92
- settled = true;
93
- if (timeout) clearTimeout(timeout);
94
- if (forceTermination) clearTimeout(forceTermination);
95
- resolve({
96
- ...result,
97
- timedOut,
98
- stdout: Buffer.concat(stdout),
99
- stderr: Buffer.concat(stderr),
100
- stdoutBytes,
101
- stderrBytes,
102
- outputTruncated,
103
- });
104
- };
105
- try {
106
- const child = spawn(argv[0], argv.slice(1), {
107
- cwd,
108
- shell: false,
109
- stdio: ["ignore", "pipe", "pipe"],
110
- });
111
- child.stdout?.on("data", (chunk) => { stdoutBytes = capture(stdout, chunk, stdoutBytes); });
112
- child.stderr?.on("data", (chunk) => { stderrBytes = capture(stderr, chunk, stderrBytes); });
113
- child.once("error", (error) => {
114
- spawnError = error;
115
- });
116
- child.once("close", (exitCode, signal) => {
117
- finish({ exitCode, signal, spawnError });
118
- });
119
- if (Number.isInteger(timeoutMs) && timeoutMs > 0) {
120
- timeout = setTimeout(() => {
121
- timedOut = true;
122
- child.kill("SIGTERM");
123
- forceTermination = setTimeout(() => {
124
- child.kill("SIGKILL");
125
- }, TERMINATION_GRACE_MS);
126
- }, timeoutMs);
127
- }
128
- } catch (error) {
129
- finish({ exitCode: null, signal: null, spawnError: error });
130
- }
131
- });
132
- }
133
-
134
29
  /**
135
30
  * Resolves where a new execution artifact should be written. Task-scoped
136
31
  * execution artifacts require a real modern task namespace (a task.json
@@ -148,6 +43,12 @@ export async function resolveExecutionArtifactPath(target, taskId, executionId)
148
43
  return executionArtifactPath(executionId);
149
44
  }
150
45
 
46
+ /**
47
+ * Deterministic pre-launch preparation followed by an exact-argv launch.
48
+ * Kept as the canonical single-command entrypoint for non-durable callers
49
+ * (run-check); durable actions use the two phases separately so that
50
+ * ACTION_STARTED lands exactly on the launch boundary (INV-EXEC-01).
51
+ */
151
52
  export async function runCommandExecution({
152
53
  target,
153
54
  packageRoot,
@@ -162,87 +63,24 @@ export async function runCommandExecution({
162
63
  executionPath,
163
64
  timeoutMs = null,
164
65
  } = {}) {
165
- const commandArgv = normalizeArgv(argv);
166
- const resolution = await resolveExecutionResolution({
167
- argv: commandArgv,
168
- cwd: target,
169
- });
170
-
171
- if (
172
- resolution.resolutionMode === "UNKNOWN"
173
- && resolution.mayInstall === true
174
- && (
175
- resolution.reason === "NPM_WORKSPACE_SCRIPT_UNRESOLVED"
176
- || resolution.reason === "NPM_SUBCOMMAND_AMBIGUOUS"
177
- || resolution.reason === "NPM_COMMAND_UNCLASSIFIED"
178
- || resolution.reason === "NPM_OPTION_VALUE_AMBIGUOUS"
179
- )
180
- ) {
181
- const error = new Error(
182
- resolution.reason === "NPM_WORKSPACE_SCRIPT_UNRESOLVED"
183
- ? "npm workspace script execution cannot be proven from the current target. Run ForgeLoop against the selected workspace directory."
184
- : "Command execution context could not be proven safe before launch."
185
- );
186
- error.code = E_COMMAND_RESOLUTION_AMBIGUOUS;
187
- error.resolution = resolution;
188
- throw error;
189
- }
190
-
191
- validateAuthorityBeforeLaunch({
66
+ const prepared = await prepareCommandExecution({
192
67
  target,
193
- taskId,
194
- argv: commandArgv,
195
- resolution,
68
+ argv,
196
69
  details,
197
70
  authorityContext,
198
71
  runtimeContext,
199
72
  });
200
-
201
- const executionId = `exec-${randomUUID()}`;
202
- const startedAt = new Date().toISOString();
203
- const processResult = await executeProcess(commandArgv, target, { timeoutMs });
204
- const finishedAt = new Date().toISOString();
205
- const execution = {
206
- schemaVersion: 1,
207
- protocolVersion: 1,
208
- executionId,
73
+ return runPreparedCommandExecution({
74
+ target,
75
+ packageRoot,
209
76
  taskId,
210
77
  checkId,
211
78
  requirement,
212
79
  verificationCycle,
213
- kind: EXECUTION_KIND,
214
- argv: commandArgv,
215
- cwd: target,
216
- resolution: {
217
- resolutionMode: resolution.resolutionMode,
218
- mayInstall: resolution.mayInstall,
219
- installer: resolution.installer,
220
- tool: resolution.tool,
221
- },
222
- ...(resolution.dispatch ? { dispatch: resolution.dispatch } : {}),
223
- startedAt,
224
- finishedAt,
225
- status: processResult.exitCode === 0 && !processResult.spawnError && !processResult.timedOut ? "passed" : "failed",
226
- exitCode: processResult.exitCode,
227
- durationMs: Math.max(0, Date.parse(finishedAt) - Date.parse(startedAt)),
228
- termination: processResult.spawnError ? "spawn-error" : processResult.timedOut ? "timeout" : processResult.signal ? "signal" : "exit",
229
- signal: processResult.signal ?? null,
230
- stdoutSha256: digest(processResult.stdout),
231
- stderrSha256: digest(processResult.stderr),
232
- stdoutBytes: processResult.stdoutBytes,
233
- stderrBytes: processResult.stderrBytes,
234
- outputTruncated: processResult.outputTruncated,
235
- ...(Number.isInteger(timeoutMs) && timeoutMs > 0 ? { timeoutMs, terminationGraceMs: TERMINATION_GRACE_MS } : {}),
236
- };
237
- const execPath = executionPath ?? await resolveExecutionArtifactPath(target, taskId, executionId);
238
- const written = await writeJsonArtifact(target, execPath, execution, "execution", packageRoot);
239
- return {
240
- path: written.path,
241
- execution: written.value,
242
- result: processResult.spawnError
243
- ? "process failed to start"
244
- : `process exited with code ${processResult.exitCode}`,
245
- };
80
+ prepared,
81
+ timeoutMs,
82
+ executionPath,
83
+ });
246
84
  }
247
85
 
248
86
  export async function readExecutionArtifact({ target, executionRef, packageRoot, taskId } = {}) {
@@ -287,7 +125,7 @@ export async function readExecutionArtifact({ target, executionRef, packageRoot,
287
125
  }
288
126
 
289
127
  export function validateExecutionBinding({ execution, taskId, checkId, requirement, verificationCycle = 1 } = {}) {
290
- if (!execution || execution.kind !== EXECUTION_KIND
128
+ if (!execution || execution.kind !== "COMMAND_EXECUTION"
291
129
  || execution.taskId !== taskId
292
130
  || execution.checkId !== checkId
293
131
  || execution.requirement !== requirement
@@ -0,0 +1,70 @@
1
+ import { canonicalFingerprint } from "./artifacts.js";
2
+
3
+ export function computeFailureSignature({ requirement, checkKind = null, status, exitCode = null, failureToken = null }) {
4
+ if (!requirement || typeof requirement !== "string") {
5
+ const error = new Error("failure signature requires a requirement");
6
+ error.code = "E_FAILURE_SIGNATURE_INVALID";
7
+ throw error;
8
+ }
9
+ if (!status || typeof status !== "string") {
10
+ const error = new Error("failure signature requires a status");
11
+ error.code = "E_FAILURE_SIGNATURE_INVALID";
12
+ throw error;
13
+ }
14
+ return canonicalFingerprint({
15
+ requirement,
16
+ ...(checkKind ? { checkKind } : {}),
17
+ status,
18
+ ...(Number.isInteger(exitCode) ? { exitCode } : {}),
19
+ ...(failureToken ? { failureToken: String(failureToken) } : {}),
20
+ });
21
+ }
22
+
23
+ function failureTokenOf(details) {
24
+ if (typeof details?.failureToken === "string" && details.failureToken) return details.failureToken;
25
+ if (typeof details?.details?.failureToken === "string" && details.details.failureToken) return details.details.failureToken;
26
+ return null;
27
+ }
28
+
29
+ export function projectFailureSignatures({ state = null, events = [] } = {}) {
30
+ const byCycle = new Map();
31
+ const record = (cycle, details) => {
32
+ if (details.status !== "failed" && details.status !== "blocked") return;
33
+ const requirement = details.requirement ?? details.id ?? details.checkId;
34
+ if (!requirement) return;
35
+ const signature = computeFailureSignature({
36
+ requirement,
37
+ status: details.status,
38
+ exitCode: Number.isInteger(details.exitCode) ? details.exitCode : null,
39
+ failureToken: failureTokenOf(details),
40
+ });
41
+ if (!byCycle.has(cycle)) byCycle.set(cycle, new Map());
42
+ const cycleMap = byCycle.get(cycle);
43
+ if (!cycleMap.has(signature)) cycleMap.set(signature, { signature, requirements: new Set(), cycles: new Set() });
44
+ cycleMap.get(signature).requirements.add(requirement);
45
+ cycleMap.get(signature).cycles.add(cycle);
46
+ };
47
+
48
+ for (const event of events) {
49
+ if (event.event !== "VERIFICATION_RECORDED") continue;
50
+ record(event.details?.verificationCycle ?? 1, event.details ?? {});
51
+ }
52
+ for (const check of state?.checks ?? []) {
53
+ if (check.status !== "failed" && check.status !== "blocked") continue;
54
+ record(check.details?.verificationCycle ?? state?.verificationCycle ?? 1, check);
55
+ }
56
+
57
+ const signaturesByIdentity = new Map();
58
+ for (const [, cycleMap] of [...byCycle.entries()].sort(([a], [b]) => a - b)) {
59
+ for (const [signature, entry] of cycleMap) {
60
+ if (!signaturesByIdentity.has(signature)) signaturesByIdentity.set(signature, { signature, cycles: [], requirements: new Set() });
61
+ const aggregated = signaturesByIdentity.get(signature);
62
+ aggregated.cycles.push(...entry.cycles);
63
+ for (const requirement of entry.requirements) aggregated.requirements.add(requirement);
64
+ }
65
+ }
66
+
67
+ return [...signaturesByIdentity.values()]
68
+ .map((entry) => ({ ...entry, cycles: [...new Set(entry.cycles)].sort((a, b) => a - b), requirements: [...entry.requirements].sort() }))
69
+ .sort((a, b) => a.signature.localeCompare(b.signature));
70
+ }
@@ -0,0 +1,57 @@
1
+ export function projectFailureSurfaces({ state = null, events = [] } = {}) {
2
+ const failedRequirementsByCycle = new Map();
3
+ // Track every canonically verified cycle so a successful verification
4
+ // appears explicitly as surface: [] instead of silently disappearing.
5
+ const knownCycles = new Set();
6
+ const record = (cycle, requirement) => {
7
+ if (!Number.isInteger(cycle)) cycle = Number(cycle) || 1;
8
+ knownCycles.add(cycle);
9
+ if (!failedRequirementsByCycle.has(cycle)) failedRequirementsByCycle.set(cycle, new Set());
10
+ if (!requirement) return;
11
+ failedRequirementsByCycle.get(cycle).add(requirement);
12
+ };
13
+ const knowCycle = (cycle) => {
14
+ if (Number.isInteger(cycle)) knownCycles.add(cycle);
15
+ };
16
+
17
+ for (const event of events) {
18
+ if (event.event === "VERIFICATION_STARTED") {
19
+ knowCycle(event.details?.verificationCycle);
20
+ }
21
+ if (event.event !== "VERIFICATION_RECORDED") continue;
22
+ const d = event.details ?? {};
23
+ if (d.status === "failed" || d.status === "blocked") {
24
+ record(d.verificationCycle ?? 1, d.requirement ?? d.id ?? d.checkId);
25
+ } else {
26
+ knowCycle(d.verificationCycle);
27
+ }
28
+ }
29
+
30
+ for (const check of state?.checks ?? []) {
31
+ knowCycle(check.details?.verificationCycle ?? state?.verificationCycle ?? 1);
32
+ if (check.status !== "failed" && check.status !== "blocked") continue;
33
+ record(check.details?.verificationCycle ?? state?.verificationCycle ?? 1, check.requirement ?? check.id ?? check.checkId);
34
+ }
35
+ for (const cycle of knownCycles) {
36
+ if (!failedRequirementsByCycle.has(cycle)) failedRequirementsByCycle.set(cycle, new Set());
37
+ }
38
+
39
+ return [...failedRequirementsByCycle.entries()]
40
+ .sort(([a], [b]) => a - b)
41
+ .map(([verificationCycle, requirements]) => ({
42
+ verificationCycle,
43
+ surface: [...requirements].sort(),
44
+ size: requirements.size,
45
+ }));
46
+ }
47
+
48
+ export function compareFailureSurface(previous, current) {
49
+ if (!previous || !current) return { changed: false, direction: "UNCHANGED" };
50
+ const previousSet = new Set(previous);
51
+ const currentSet = new Set(current);
52
+ const removed = [...previousSet].filter((item) => !currentSet.has(item));
53
+ const added = [...currentSet].filter((item) => !previousSet.has(item));
54
+ if (added.length > 0) return { changed: true, direction: "EXPANDED", added, removed };
55
+ if (removed.length > 0) return { changed: true, direction: "REDUCED", added, removed };
56
+ return { changed: false, direction: "UNCHANGED", added, removed };
57
+ }
@@ -0,0 +1,110 @@
1
+ import { buildTaskTrace } from "./trace.js";
2
+
3
+ export const HISTORY_FILTER_OPTIONS = Object.freeze([
4
+ "type",
5
+ "phase",
6
+ "failures",
7
+ "checks",
8
+ "since",
9
+ "until",
10
+ "limit",
11
+ ]);
12
+
13
+ export async function buildTaskHistory({
14
+ target,
15
+ packageRoot,
16
+ taskId = null,
17
+ filters = {},
18
+ } = {}) {
19
+ const trace = await buildTaskTrace({ target, packageRoot, taskId });
20
+ let events = [...trace.events];
21
+
22
+ if (filters.type) {
23
+ const types = String(filters.type).split(",").map((value) => value.trim()).filter(Boolean);
24
+ if (types.length > 0) {
25
+ events = events.filter((event) => types.includes(event.type) || types.includes(event.category));
26
+ }
27
+ }
28
+ if (filters.phase) {
29
+ const phases = String(filters.phase).split(",").map((value) => value.trim()).filter(Boolean);
30
+ if (phases.length > 0) events = events.filter((event) => phases.includes(event.phase));
31
+ }
32
+ if (filters.failures) {
33
+ events = events.filter((event) => event.category === "verification"
34
+ && ["failed", "blocked"].includes(String(event.data?.status ?? "")));
35
+ }
36
+ if (filters.checks) {
37
+ events = events.filter((event) => event.category === "verification");
38
+ }
39
+ if (filters.since) {
40
+ const since = Date.parse(filters.since);
41
+ if (!Number.isNaN(since)) events = events.filter((event) => event.timestamp && Date.parse(event.timestamp) >= since);
42
+ }
43
+ if (filters.until) {
44
+ const until = Date.parse(filters.until);
45
+ if (!Number.isNaN(until)) events = events.filter((event) => event.timestamp && Date.parse(event.timestamp) <= until);
46
+ }
47
+
48
+ let omittedEvents = 0;
49
+ if (Number.isInteger(filters.limit) && filters.limit >= 0 && events.length > filters.limit) {
50
+ omittedEvents = events.length - filters.limit;
51
+ events = events.slice(-filters.limit);
52
+ }
53
+
54
+ const checkAttempts = trace.checks.reduce(
55
+ (total, check) => total + check.attemptCount,
56
+ 0,
57
+ );
58
+ const failedAttempts = trace.checks.reduce(
59
+ (total, check) => total + check.failedAttempts,
60
+ 0,
61
+ );
62
+
63
+ return {
64
+ schemaVersion: 1,
65
+ command: "history",
66
+ task: trace.task,
67
+ snapshot: trace.snapshot,
68
+ summary: {
69
+ eventCount: events.length,
70
+ totalEventCount: trace.events.length,
71
+ checkAttemptCount: checkAttempts,
72
+ failedAttemptCount: failedAttempts,
73
+ diagnosticCaseCount: trace.diagnostics.cases.length,
74
+ interventionCount: trace.diagnostics.interventions.length,
75
+ },
76
+ historyQuality: trace.historyQuality,
77
+ integrity: trace.integrity,
78
+ events,
79
+ ...(omittedEvents > 0 ? { truncated: true, truncation: { reason: "OUTPUT_LIMIT", omittedEvents } } : {}),
80
+ };
81
+ }
82
+
83
+ export function formatHistoryEvent(event) {
84
+ const time = event.timestamp && !Number.isNaN(Date.parse(event.timestamp))
85
+ ? new Date(event.timestamp).toISOString().slice(11, 19)
86
+ : "--:--:--";
87
+ const lines = [`${time} ${event.type}`];
88
+ if (event.summary && event.summary !== event.type) lines.push(` ${event.summary}`);
89
+ if (event.data?.provenance) lines.push(` provenance: ${event.data.provenance}`);
90
+ return lines.join("\n");
91
+ }
92
+
93
+ export function formatHistoryResult(result) {
94
+ const lines = [
95
+ "ForgeLoop Execution History",
96
+ "─".repeat(56),
97
+ "",
98
+ `Task: ${result.task.id ?? "unknown"}`,
99
+ `Phase: ${result.task.phase ?? "UNKNOWN"}`,
100
+ `Integrity: ${result.integrity.valid ? "VALID" : "INCONSISTENT"}`,
101
+ `History quality: ${result.historyQuality.level}`,
102
+ result.historyQuality.reasons.length > 0 ? `Reasons: ${result.historyQuality.reasons.join(", ")}` : null,
103
+ "",
104
+ ...result.events.map(formatHistoryEvent),
105
+ ].filter((line) => line !== null);
106
+ if (result.truncated) {
107
+ lines.push("", `[truncated: ${result.truncation.omittedEvents} earlier events omitted]`);
108
+ }
109
+ return lines.join("\n") + "\n";
110
+ }
@@ -0,0 +1,85 @@
1
+ import { DISPOSITION_TRANSITIONS } from "./diagnostic-model.js";
2
+
3
+ const TERMINAL_STATUSES = Object.freeze(["FALSIFIED", "SUPERSEDED", "UNRESOLVED"]);
4
+ export const HYPOTHESIS_LEGACY_ID = "h-legacy";
5
+
6
+ function canTransition(from, to) {
7
+ if (from === to) return false;
8
+ if (TERMINAL_STATUSES.includes(from)) return false;
9
+ return (DISPOSITION_TRANSITIONS[from] ?? []).includes(to);
10
+ }
11
+
12
+ function blankState(id) {
13
+ return {
14
+ id,
15
+ sourceEventSeq: null,
16
+ sourceCycle: null,
17
+ initialStatus: "OPEN",
18
+ currentStatus: "OPEN",
19
+ dispositionHistory: [],
20
+ };
21
+ }
22
+
23
+ export function projectHypothesisStates(events, { taskId = null } = {}) {
24
+ const ordered = [...events].sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
25
+ const byId = new Map();
26
+ const invalidTransitions = [];
27
+
28
+ const ensure = (id, seq, cycle) => {
29
+ if (!byId.has(id)) {
30
+ const state = blankState(id);
31
+ state.sourceEventSeq = seq ?? null;
32
+ state.sourceCycle = cycle ?? null;
33
+ byId.set(id, state);
34
+ }
35
+ return byId.get(id);
36
+ };
37
+
38
+ for (const event of ordered) {
39
+ if (taskId && event.taskId && event.taskId !== taskId) continue;
40
+ const details = event.details ?? {};
41
+ if (event.event === "DIAGNOSTIC_CASE_RECORDED") {
42
+ for (const hypothesis of details.hypotheses ?? []) {
43
+ if (!hypothesis?.id) continue;
44
+ ensure(hypothesis.id, event.seq, details.verificationCycle);
45
+ }
46
+ } else if (event.event === "DIAGNOSIS_RECORDED") {
47
+ ensure(HYPOTHESIS_LEGACY_ID, event.seq, details.verificationCycle);
48
+ } else if (event.event === "HYPOTHESIS_DISPOSITION_RECORDED") {
49
+ const ref = details.hypothesisRef;
50
+ if (!ref || !details.status) continue;
51
+ const state = ensure(ref, null, null);
52
+ if (!canTransition(state.currentStatus, details.status)) {
53
+ invalidTransitions.push({
54
+ sequence: event.seq,
55
+ hypothesisRef: ref,
56
+ from: state.currentStatus,
57
+ to: details.status,
58
+ });
59
+ continue;
60
+ }
61
+ state.currentStatus = details.status;
62
+ state.dispositionHistory.push({
63
+ sequence: event.seq,
64
+ status: details.status,
65
+ evidenceRefs: [...(details.evidenceRefs ?? [])],
66
+ reason: details.reason ?? null,
67
+ });
68
+ }
69
+ }
70
+
71
+ const hypotheses = [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
72
+ return {
73
+ hypotheses,
74
+ openHypotheses: hypotheses.filter((hypothesis) => hypothesis.currentStatus === "OPEN").map((hypothesis) => hypothesis.id),
75
+ invalidTransitions,
76
+ };
77
+ }
78
+
79
+ export function getOpenHypotheses(projection) {
80
+ return projection?.openHypotheses ?? [];
81
+ }
82
+
83
+ export function getHypothesisState(projection, id) {
84
+ return projection?.hypotheses.find((hypothesis) => hypothesis.id === id) ?? null;
85
+ }