@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.
- package/DOCS_INDEX.md +13 -8
- package/EXECUTION_STATE.md +20 -0
- package/LOOP_ENGINEERING.md +81 -0
- package/LOOP_SYSTEM_DESIGN.md +32 -0
- package/PROTOCOL_INTEGRATION.md +46 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +31 -9
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/THREAT_MODEL.md +39 -0
- package/docs/ARTIFACT_REFERENCE.md +140 -0
- package/docs/CLI_REFERENCE.md +294 -3
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +22 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/MCP.md +33 -0
- package/docs/RECIPES.md +67 -0
- package/docs/TROUBLESHOOTING.md +106 -2
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
- package/docs/diagrams/README.md +55 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
- package/package.json +8 -6
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/hypothesis-disposition.schema.json +16 -0
- package/schemas/intervention.schema.json +27 -0
- package/schemas/policy-lock.schema.json +1 -0
- package/schemas/policy-snapshot.schema.json +2 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +94 -0
- package/src/commands/action-authorize.js +41 -0
- package/src/commands/action-propose.js +10 -0
- package/src/commands/action-reconcile.js +10 -0
- package/src/commands/action-record.js +47 -0
- package/src/commands/action-show.js +10 -0
- package/src/commands/action-verify.js +10 -0
- package/src/commands/advance.js +7 -2
- package/src/commands/approval-request.js +64 -0
- package/src/commands/approval-resolve.js +10 -0
- package/src/commands/baseline.js +3 -3
- package/src/commands/eval.js +6 -0
- package/src/commands/history.js +18 -0
- package/src/commands/init.js +2 -2
- package/src/commands/inspect.js +49 -0
- package/src/commands/metrics.js +7 -0
- package/src/commands/next.js +8 -2
- package/src/commands/policy-discover.js +2 -2
- package/src/commands/record-diagnosis.js +37 -1
- package/src/commands/record-hypothesis-disposition.js +45 -0
- package/src/commands/record-intervention.js +35 -0
- package/src/commands/reflect.js +38 -0
- package/src/commands/report.js +9 -1
- package/src/commands/run-action.js +18 -0
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +21 -13
- package/src/core/action-authorization.js +106 -0
- package/src/core/action-constants.js +86 -0
- package/src/core/action-execution.js +105 -0
- package/src/core/action-ledger-projection.js +302 -0
- package/src/core/action-model.js +581 -0
- package/src/core/action-readiness.js +141 -0
- package/src/core/action-reconciliation-policy.js +49 -0
- package/src/core/action-reconciliation.js +66 -0
- package/src/core/action-verification.js +111 -0
- package/src/core/actions.js +462 -0
- package/src/core/approvals.js +405 -0
- package/src/core/artifact-registry.js +48 -0
- package/src/core/audit.js +25 -0
- package/src/core/bundles.js +15 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +210 -1
- package/src/core/command-executors.js +171 -15
- package/src/core/command-runtime.js +12 -1
- package/src/core/completion-artifacts.js +37 -12
- package/src/core/completion-recovery-rebind.js +194 -0
- package/src/core/completion.js +70 -0
- package/src/core/continuity-reconciliation.js +24 -5
- package/src/core/diagnostic-model.js +396 -0
- package/src/core/diagnostic-projection.js +51 -0
- package/src/core/diagnostic-record.js +360 -0
- package/src/core/error-codes.js +343 -0
- package/src/core/events.js +41 -1
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +26 -188
- package/src/core/failure-signature.js +70 -0
- package/src/core/failure-surface.js +57 -0
- package/src/core/history.js +110 -0
- package/src/core/hypothesis-projection.js +85 -0
- package/src/core/information-gain-projection.js +283 -0
- package/src/core/information-gain.js +138 -0
- package/src/core/inspect.js +105 -7
- package/src/core/integration-invocation-policy.js +47 -0
- package/src/core/integration-resources.js +51 -0
- package/src/core/next-action-model.js +35 -1
- package/src/core/next-action.js +459 -3
- package/src/core/phase.js +40 -21
- package/src/core/policy-engine.js +113 -6
- package/src/core/preflight-consistency.js +31 -5
- package/src/core/preflight.js +19 -2
- package/src/core/prepared-execution.js +227 -0
- package/src/core/progress.js +41 -4
- package/src/core/protocol-info.js +48 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +15 -12
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +8 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-paths.js +28 -0
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +8 -0
- package/src/core/trace.js +548 -0
- package/src/core/trajectory-evaluation.js +71 -0
- package/src/core/trajectory-metrics.js +80 -0
- package/src/core/transaction.js +8 -0
- package/src/core/work-state.js +10 -5
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
package/src/core/execution.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
194
|
-
argv: commandArgv,
|
|
195
|
-
resolution,
|
|
68
|
+
argv,
|
|
196
69
|
details,
|
|
197
70
|
authorityContext,
|
|
198
71
|
runtimeContext,
|
|
199
72
|
});
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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 !==
|
|
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
|
+
}
|