@cassiomc1/forgeloop 1.8.1 → 1.10.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/.cursor/rules/project-loop.mdc +6 -3
- package/.github/copilot-instructions.md +5 -0
- package/AGENTS.md +6 -0
- package/AGENT_COMPATIBILITY.md +15 -0
- package/CLAUDE.md +6 -0
- package/DELEGATION_PROTOCOL.md +6 -0
- package/DOCS_INDEX.md +9 -2
- package/ENG/accessibility-eng.md +12 -2
- package/ENG/design-code-eng.md +22 -1
- package/LOOP_ENGINEERING.md +41 -0
- package/LOOP_SYSTEM_DESIGN.md +33 -0
- package/ORCHESTRATOR_INTEGRATION.md +9 -0
- package/PROTOCOL_INTEGRATION.md +57 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +51 -0
- package/TERMINOLOGY.md +12 -0
- package/THREAT_MODEL.md +48 -0
- package/completions/_forgeloop +5 -1
- package/completions/forgeloop.bash +9 -1
- package/completions/forgeloop.fish +27 -1
- package/docs/ADVISORY_CONTEXT.md +174 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +33 -2
- package/docs/ARTIFACT_REFERENCE.md +142 -0
- package/docs/CLI_REFERENCE.md +124 -1
- package/docs/CROSS_HARNESS_CONTINUITY.md +85 -0
- package/docs/DOCUMENTATION_GUIDE.md +7 -0
- package/docs/GETTING_STARTED.md +22 -0
- package/docs/KNOWLEDGE_SOURCES.md +171 -0
- package/docs/MCP.md +17 -1
- package/docs/RECIPES.md +111 -0
- package/docs/RELEASE_CHECKLIST.md +14 -0
- package/docs/STRUCTURAL_QUALITY.md +350 -0
- package/docs/TROUBLESHOOTING.md +161 -2
- package/docs/UNIVERSAL_INTEGRATION.md +60 -0
- package/package.json +4 -1
- package/schemas/config.schema.json +46 -0
- package/schemas/handoff-envelope.schema.json +1 -0
- package/schemas/preflight.schema.json +2 -1
- package/schemas/structural-quality.schema.json +175 -0
- package/scripts/check-changelog-freshness.mjs +27 -3
- package/scripts/generate-agent-protocol-summary.mjs +18 -0
- package/src/cli.js +24 -0
- package/src/commands/handoff-accept.js +36 -0
- package/src/commands/handoff-list.js +28 -2
- package/src/commands/handoff-show.js +27 -2
- package/src/commands/quality-baseline.js +28 -0
- package/src/commands/quality-status.js +34 -0
- package/src/commands/quality-verify.js +30 -0
- package/src/commands/reconcile-continuity.js +4 -0
- package/src/core/advisory-context/constants.js +74 -0
- package/src/core/advisory-context/provider.js +287 -0
- package/src/core/advisory-context/service.js +140 -0
- package/src/core/artifact-registry.js +12 -0
- package/src/core/audit.js +38 -0
- package/src/core/bundles.js +134 -1
- package/src/core/cli-command-definitions.js +62 -0
- package/src/core/command-executors.js +28 -0
- package/src/core/command-input.js +23 -1
- package/src/core/completion-artifacts.js +2 -0
- package/src/core/completion.js +42 -0
- package/src/core/config.js +3 -0
- package/src/core/continuity-lint.js +89 -0
- package/src/core/continuity-reconciliation.js +16 -0
- package/src/core/continuity.js +10 -11
- package/src/core/error-codes.js +186 -0
- package/src/core/events.js +32 -0
- package/src/core/execution-profile-context.js +15 -1
- package/src/core/filesystem.js +34 -3
- package/src/core/handoff-acceptance.js +277 -0
- package/src/core/handoff.js +41 -8
- package/src/core/inspect.js +64 -0
- package/src/core/integration-invocation-policy.js +34 -2
- package/src/core/integration-resources.js +38 -1
- package/src/core/next-action-model.js +11 -1
- package/src/core/next-action-phases.js +84 -5
- package/src/core/phase.js +9 -1
- package/src/core/portable-context.js +103 -0
- package/src/core/preflight.js +33 -0
- package/src/core/protocol-info.js +33 -2
- package/src/core/runtime-context.js +58 -0
- package/src/core/schema-validation.js +1 -0
- package/src/core/structural-quality/artifacts.js +329 -0
- package/src/core/structural-quality/constants.js +67 -0
- package/src/core/structural-quality/policy.js +227 -0
- package/src/core/structural-quality/provider.js +287 -0
- package/src/core/structural-quality/sentrux-mcp.js +477 -0
- package/src/core/structural-quality/service.js +1138 -0
- package/src/core/structural-quality/source-fingerprint.js +112 -0
- package/src/core/structural-quality/status.js +3 -0
- package/src/core/task-paths.js +24 -0
- package/src/core/templates.js +1 -0
- package/src/integration.d.ts +141 -0
- package/src/integration.js +36 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
normalizePortableText,
|
|
5
|
+
assertPortableContextSafe,
|
|
6
|
+
} from "../portable-context.js";
|
|
7
|
+
import {
|
|
8
|
+
ADVISORY_CONTEXT_LIMITS,
|
|
9
|
+
normalizeAdvisoryRecallOptions,
|
|
10
|
+
} from "./constants.js";
|
|
11
|
+
import {
|
|
12
|
+
normalizeAdvisoryContextResult,
|
|
13
|
+
resolveAdvisoryContextProvider,
|
|
14
|
+
} from "./provider.js";
|
|
15
|
+
import {
|
|
16
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
17
|
+
E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
|
|
18
|
+
E_ADVISORY_CONTEXT_QUERY_INVALID,
|
|
19
|
+
E_ADVISORY_CONTEXT_TIMEOUT,
|
|
20
|
+
E_PORTABLE_CONTEXT_INVALID,
|
|
21
|
+
} from "../error-codes.js";
|
|
22
|
+
|
|
23
|
+
function serviceError(code, message, cause) {
|
|
24
|
+
const error = new Error(message, cause !== undefined ? { cause } : undefined);
|
|
25
|
+
error.name = "AdvisoryContextServiceError";
|
|
26
|
+
error.code = code;
|
|
27
|
+
return error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function recallAdvisoryContext({
|
|
31
|
+
target,
|
|
32
|
+
taskId,
|
|
33
|
+
providerName,
|
|
34
|
+
query,
|
|
35
|
+
limit,
|
|
36
|
+
maxItemChars,
|
|
37
|
+
maxTotalChars,
|
|
38
|
+
timeoutMs,
|
|
39
|
+
runtimeContext,
|
|
40
|
+
} = {}) {
|
|
41
|
+
if (!target || typeof target !== "string") {
|
|
42
|
+
throw serviceError(
|
|
43
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
44
|
+
"Target project path is required for advisory recall",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!taskId || typeof taskId !== "string") {
|
|
49
|
+
throw serviceError(
|
|
50
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
51
|
+
"taskId is required for advisory recall",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!providerName || typeof providerName !== "string") {
|
|
56
|
+
throw serviceError(
|
|
57
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
58
|
+
"providerName is required for advisory recall",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let normalizedQuery;
|
|
63
|
+
try {
|
|
64
|
+
normalizedQuery = normalizePortableText(query, {
|
|
65
|
+
label: "advisory query",
|
|
66
|
+
maxLength: ADVISORY_CONTEXT_LIMITS.maxQueryChars,
|
|
67
|
+
});
|
|
68
|
+
assertPortableContextSafe(normalizedQuery, { label: "advisory query" });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err.code === E_PORTABLE_CONTEXT_INVALID) {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
throw serviceError(
|
|
74
|
+
E_ADVISORY_CONTEXT_QUERY_INVALID,
|
|
75
|
+
`Advisory context query is invalid: ${err.message}`,
|
|
76
|
+
err,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const effectiveOptions = normalizeAdvisoryRecallOptions({
|
|
81
|
+
limit,
|
|
82
|
+
maxItemChars,
|
|
83
|
+
maxTotalChars,
|
|
84
|
+
timeoutMs,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
let provider;
|
|
88
|
+
try {
|
|
89
|
+
provider = await resolveAdvisoryContextProvider({
|
|
90
|
+
providers: runtimeContext?.advisoryContextProviders,
|
|
91
|
+
providerName,
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
throw serviceError(
|
|
95
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
96
|
+
`Advisory context provider "${providerName}" failed validation: ${err.message}`,
|
|
97
|
+
err,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!provider) {
|
|
102
|
+
throw serviceError(
|
|
103
|
+
E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
|
|
104
|
+
`Advisory context provider "${providerName}" is not registered in runtime context`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let timer;
|
|
109
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
110
|
+
timer = setTimeout(() => {
|
|
111
|
+
reject(
|
|
112
|
+
serviceError(
|
|
113
|
+
E_ADVISORY_CONTEXT_TIMEOUT,
|
|
114
|
+
`Advisory recall from provider "${providerName}" timed out after ${effectiveOptions.timeoutMs}ms`,
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
}, effectiveOptions.timeoutMs);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
let rawResult;
|
|
121
|
+
try {
|
|
122
|
+
const recallPromise = Promise.resolve(
|
|
123
|
+
provider.recall({
|
|
124
|
+
projectPath: path.resolve(target),
|
|
125
|
+
taskId,
|
|
126
|
+
query: normalizedQuery,
|
|
127
|
+
...effectiveOptions,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
rawResult = await Promise.race([recallPromise, timeoutPromise]);
|
|
131
|
+
} finally {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return normalizeAdvisoryContextResult(rawResult, {
|
|
136
|
+
provider,
|
|
137
|
+
taskId,
|
|
138
|
+
...effectiveOptions,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -283,6 +283,18 @@ export const ARTIFACT_REGISTRY = Object.freeze({
|
|
|
283
283
|
isPersisted: true,
|
|
284
284
|
description: "Immutable trajectory evaluation results compiled from the canonical trace against a local reference scenario.",
|
|
285
285
|
}),
|
|
286
|
+
structuralQuality: Object.freeze({
|
|
287
|
+
key: "structuralQuality",
|
|
288
|
+
scope: "TASK",
|
|
289
|
+
path: `${TASK_STATE_ROOT}/<task-key>/${TASK_ARTIFACT_FILES.structuralQuality}/baseline.json`,
|
|
290
|
+
schema: "structural-quality",
|
|
291
|
+
owner: "PROTOCOL_COMPILED",
|
|
292
|
+
mutability: "BASELINE_IMMUTABLE_AFTER_EXECUTION",
|
|
293
|
+
trustRole: "STRUCTURAL_QUALITY_EVIDENCE",
|
|
294
|
+
isPublic: true,
|
|
295
|
+
isPersisted: true,
|
|
296
|
+
description: "Typed, provider-neutral structural-quality baseline and evaluation evidence bound to task, route, policy, scope, and provider identity.",
|
|
297
|
+
}),
|
|
286
298
|
usage: Object.freeze({
|
|
287
299
|
key: "usage",
|
|
288
300
|
scope: "TASK",
|
package/src/core/audit.js
CHANGED
|
@@ -11,6 +11,7 @@ import { validateActionLedgerConsistency } from "./actions.js";
|
|
|
11
11
|
import { readCodeManifest } from "./code-manifest.js";
|
|
12
12
|
import { readAttestationStatement, validateAttestationStatement } from "./attestation.js";
|
|
13
13
|
import { assertAttestationStatementBindings, verifyCodeManifestContent } from "./attestation-verifier.js";
|
|
14
|
+
import { projectStructuralQualityStatus } from "./structural-quality/status.js";
|
|
14
15
|
|
|
15
16
|
function sortErrors(errors) {
|
|
16
17
|
return [...errors].sort((left, right) => left.code.localeCompare(right.code)
|
|
@@ -244,6 +245,33 @@ export async function evaluateAudit({
|
|
|
244
245
|
policyStatus = { status: "NOT_APPLICABLE", provenRules: 0, inertRules: 0, unsupportedRules: 0, baselineViolations: 0, drift: false };
|
|
245
246
|
}
|
|
246
247
|
|
|
248
|
+
let structuralQuality = {
|
|
249
|
+
mode: "off",
|
|
250
|
+
provider: null,
|
|
251
|
+
baseline: { status: "NOT_REQUESTED", qualitySignal: null, artifactRef: null, fingerprint: null },
|
|
252
|
+
current: { status: "NOT_OBSERVED", verificationCycle: null, attempt: null, qualitySignal: null, delta: null, bottleneck: null, artifactRef: null },
|
|
253
|
+
comparable: null,
|
|
254
|
+
completionRequired: false,
|
|
255
|
+
reasonCodes: [],
|
|
256
|
+
next: null,
|
|
257
|
+
};
|
|
258
|
+
if (taskId) {
|
|
259
|
+
try {
|
|
260
|
+
structuralQuality = await projectStructuralQualityStatus({ target, packageRoot, taskId });
|
|
261
|
+
} catch (error) {
|
|
262
|
+
structuralQuality = {
|
|
263
|
+
...structuralQuality,
|
|
264
|
+
mode: "unknown",
|
|
265
|
+
reasonCodes: [error.code ?? "E_STRUCTURAL_QUALITY_EVIDENCE_STALE"],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const qualityEvidenceKind = structuralQuality.current.status === "PASS" || structuralQuality.current.status === "FAIL"
|
|
270
|
+
? "OBSERVED"
|
|
271
|
+
: structuralQuality.current.status === "BLOCKED" || (structuralQuality.mode === "gate" && structuralQuality.baseline.status !== "OBSERVED")
|
|
272
|
+
? "BLOCKED"
|
|
273
|
+
: structuralQuality.mode === "off" ? "NOT_REQUESTED" : "NOT_VERIFIED";
|
|
274
|
+
|
|
247
275
|
return {
|
|
248
276
|
schemaVersion: 1,
|
|
249
277
|
protocolVersion: PROTOCOL_VERSION,
|
|
@@ -256,6 +284,15 @@ export async function evaluateAudit({
|
|
|
256
284
|
manifest: Boolean(manifest),
|
|
257
285
|
},
|
|
258
286
|
policy: policyStatus,
|
|
287
|
+
structuralQuality: {
|
|
288
|
+
...structuralQuality,
|
|
289
|
+
policy: structuralQuality.mode === "off" ? null : {
|
|
290
|
+
mode: structuralQuality.mode,
|
|
291
|
+
provider: structuralQuality.provider,
|
|
292
|
+
result: structuralQuality.current.status,
|
|
293
|
+
},
|
|
294
|
+
evidenceKind: qualityEvidenceKind,
|
|
295
|
+
},
|
|
259
296
|
completion,
|
|
260
297
|
attestation: {
|
|
261
298
|
mode: attestation.mode,
|
|
@@ -282,6 +319,7 @@ export async function evaluateAudit({
|
|
|
282
319
|
route: routeRel,
|
|
283
320
|
state: stateRel,
|
|
284
321
|
receipt: receiptRel,
|
|
322
|
+
structuralQuality: taskId ? taskArtifactPath(taskId, "structuralQuality") : null,
|
|
285
323
|
},
|
|
286
324
|
};
|
|
287
325
|
}
|
package/src/core/bundles.js
CHANGED
|
@@ -8,7 +8,7 @@ import { validateChecksExecutionProvenance } from "./completion-artifacts.js";
|
|
|
8
8
|
import { readExecutionArtifact } from "./execution.js";
|
|
9
9
|
import { assertContinuitySemantics } from "./continuity.js";
|
|
10
10
|
import { validateEventLedger } from "./events.js";
|
|
11
|
-
import { taskArtifactPath, taskDirectory } from "./task-paths.js";
|
|
11
|
+
import { taskArtifactPath, taskDirectory, taskStructuralQualityDirectory } from "./task-paths.js";
|
|
12
12
|
import { resolveTaskClaimState } from "./task-claim-state.js";
|
|
13
13
|
import { E_TASK_CLAIM_OWNERSHIP_INCONSISTENT } from "./error-codes.js";
|
|
14
14
|
import { listActions } from "./actions.js";
|
|
@@ -19,6 +19,13 @@ import { validateWorkspaceBinding } from "./workspace-binding.js";
|
|
|
19
19
|
import { validateCodeManifest, validateCodeManifestBindings } from "./code-manifest.js";
|
|
20
20
|
import { assertAttestationStatementBindings } from "./attestation-verifier.js";
|
|
21
21
|
import { validateAttestationStatement } from "./attestation.js";
|
|
22
|
+
import {
|
|
23
|
+
listStructuralQualityEvaluations,
|
|
24
|
+
readStructuralQualityBaseline,
|
|
25
|
+
validateStructuralQualityArtifact,
|
|
26
|
+
validateStructuralQualityBindings,
|
|
27
|
+
} from "./structural-quality/artifacts.js";
|
|
28
|
+
import { normalizeStructuralQualityConfig, structuralQualityPolicyFingerprint } from "./structural-quality/policy.js";
|
|
22
29
|
|
|
23
30
|
export const BUNDLE_SCHEMA_VERSION = 1;
|
|
24
31
|
const BUNDLE_ROOT = ".forgeloop/tasks";
|
|
@@ -42,6 +49,96 @@ function bundleBindingError(code, message) {
|
|
|
42
49
|
return error;
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
function structuralQualityBundleKind(artifact) {
|
|
53
|
+
if (artifact === "structural-quality/baseline.json") return "baseline";
|
|
54
|
+
if (/^structural-quality\/evaluations\/cycle-\d+-attempt-\d+\.json$/u.test(artifact)) return "evaluation";
|
|
55
|
+
if (artifact.startsWith("structural-quality/")) {
|
|
56
|
+
throw bundleBindingError("E_BUNDLE_PATH_INVALID", `Unknown structural-quality bundle artifact: ${artifact}`);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function bundleQualityReference(reference, taskId) {
|
|
62
|
+
if (typeof reference !== "string" || reference.trim() === "") {
|
|
63
|
+
throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "Structural-quality check has no artifact reference");
|
|
64
|
+
}
|
|
65
|
+
const normalized = reference.replaceAll("\\", "/");
|
|
66
|
+
const sourceRoot = taskStructuralQualityDirectory(taskId).replaceAll("\\", "/");
|
|
67
|
+
const suffix = normalized.startsWith(`${sourceRoot}/`)
|
|
68
|
+
? normalized.slice(sourceRoot.length + 1)
|
|
69
|
+
: normalized.startsWith("structural-quality/")
|
|
70
|
+
? normalized.slice("structural-quality/".length)
|
|
71
|
+
: null;
|
|
72
|
+
if (!suffix || !(suffix === "baseline.json" || /^evaluations\/cycle-\d+-attempt-\d+\.json$/u.test(suffix))) {
|
|
73
|
+
throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "Structural-quality artifact reference escapes its task quality directory");
|
|
74
|
+
}
|
|
75
|
+
return `structural-quality/${suffix}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function structuralQualityCheckProjection(value) {
|
|
79
|
+
if (value.status === "PASS") return { status: "passed", evidenceKind: "OBSERVED" };
|
|
80
|
+
if (value.status === "FAIL") return { status: "failed", evidenceKind: "OBSERVED" };
|
|
81
|
+
if (value.status === "BLOCKED") return { status: "blocked", evidenceKind: "BLOCKED" };
|
|
82
|
+
return { status: "not-run", evidenceKind: "NOT_VERIFIED" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function assertStructuralQualityBundleEvidence({ loaded, manifest, taskId }) {
|
|
86
|
+
const quality = loaded.structuralQuality;
|
|
87
|
+
if (!quality) return;
|
|
88
|
+
const baseline = quality.baseline;
|
|
89
|
+
const evaluations = quality.evaluations ?? [];
|
|
90
|
+
const qualityArtifacts = new Map();
|
|
91
|
+
if (baseline) qualityArtifacts.set("structural-quality/baseline.json", baseline);
|
|
92
|
+
for (const evaluation of evaluations) {
|
|
93
|
+
const name = `structural-quality/evaluations/cycle-${evaluation.verificationCycle}-attempt-${evaluation.attempt}.json`;
|
|
94
|
+
qualityArtifacts.set(name, evaluation);
|
|
95
|
+
}
|
|
96
|
+
for (const value of [baseline, ...evaluations].filter(Boolean)) {
|
|
97
|
+
if (value.taskId !== taskId) throw bundleBindingError("E_BUNDLE_TASK_MISMATCH", "Structural-quality artifact taskId does not match its bundle task");
|
|
98
|
+
const bindingErrors = validateStructuralQualityBindings(value);
|
|
99
|
+
if (bindingErrors.length > 0) throw bundleBindingError(bindingErrors[0].code, bindingErrors[0].message);
|
|
100
|
+
}
|
|
101
|
+
const baselineFingerprint = baseline ? canonicalFingerprint(baseline) : null;
|
|
102
|
+
const bundledConfig = loaded.config?.structuralQuality;
|
|
103
|
+
let policy = null;
|
|
104
|
+
if (bundledConfig) policy = normalizeStructuralQualityConfig(bundledConfig);
|
|
105
|
+
const expectedContractFingerprint = loaded.contract ? canonicalFingerprint(loaded.contract) : null;
|
|
106
|
+
const expectedRouteFingerprint = loaded.route ? canonicalFingerprint(loaded.route) : null;
|
|
107
|
+
for (const value of [baseline, ...evaluations].filter(Boolean)) {
|
|
108
|
+
if (baselineFingerprint && value.role === "EVALUATION" && value.bindings?.baselineFingerprint !== baselineFingerprint) {
|
|
109
|
+
throw bundleBindingError("E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH", "Structural-quality evaluation does not bind the bundled baseline");
|
|
110
|
+
}
|
|
111
|
+
if (policy && value.bindings?.policyFingerprint !== structuralQualityPolicyFingerprint(policy)) {
|
|
112
|
+
throw bundleBindingError("E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH", "Structural-quality artifact policy binding does not match bundled configuration");
|
|
113
|
+
}
|
|
114
|
+
if (expectedContractFingerprint && value.bindings?.contractFingerprint !== expectedContractFingerprint) {
|
|
115
|
+
throw bundleBindingError("E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH", "Structural-quality contract binding does not match the bundled contract");
|
|
116
|
+
}
|
|
117
|
+
if (expectedRouteFingerprint && value.bindings?.routeFingerprint !== expectedRouteFingerprint) {
|
|
118
|
+
throw bundleBindingError("E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH", "Structural-quality route binding does not match the bundled route");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const checks = [
|
|
122
|
+
...(loaded.state?.checks ?? []),
|
|
123
|
+
...(loaded.receipt?.checks ?? []),
|
|
124
|
+
].filter((check) => check?.kind === "structural-quality");
|
|
125
|
+
for (const check of checks) {
|
|
126
|
+
const bundleReference = bundleQualityReference(check.details?.artifactRef, taskId);
|
|
127
|
+
const value = qualityArtifacts.get(bundleReference);
|
|
128
|
+
if (!value) throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", `Bundled structural-quality check references missing ${bundleReference}`);
|
|
129
|
+
if (check.details?.artifactFingerprint !== canonicalFingerprint(value)) {
|
|
130
|
+
throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", `Bundled structural-quality check fingerprint does not match ${bundleReference}`);
|
|
131
|
+
}
|
|
132
|
+
const expected = structuralQualityCheckProjection(value);
|
|
133
|
+
if (check.status !== expected.status || check.evidenceKind !== expected.evidenceKind) {
|
|
134
|
+
throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", `Bundled structural-quality check does not match ${bundleReference}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const artifact of manifest.artifacts.filter((item) => item.startsWith("structural-quality/"))) {
|
|
138
|
+
if (!qualityArtifacts.has(artifact)) throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", `Bundled structural-quality manifest entry was not loaded: ${artifact}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
45
142
|
function assertBundledCodeManifestBindings({ loaded, ledger, taskId }) {
|
|
46
143
|
const manifest = loaded.codeManifest;
|
|
47
144
|
if (!manifest) return;
|
|
@@ -191,6 +288,22 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
|
|
|
191
288
|
if (destinationName === "verification-scope.json") exportedVerificationScope = copied;
|
|
192
289
|
}
|
|
193
290
|
|
|
291
|
+
// Structural-quality evidence is provider output made portable by
|
|
292
|
+
// ForgeLoop. Copy typed artifacts rather than raw process output, and keep
|
|
293
|
+
// every immutable evaluation so an audit can inspect the complete attempt
|
|
294
|
+
// history without rescanning the project.
|
|
295
|
+
const qualityBaseline = await readStructuralQualityBaseline(target, taskId, packageRoot);
|
|
296
|
+
const qualityEvaluations = await listStructuralQualityEvaluations(target, taskId, packageRoot);
|
|
297
|
+
if (qualityBaseline) {
|
|
298
|
+
await writeJsonArtifact(target, `${directory}/structural-quality/baseline.json`, qualityBaseline.value, "structural-quality", packageRoot);
|
|
299
|
+
artifacts.push("structural-quality/baseline.json");
|
|
300
|
+
}
|
|
301
|
+
for (const evaluation of qualityEvaluations) {
|
|
302
|
+
const destination = `structural-quality/evaluations/cycle-${evaluation.value.verificationCycle}-attempt-${evaluation.value.attempt}.json`;
|
|
303
|
+
await writeJsonArtifact(target, `${directory}/${destination}`, evaluation.value, "structural-quality", packageRoot);
|
|
304
|
+
artifacts.push(destination);
|
|
305
|
+
}
|
|
306
|
+
|
|
194
307
|
if (exportedWorkspaceBinding?.value) {
|
|
195
308
|
exportedWorkspaceBinding.value = await validateWorkspaceBinding(exportedWorkspaceBinding.value, packageRoot);
|
|
196
309
|
if (exportedWorkspaceBinding.value.taskId !== taskId) {
|
|
@@ -335,6 +448,22 @@ export async function readTaskBundle(target, taskId, packageRoot) {
|
|
|
335
448
|
};
|
|
336
449
|
const executions = {};
|
|
337
450
|
for (const artifact of manifest.value.artifacts) {
|
|
451
|
+
const qualityKind = structuralQualityBundleKind(artifact);
|
|
452
|
+
if (qualityKind) {
|
|
453
|
+
const qualityArtifact = await readJsonArtifact(target, `${directory}/${artifact}`, "structural-quality", packageRoot);
|
|
454
|
+
validateStructuralQualityArtifact(qualityArtifact.value, artifact);
|
|
455
|
+
if (qualityArtifact.value.taskId !== taskId) {
|
|
456
|
+
throw bundleBindingError("E_BUNDLE_TASK_MISMATCH", `Structural-quality ${qualityKind} taskId does not match its bundle task`);
|
|
457
|
+
}
|
|
458
|
+
loaded.structuralQuality ??= { baseline: null, evaluations: [] };
|
|
459
|
+
if (qualityKind === "baseline") {
|
|
460
|
+
if (loaded.structuralQuality.baseline) throw bundleBindingError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "A bundle cannot contain more than one structural-quality baseline");
|
|
461
|
+
loaded.structuralQuality.baseline = qualityArtifact.value;
|
|
462
|
+
} else {
|
|
463
|
+
loaded.structuralQuality.evaluations.push(qualityArtifact.value);
|
|
464
|
+
}
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
338
467
|
if (artifact.startsWith("executions/") && artifact.endsWith(".json")) {
|
|
339
468
|
const execution = await readJsonArtifact(target, `${directory}/${artifact}`, "execution", packageRoot);
|
|
340
469
|
executions[execution.value.executionId] = execution.value;
|
|
@@ -415,6 +544,10 @@ export async function readTaskBundle(target, taskId, packageRoot) {
|
|
|
415
544
|
}
|
|
416
545
|
loaded[mapping[0]] = loadedArtifact.value;
|
|
417
546
|
}
|
|
547
|
+
if (loaded.structuralQuality) {
|
|
548
|
+
loaded.structuralQuality.evaluations.sort((left, right) => left.verificationCycle - right.verificationCycle || left.attempt - right.attempt);
|
|
549
|
+
assertStructuralQualityBundleEvidence({ loaded, manifest: manifest.value, taskId });
|
|
550
|
+
}
|
|
418
551
|
const bundledLedger = loaded.codeManifest && manifest.value.artifacts.includes("events.ndjson")
|
|
419
552
|
? await validateEventLedger(target, packageRoot, { taskId, eventsPath: `${directory}/events.ndjson` })
|
|
420
553
|
: null;
|
|
@@ -162,6 +162,51 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
162
162
|
mayExecuteExternalProcess: false,
|
|
163
163
|
description: "Evaluates pre-implementation contract, routing, and gates; synchronizes work state when READY.",
|
|
164
164
|
}),
|
|
165
|
+
"quality-baseline": Object.freeze({
|
|
166
|
+
name: "quality-baseline",
|
|
167
|
+
category: "verification",
|
|
168
|
+
mutation: "EXTERNAL_EXECUTION",
|
|
169
|
+
options: Object.freeze({
|
|
170
|
+
...CLI_COMMON_OPTIONS,
|
|
171
|
+
...CLI_TASK_OPTION,
|
|
172
|
+
"--replace": Object.freeze({ targetKey: "replace", parseType: "boolean", takesValue: false, description: "replace a different baseline before EXECUTING" }),
|
|
173
|
+
"--timeout-ms": Object.freeze({ targetKey: "timeoutMs", parseType: "non-negative-integer", takesValue: true, valueName: "number", missingValueMessage: "--timeout-ms requires a non-negative integer", description: "bounded analyzer timeout in milliseconds" }),
|
|
174
|
+
"--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit baseline result as JSON" }),
|
|
175
|
+
}),
|
|
176
|
+
writes: [".forgeloop/task-state/<taskKey>/structural-quality/baseline.json", ".forgeloop/task-state/<taskKey>/events.ndjson"],
|
|
177
|
+
removes: [],
|
|
178
|
+
mayExecuteExternalProcess: true,
|
|
179
|
+
description: "Captures an immutable provider-neutral structural-quality baseline through the trusted analyzer adapter.",
|
|
180
|
+
}),
|
|
181
|
+
"quality-verify": Object.freeze({
|
|
182
|
+
name: "quality-verify",
|
|
183
|
+
category: "verification",
|
|
184
|
+
mutation: "EXTERNAL_EXECUTION",
|
|
185
|
+
options: Object.freeze({
|
|
186
|
+
...CLI_COMMON_OPTIONS,
|
|
187
|
+
...CLI_TASK_OPTION,
|
|
188
|
+
"--timeout-ms": Object.freeze({ targetKey: "timeoutMs", parseType: "non-negative-integer", takesValue: true, valueName: "number", missingValueMessage: "--timeout-ms requires a non-negative integer", description: "bounded analyzer timeout in milliseconds" }),
|
|
189
|
+
"--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit structural-quality verification as JSON" }),
|
|
190
|
+
}),
|
|
191
|
+
writes: [".forgeloop/task-state/<taskKey>/structural-quality/evaluations/cycle-<n>-attempt-<n>.json", ".forgeloop/task-state/<taskKey>/work-state.json", ".forgeloop/task-state/<taskKey>/execution-receipt.json", ".forgeloop/task-state/<taskKey>/events.ndjson"],
|
|
192
|
+
removes: [],
|
|
193
|
+
mayExecuteExternalProcess: true,
|
|
194
|
+
description: "Captures one bounded structural-quality evaluation and projects it into canonical verification evidence.",
|
|
195
|
+
}),
|
|
196
|
+
"quality-status": Object.freeze({
|
|
197
|
+
name: "quality-status",
|
|
198
|
+
category: "verification",
|
|
199
|
+
mutation: "READ_ONLY",
|
|
200
|
+
options: Object.freeze({
|
|
201
|
+
...CLI_COMMON_OPTIONS,
|
|
202
|
+
...CLI_TASK_OPTION,
|
|
203
|
+
"--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit persisted structural-quality status as JSON" }),
|
|
204
|
+
}),
|
|
205
|
+
writes: [],
|
|
206
|
+
removes: [],
|
|
207
|
+
mayExecuteExternalProcess: false,
|
|
208
|
+
description: "Projects persisted structural-quality baseline and evaluation status without invoking a provider.",
|
|
209
|
+
}),
|
|
165
210
|
advance: Object.freeze({
|
|
166
211
|
name: "advance",
|
|
167
212
|
category: "lifecycle",
|
|
@@ -1127,6 +1172,23 @@ export const CLI_COMMAND_DEFINITIONS = Object.freeze({
|
|
|
1127
1172
|
mayExecuteExternalProcess: false,
|
|
1128
1173
|
description: "Reads and verifies one immutable handoff snapshot.",
|
|
1129
1174
|
}),
|
|
1175
|
+
"handoff-accept": Object.freeze({
|
|
1176
|
+
name: "handoff-accept",
|
|
1177
|
+
category: "task",
|
|
1178
|
+
mutation: "MUTATING",
|
|
1179
|
+
options: Object.freeze({
|
|
1180
|
+
...CLI_COMMON_OPTIONS,
|
|
1181
|
+
...CLI_TASK_OPTION,
|
|
1182
|
+
"--handoff": Object.freeze({ targetKey: "handoffId", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--handoff requires a handoff ID", description: "handoff identifier" }),
|
|
1183
|
+
"--consumer-id": Object.freeze({ targetKey: "consumerId", parseType: "string", takesValue: true, valueName: "id", missingValueMessage: "--consumer-id requires a consumer ID", description: "consumer identifier accepting the handoff" }),
|
|
1184
|
+
"--harness": Object.freeze({ targetKey: "harness", parseType: "string", takesValue: true, valueName: "name", missingValueMessage: "--harness requires a harness name", description: "optional harness accepting the handoff" }),
|
|
1185
|
+
"--json": Object.freeze({ targetKey: "json", parseType: "boolean", takesValue: false, description: "emit acceptance result as JSON" }),
|
|
1186
|
+
}),
|
|
1187
|
+
writes: [".forgeloop/task-state/<taskKey>/events.ndjson"],
|
|
1188
|
+
removes: [],
|
|
1189
|
+
mayExecuteExternalProcess: false,
|
|
1190
|
+
description: "Accepts an immutable handoff exactly once in the task event ledger.",
|
|
1191
|
+
}),
|
|
1130
1192
|
"responsibility-set": Object.freeze({
|
|
1131
1193
|
name: "responsibility-set",
|
|
1132
1194
|
category: "scope",
|
|
@@ -11,6 +11,9 @@ import { runUpdate } from "../commands/update.js";
|
|
|
11
11
|
import { runActivate } from "../commands/activate.js";
|
|
12
12
|
import { runAdvance } from "../commands/advance.js";
|
|
13
13
|
import { runPreflight } from "../commands/preflight.js";
|
|
14
|
+
import { runQualityBaseline } from "../commands/quality-baseline.js";
|
|
15
|
+
import { runQualityVerify } from "../commands/quality-verify.js";
|
|
16
|
+
import { runQualityStatus } from "../commands/quality-status.js";
|
|
14
17
|
import { runComplete } from "../commands/complete.js";
|
|
15
18
|
import { runAudit } from "../commands/audit.js";
|
|
16
19
|
import { runReport } from "../commands/report.js";
|
|
@@ -70,6 +73,7 @@ import { runWorkspaceStatus } from "../commands/workspace-status.js";
|
|
|
70
73
|
import { runHandoffCreate } from "../commands/handoff-create.js";
|
|
71
74
|
import { runHandoffList } from "../commands/handoff-list.js";
|
|
72
75
|
import { runHandoffShow } from "../commands/handoff-show.js";
|
|
76
|
+
import { runHandoffAccept } from "../commands/handoff-accept.js";
|
|
73
77
|
import { runResponsibilitySet } from "../commands/responsibility-set.js";
|
|
74
78
|
import { runResponsibilityStatus } from "../commands/responsibility-status.js";
|
|
75
79
|
import { runVerifyScope } from "../commands/verify-scope.js";
|
|
@@ -132,6 +136,19 @@ export const COMMAND_EXECUTORS = {
|
|
|
132
136
|
const result = await runPreflight({ target, packageRoot, strict: options.strict, taskId: options.taskId });
|
|
133
137
|
return { result, exitCode: result.status === "READY" ? 0 : 1 };
|
|
134
138
|
},
|
|
139
|
+
"quality-baseline": async ({ target, packageRoot, options, runtimeContext }) => {
|
|
140
|
+
const result = await runQualityBaseline({ target, packageRoot, taskId: options.taskId, replace: options.replace, timeoutMs: options.timeoutMs, runtimeContext });
|
|
141
|
+
return { result, exitCode: ["CAPTURED", "EXISTING", "REPLACED", "NOT_REQUESTED"].includes(result.status) ? 0 : 1 };
|
|
142
|
+
},
|
|
143
|
+
"quality-verify": async ({ target, packageRoot, options, authorityContext, runtimeContext }) => {
|
|
144
|
+
const result = await runQualityVerify({ target, packageRoot, taskId: options.taskId, timeoutMs: options.timeoutMs, authorityContext, runtimeContext });
|
|
145
|
+
const status = result.evaluation?.status ?? result.status;
|
|
146
|
+
return { result, exitCode: ["PASS", "NOT_OBSERVED", "CONVERGED", "NOT_REQUESTED"].includes(status) ? 0 : 1 };
|
|
147
|
+
},
|
|
148
|
+
"quality-status": async ({ target, packageRoot, options }) => ({
|
|
149
|
+
result: await runQualityStatus({ target, packageRoot, taskId: options.taskId }),
|
|
150
|
+
exitCode: 0,
|
|
151
|
+
}),
|
|
135
152
|
advance: async ({ target, packageRoot, options, authorityContext, runtimeContext }) => ({
|
|
136
153
|
result: await runAdvance({
|
|
137
154
|
target,
|
|
@@ -220,6 +237,17 @@ export const COMMAND_EXECUTORS = {
|
|
|
220
237
|
result: await runHandoffShow({ target, packageRoot, taskId: options.taskId, handoffId: options.handoffId }),
|
|
221
238
|
exitCode: 0,
|
|
222
239
|
}),
|
|
240
|
+
"handoff-accept": async ({ target, packageRoot, options }) => ({
|
|
241
|
+
result: await runHandoffAccept({
|
|
242
|
+
target,
|
|
243
|
+
packageRoot,
|
|
244
|
+
taskId: options.taskId,
|
|
245
|
+
handoffId: options.handoffId,
|
|
246
|
+
consumerId: options.consumerId,
|
|
247
|
+
harness: options.harness,
|
|
248
|
+
}),
|
|
249
|
+
exitCode: 0,
|
|
250
|
+
}),
|
|
223
251
|
"responsibility-set": async ({ target, packageRoot, options }) => ({
|
|
224
252
|
result: await runResponsibilitySet({
|
|
225
253
|
target,
|
|
@@ -60,6 +60,7 @@ export function defaultCommandInputValues() {
|
|
|
60
60
|
checkExecutionRef: null,
|
|
61
61
|
checkProvenance: null,
|
|
62
62
|
timeoutMs: null,
|
|
63
|
+
replace: false,
|
|
63
64
|
scopeRef: null,
|
|
64
65
|
commandArgv: [],
|
|
65
66
|
checkType: null,
|
|
@@ -69,6 +70,8 @@ export function defaultCommandInputValues() {
|
|
|
69
70
|
recipientHint: null,
|
|
70
71
|
handoffNote: null,
|
|
71
72
|
handoffId: null,
|
|
73
|
+
consumerId: null,
|
|
74
|
+
harness: null,
|
|
72
75
|
responsibilityLabel: null,
|
|
73
76
|
responsibilityAllowedPaths: [],
|
|
74
77
|
responsibilityReadOnlyPaths: [],
|
|
@@ -130,19 +133,38 @@ export function validateForgeLoopCommandInput({ command, input, help = false } =
|
|
|
130
133
|
if (command === "efficiency" && !help && !options.taskId) {
|
|
131
134
|
throw inputError("efficiency requires --task");
|
|
132
135
|
}
|
|
136
|
+
if (["quality-baseline", "quality-verify", "quality-status"].includes(command) && !help && !options.taskId) {
|
|
137
|
+
throw inputError(`${command} requires --task`);
|
|
138
|
+
}
|
|
139
|
+
if (["quality-baseline", "quality-verify"].includes(command) && !help
|
|
140
|
+
&& options.timeoutMs !== null && options.timeoutMs !== undefined
|
|
141
|
+
&& (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 0 || options.timeoutMs > 300000)) {
|
|
142
|
+
throw inputError(`${command} --timeout-ms must be between 0 and 300000`);
|
|
143
|
+
}
|
|
144
|
+
if (command !== "quality-baseline" && options.replace === true) {
|
|
145
|
+
throw inputError(`--replace is only valid for quality-baseline`);
|
|
146
|
+
}
|
|
133
147
|
if (command !== "usage-record" && options.usageSource !== undefined && options.usageSource !== "ACTOR_REPORTED") {
|
|
134
148
|
throw inputError(`usageSource is not valid for ${command}`);
|
|
135
149
|
}
|
|
136
150
|
if (options.compact === true && !["next", "task-show"].includes(command)) {
|
|
137
151
|
throw inputError(`compact output is not valid for ${command}`);
|
|
138
152
|
}
|
|
139
|
-
if (["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"].includes(command)
|
|
153
|
+
if (["workspace-bind", "workspace-status", "handoff-create", "handoff-list", "handoff-show", "handoff-accept", "responsibility-set", "responsibility-status", "verify-scope", "attestation-create", "attestation-status", "attestation-verify"].includes(command)
|
|
140
154
|
&& !options.taskId) {
|
|
141
155
|
throw inputError(`${command} requires --task`);
|
|
142
156
|
}
|
|
143
157
|
if (command === "handoff-show" && !help && !options.handoffId) {
|
|
144
158
|
throw inputError("handoff-show requires --id");
|
|
145
159
|
}
|
|
160
|
+
if (command === "handoff-accept" && !help) {
|
|
161
|
+
if (!options.handoffId) {
|
|
162
|
+
throw inputError("handoff-accept requires --handoff");
|
|
163
|
+
}
|
|
164
|
+
if (!options.consumerId) {
|
|
165
|
+
throw inputError("handoff-accept requires --consumer-id");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
146
168
|
if (command === "responsibility-set" && !help && !options.responsibilityLabel) {
|
|
147
169
|
throw inputError("responsibility-set requires --label");
|
|
148
170
|
}
|
|
@@ -25,6 +25,7 @@ import { taskArtifactPath, taskExecutionPath } from "./task-paths.js";
|
|
|
25
25
|
import { assertClaimsCoverChangedPaths } from "./task-scope.js";
|
|
26
26
|
import { discoverTasks } from "./task-discovery.js";
|
|
27
27
|
import { listActions } from "./actions.js";
|
|
28
|
+
import { STRUCTURAL_QUALITY_REQUIREMENT } from "./structural-quality/constants.js";
|
|
28
29
|
|
|
29
30
|
async function actionReceiptSummary(target, packageRoot, taskId) {
|
|
30
31
|
const actions = await listActions(target, { packageRoot, taskId });
|
|
@@ -239,6 +240,7 @@ export async function requiredEvidenceForTarget({
|
|
|
239
240
|
...(contract.value.successCriteria ?? []),
|
|
240
241
|
...guideEvidence,
|
|
241
242
|
...(config.requiredEvidence ?? []),
|
|
243
|
+
...(config.structuralQuality?.mode === "gate" ? [STRUCTURAL_QUALITY_REQUIREMENT] : []),
|
|
242
244
|
...additionalEvidence,
|
|
243
245
|
])].sort();
|
|
244
246
|
}
|
package/src/core/completion.js
CHANGED
|
@@ -18,6 +18,7 @@ import { readConfig } from "./config.js";
|
|
|
18
18
|
import { resolveResponsibilityStatus } from "./responsibility.js";
|
|
19
19
|
import { createCodeManifest, readCodeManifest, validateCodeManifestBindings, writeCodeManifest } from "./code-manifest.js";
|
|
20
20
|
import { getActiveTaskTransaction, withTaskTransaction } from "./transaction.js";
|
|
21
|
+
import { validateStructuralQualityCheckProvenance } from "./structural-quality/service.js";
|
|
21
22
|
|
|
22
23
|
async function attestationConfiguration(target, packageRoot, errors) {
|
|
23
24
|
try {
|
|
@@ -157,6 +158,26 @@ function repairNext(error) {
|
|
|
157
158
|
return "Do not execute installation-capable verification commands without explicit scoped installation authority; use local equivalents or record NOT_VERIFIED.";
|
|
158
159
|
case "E_VERIFICATION_TOOL_UNAVAILABLE":
|
|
159
160
|
return "Use an available local verifier, an existing equivalent, or record NOT_VERIFIED if installation was not authorized.";
|
|
161
|
+
case "E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID":
|
|
162
|
+
return "Repair structuralQuality configuration in .forgeloop/config.json and rerun preflight.";
|
|
163
|
+
case "E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE":
|
|
164
|
+
case "E_STRUCTURAL_QUALITY_PROVIDER_PROTOCOL_INVALID":
|
|
165
|
+
case "E_STRUCTURAL_QUALITY_SCAN_FAILED":
|
|
166
|
+
case "E_STRUCTURAL_QUALITY_TIMEOUT":
|
|
167
|
+
case "E_STRUCTURAL_QUALITY_OUTPUT_LIMIT":
|
|
168
|
+
return "Restore the trusted structural-quality provider and rerun quality-verify; do not promote an unavailable or malformed scan.";
|
|
169
|
+
case "E_STRUCTURAL_QUALITY_BASELINE_MISSING":
|
|
170
|
+
return "Run forgeloop quality-baseline --task <id> while the task is PLANNED, before execution begins.";
|
|
171
|
+
case "E_STRUCTURAL_QUALITY_BASELINE_EXISTS":
|
|
172
|
+
case "E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID":
|
|
173
|
+
return "Keep the original baseline and correct the task against it; baseline replacement is only allowed before EXECUTING.";
|
|
174
|
+
case "E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH":
|
|
175
|
+
case "E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE":
|
|
176
|
+
return "Reconcile provider, version, rules, policy, scope, contract, or route drift, then rerun quality-verify in the active cycle.";
|
|
177
|
+
case "E_STRUCTURAL_QUALITY_EVIDENCE_STALE":
|
|
178
|
+
return "Run quality-verify for the current verification cycle and refresh the receipt through the canonical completion pipeline.";
|
|
179
|
+
case "E_STRUCTURAL_QUALITY_REGRESSION":
|
|
180
|
+
return "Use the bottleneck and root-cause deltas to record an evidence-backed diagnosis, correct within scope, and verify a new cycle.";
|
|
160
181
|
case "E_NEW_POLICY_VIOLATION":
|
|
161
182
|
return "Resolve the new policy violation or record baseline if adopted debt before completion.";
|
|
162
183
|
case "E_POLICY_WEAKENING":
|
|
@@ -433,6 +454,27 @@ export async function evaluateCompletion({
|
|
|
433
454
|
coverage = receipt?.value?.evidenceCoverage ?? [];
|
|
434
455
|
}
|
|
435
456
|
|
|
457
|
+
if (contract && route && state) {
|
|
458
|
+
const qualityChecks = [...new Map([
|
|
459
|
+
...(state.checks ?? []),
|
|
460
|
+
...(receipt?.value?.checks ?? []),
|
|
461
|
+
].filter((check) => check?.kind === "structural-quality").map((check) => [check.id, check])).values()];
|
|
462
|
+
for (const check of qualityChecks) {
|
|
463
|
+
const provenanceErrors = await validateStructuralQualityCheckProvenance(check, {
|
|
464
|
+
target,
|
|
465
|
+
packageRoot,
|
|
466
|
+
taskId: contract.value.taskId,
|
|
467
|
+
state,
|
|
468
|
+
contract,
|
|
469
|
+
route,
|
|
470
|
+
runtimeContext,
|
|
471
|
+
});
|
|
472
|
+
for (const error of provenanceErrors) {
|
|
473
|
+
errors.push(issue(error.code ?? "E_STRUCTURAL_QUALITY_EVIDENCE_STALE", error.message, error.artifacts ?? [stateRel, receiptRel]));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
436
478
|
const ledger = contract && state
|
|
437
479
|
? await validateLedger(target, taskId, contract.value.taskId, state, errors, packageRoot, { eventsPath, statePath })
|
|
438
480
|
: { valid: false, events: [], errors: [] };
|