@cassiomc1/forgeloop 1.8.1 → 1.9.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 (64) hide show
  1. package/.cursor/rules/project-loop.mdc +6 -3
  2. package/.github/copilot-instructions.md +5 -0
  3. package/AGENTS.md +6 -0
  4. package/CLAUDE.md +6 -0
  5. package/DOCS_INDEX.md +5 -0
  6. package/ENG/accessibility-eng.md +12 -2
  7. package/ENG/design-code-eng.md +22 -1
  8. package/LOOP_ENGINEERING.md +28 -0
  9. package/PROTOCOL_INTEGRATION.md +26 -0
  10. package/QUALITY_SCORECARD.md +2 -0
  11. package/README.md +11 -0
  12. package/THREAT_MODEL.md +24 -0
  13. package/completions/_forgeloop +4 -1
  14. package/completions/forgeloop.bash +7 -1
  15. package/completions/forgeloop.fish +19 -1
  16. package/docs/AGENT_PROTOCOL_SUMMARY.md +6 -1
  17. package/docs/ARTIFACT_REFERENCE.md +128 -0
  18. package/docs/CLI_REFERENCE.md +84 -1
  19. package/docs/KNOWLEDGE_SOURCES.md +161 -0
  20. package/docs/MCP.md +1 -1
  21. package/docs/RECIPES.md +31 -0
  22. package/docs/STRUCTURAL_QUALITY.md +350 -0
  23. package/docs/TROUBLESHOOTING.md +107 -0
  24. package/package.json +3 -1
  25. package/schemas/config.schema.json +46 -0
  26. package/schemas/preflight.schema.json +2 -1
  27. package/schemas/structural-quality.schema.json +175 -0
  28. package/src/cli.js +18 -0
  29. package/src/commands/quality-baseline.js +28 -0
  30. package/src/commands/quality-status.js +34 -0
  31. package/src/commands/quality-verify.js +30 -0
  32. package/src/core/artifact-registry.js +12 -0
  33. package/src/core/audit.js +38 -0
  34. package/src/core/bundles.js +134 -1
  35. package/src/core/cli-command-definitions.js +45 -0
  36. package/src/core/command-executors.js +16 -0
  37. package/src/core/command-input.js +12 -0
  38. package/src/core/completion-artifacts.js +2 -0
  39. package/src/core/completion.js +42 -0
  40. package/src/core/config.js +3 -0
  41. package/src/core/error-codes.js +73 -0
  42. package/src/core/filesystem.js +18 -3
  43. package/src/core/inspect.js +64 -0
  44. package/src/core/integration-invocation-policy.js +15 -0
  45. package/src/core/integration-resources.js +17 -0
  46. package/src/core/next-action-model.js +11 -1
  47. package/src/core/next-action-phases.js +84 -5
  48. package/src/core/phase.js +9 -1
  49. package/src/core/preflight.js +33 -0
  50. package/src/core/protocol-info.js +15 -0
  51. package/src/core/runtime-context.js +27 -0
  52. package/src/core/schema-validation.js +1 -0
  53. package/src/core/structural-quality/artifacts.js +329 -0
  54. package/src/core/structural-quality/constants.js +67 -0
  55. package/src/core/structural-quality/policy.js +227 -0
  56. package/src/core/structural-quality/provider.js +287 -0
  57. package/src/core/structural-quality/sentrux-mcp.js +477 -0
  58. package/src/core/structural-quality/service.js +1138 -0
  59. package/src/core/structural-quality/source-fingerprint.js +112 -0
  60. package/src/core/structural-quality/status.js +3 -0
  61. package/src/core/task-paths.js +24 -0
  62. package/src/core/templates.js +1 -0
  63. package/src/integration.d.ts +25 -0
  64. package/src/integration.js +14 -0
@@ -0,0 +1,175 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "forgeloop://schemas/structural-quality.schema.json",
4
+ "title": "ForgeLoop structural quality evidence",
5
+ "type": "object",
6
+ "required": ["schemaVersion", "protocolVersion", "role", "taskId", "capturedAt", "verificationCycle", "attempt", "bindings", "provider", "scope", "status", "reasonCodes"],
7
+ "properties": {
8
+ "schemaVersion": { "const": 1 },
9
+ "protocolVersion": { "const": 1 },
10
+ "role": { "enum": ["BASELINE", "EVALUATION"] },
11
+ "taskId": { "type": "string", "minLength": 1 },
12
+ "capturedAt": { "type": "string", "minLength": 1 },
13
+ "verificationCycle": { "oneOf": [{ "type": "integer", "minimum": 1 }, { "type": "null" }] },
14
+ "attempt": { "type": "integer", "minimum": 1 },
15
+ "status": { "enum": ["PASS", "FAIL", "BLOCKED", "NOT_OBSERVED"] },
16
+ "reasonCodes": { "type": "array", "items": { "type": "string", "minLength": 1 } },
17
+ "errorCode": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
18
+ "baselineSignal": { "oneOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] },
19
+ "currentSignal": { "oneOf": [{ "type": "integer", "minimum": 0, "maximum": 10000 }, { "type": "null" }] },
20
+ "bindings": {
21
+ "type": "object",
22
+ "required": ["contractFingerprint", "routeFingerprint", "policyFingerprint", "scopeFingerprint"],
23
+ "properties": {
24
+ "contractFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
25
+ "routeFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
26
+ "policyFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
27
+ "scopeFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
28
+ "baselineFingerprint": { "oneOf": [{ "type": "string", "pattern": "^[a-f0-9]{64}$" }, { "type": "null" }] },
29
+ "sourceMaterialFingerprint": { "oneOf": [{ "type": "string", "pattern": "^[a-f0-9]{64}$" }, { "type": "null" }] },
30
+ "stateRevision": { "type": "integer", "minimum": 0 }
31
+ },
32
+ "additionalProperties": false
33
+ },
34
+ "sourceObservation": {
35
+ "type": "object",
36
+ "required": ["beforeFingerprint", "afterFingerprint", "stable"],
37
+ "properties": {
38
+ "beforeFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
39
+ "afterFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
40
+ "stable": { "type": "boolean" }
41
+ },
42
+ "additionalProperties": false
43
+ },
44
+ "provider": {
45
+ "type": "object",
46
+ "required": ["id", "version", "transport", "executionMode"],
47
+ "properties": {
48
+ "id": { "type": "string", "minLength": 1 },
49
+ "version": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
50
+ "transport": { "type": "string", "minLength": 1 },
51
+ "executionMode": { "type": "string", "minLength": 1 },
52
+ "measurementModel": { "type": "string", "minLength": 1 },
53
+ "compatibilityKey": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }
54
+ },
55
+ "additionalProperties": false
56
+ },
57
+ "detection": {
58
+ "type": "object",
59
+ "required": ["available", "providerId", "transport", "reasonCode"],
60
+ "properties": {
61
+ "available": { "type": "boolean" },
62
+ "providerId": { "type": "string", "minLength": 1 },
63
+ "providerVersion": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
64
+ "transport": { "type": "string", "minLength": 1 },
65
+ "measurementModel": { "type": "string", "minLength": 1 },
66
+ "compatibilityKey": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },
67
+ "reasonCode": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }
68
+ },
69
+ "additionalProperties": false
70
+ },
71
+ "scope": {
72
+ "type": "object",
73
+ "required": ["kind", "projectRoot"],
74
+ "properties": {
75
+ "kind": { "const": "PROJECT" },
76
+ "projectRoot": { "const": "." },
77
+ "providerConfigFingerprint": { "oneOf": [{ "type": "string", "pattern": "^[a-f0-9]{64}$" }, { "type": "null" }] },
78
+ "architectureRulesFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
79
+ },
80
+ "additionalProperties": false
81
+ },
82
+ "snapshot": { "$ref": "#/$defs/snapshot" },
83
+ "comparison": { "$ref": "#/$defs/comparison" }
84
+ },
85
+ "additionalProperties": false,
86
+ "$defs": {
87
+ "rootCause": {
88
+ "type": "object",
89
+ "required": ["score", "raw"],
90
+ "properties": {
91
+ "score": { "type": "integer", "minimum": 0, "maximum": 10000 },
92
+ "raw": { "type": "number" }
93
+ },
94
+ "additionalProperties": false
95
+ },
96
+ "snapshot": {
97
+ "type": "object",
98
+ "required": ["qualitySignal", "bottleneck", "rootCauses", "statistics", "diagnostics"],
99
+ "properties": {
100
+ "qualitySignal": { "type": "integer", "minimum": 0, "maximum": 10000 },
101
+ "bottleneck": { "enum": ["modularity", "acyclicity", "depth", "equality", "redundancy"] },
102
+ "rootCauses": {
103
+ "type": "object",
104
+ "required": ["modularity", "acyclicity", "depth", "equality", "redundancy"],
105
+ "properties": {
106
+ "modularity": { "$ref": "#/$defs/rootCause" },
107
+ "acyclicity": { "$ref": "#/$defs/rootCause" },
108
+ "depth": { "$ref": "#/$defs/rootCause" },
109
+ "equality": { "$ref": "#/$defs/rootCause" },
110
+ "redundancy": { "$ref": "#/$defs/rootCause" }
111
+ },
112
+ "additionalProperties": false
113
+ },
114
+ "statistics": {
115
+ "type": "object",
116
+ "required": ["files", "lines", "importEdges", "crossModuleEdges"],
117
+ "properties": {
118
+ "files": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }] },
119
+ "lines": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }] },
120
+ "importEdges": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }] },
121
+ "crossModuleEdges": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "null" }] }
122
+ },
123
+ "additionalProperties": false
124
+ },
125
+ "diagnostics": { "oneOf": [{ "type": "object" }, { "type": "null" }] }
126
+ },
127
+ "additionalProperties": false
128
+ },
129
+ "comparison": {
130
+ "type": "object",
131
+ "required": ["comparable", "qualityDelta", "rootCauseDeltas", "status", "reasonCodes"],
132
+ "properties": {
133
+ "comparable": { "type": "boolean" },
134
+ "qualityDelta": { "oneOf": [{ "type": "integer" }, { "type": "null" }] },
135
+ "rootCauseDeltas": { "type": "object" },
136
+ "failedConditions": { "type": "array", "items": { "type": "string", "minLength": 1 } },
137
+ "status": { "enum": ["PASS", "FAIL", "BLOCKED", "NOT_OBSERVED"] },
138
+ "reasonCodes": { "type": "array", "items": { "type": "string", "minLength": 1 } }
139
+ },
140
+ "additionalProperties": false
141
+ }
142
+ },
143
+ "allOf": [
144
+ {
145
+ "if": { "properties": { "role": { "const": "BASELINE" } } },
146
+ "then": {
147
+ "required": ["sourceObservation"],
148
+ "properties": {
149
+ "bindings": {
150
+ "required": ["sourceMaterialFingerprint"],
151
+ "properties": {
152
+ "sourceMaterialFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
153
+ }
154
+ },
155
+ "sourceObservation": { "properties": { "stable": { "const": true } } }
156
+ }
157
+ }
158
+ },
159
+ {
160
+ "if": { "properties": { "status": { "enum": ["PASS", "FAIL"] } } },
161
+ "then": {
162
+ "required": ["sourceObservation"],
163
+ "properties": {
164
+ "bindings": {
165
+ "required": ["sourceMaterialFingerprint"],
166
+ "properties": {
167
+ "sourceMaterialFingerprint": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
168
+ }
169
+ },
170
+ "sourceObservation": { "properties": { "stable": { "const": true } } }
171
+ }
172
+ }
173
+ }
174
+ ]
175
+ }
package/src/cli.js CHANGED
@@ -13,6 +13,9 @@ import { formatValidateProtocolResult } from "./commands/validate-protocol.js";
13
13
  import { formatActivateResult } from "./commands/activate.js";
14
14
  import { formatAdvanceResult } from "./commands/advance.js";
15
15
  import { formatPreflightResult } from "./commands/preflight.js";
16
+ import { formatQualityBaselineResult } from "./commands/quality-baseline.js";
17
+ import { formatQualityVerifyResult } from "./commands/quality-verify.js";
18
+ import { formatQualityStatusResult } from "./commands/quality-status.js";
16
19
  import { formatCompleteResult } from "./commands/complete.js";
17
20
  import { formatAuditResult } from "./commands/audit.js";
18
21
  import { formatReportResult } from "./commands/report.js";
@@ -379,6 +382,21 @@ export const COMMAND_HANDLERS = Object.freeze({
379
382
  renderJsonOr(options, result, formatPreflightResult);
380
383
  return exitCode;
381
384
  },
385
+ "quality-baseline": async ({ target, packageRoot, options }) => {
386
+ const { result, exitCode } = await COMMAND_EXECUTORS["quality-baseline"]({ target, packageRoot, options });
387
+ renderJsonOr(options, result, formatQualityBaselineResult);
388
+ return exitCode;
389
+ },
390
+ "quality-verify": async ({ target, packageRoot, options }) => {
391
+ const { result, exitCode } = await COMMAND_EXECUTORS["quality-verify"]({ target, packageRoot, options });
392
+ renderJsonOr(options, result, formatQualityVerifyResult);
393
+ return exitCode;
394
+ },
395
+ "quality-status": async ({ target, packageRoot, options }) => {
396
+ const { result, exitCode } = await COMMAND_EXECUTORS["quality-status"]({ target, packageRoot, options });
397
+ renderJsonOr(options, result, formatQualityStatusResult);
398
+ return exitCode;
399
+ },
382
400
  advance: async ({ target, packageRoot, options }) => {
383
401
  const { result } = await COMMAND_EXECUTORS.advance({ target, packageRoot, options });
384
402
  renderJsonOr(options, result, formatAdvanceResult);
@@ -0,0 +1,28 @@
1
+ import { withTaskMutation } from "../core/task-command.js";
2
+ import { captureStructuralQualityBaseline } from "../core/structural-quality/service.js";
3
+
4
+ export async function runQualityBaseline({ target, packageRoot, taskId, replace = false, timeoutMs, runtimeContext } = {}) {
5
+ return withTaskMutation(target, { taskId, packageRoot }, "quality-baseline", async (ctx) => captureStructuralQualityBaseline({
6
+ target,
7
+ packageRoot,
8
+ taskId: ctx?.taskId ?? taskId,
9
+ replace,
10
+ timeoutMs,
11
+ runtimeContext,
12
+ }));
13
+ }
14
+
15
+ export function formatQualityBaselineResult(result) {
16
+ return [
17
+ "FORGELOOP STRUCTURAL QUALITY BASELINE",
18
+ `status: ${result.status}`,
19
+ `mode: ${result.mode}`,
20
+ `provider: ${result.provider?.id ?? "not-requested"}`,
21
+ `provider-version: ${result.provider?.version ?? "unknown"}`,
22
+ `quality: ${result.baseline?.snapshot?.qualitySignal ?? "none"}`,
23
+ `bottleneck: ${result.baseline?.snapshot?.bottleneck ?? "none"}`,
24
+ `artifact: ${result.artifactRef ?? "none"}`,
25
+ `next: ${result.mode === "gate" ? "VERIFY_STRUCTURAL_QUALITY" : "none"}`,
26
+ "",
27
+ ].join("\n");
28
+ }
@@ -0,0 +1,34 @@
1
+ import { withResolvedTask } from "../core/task-command.js";
2
+ import { projectStructuralQualityStatus } from "../core/structural-quality/status.js";
3
+
4
+ export async function runQualityStatus({ target, packageRoot, taskId, runtimeContext } = {}) {
5
+ return withResolvedTask(target, { taskId, packageRoot }, async (ctx) => projectStructuralQualityStatus({
6
+ target,
7
+ packageRoot,
8
+ taskId: ctx?.taskId ?? taskId,
9
+ runtimeContext,
10
+ }), { explicitRequired: true });
11
+ }
12
+
13
+ export function formatQualityStatusResult(result) {
14
+ const status = result.mode === "off"
15
+ ? "OFF"
16
+ : result.current.status === "PASS"
17
+ ? "PASS"
18
+ : result.current.status === "FAIL"
19
+ ? "FAIL"
20
+ : result.current.status === "BLOCKED" || (result.mode === "gate" && result.baseline.status !== "OBSERVED")
21
+ ? "BLOCKED"
22
+ : "NOT OBSERVED";
23
+ return [
24
+ `STRUCTURAL QUALITY: ${status}`,
25
+ `mode: ${result.mode}`,
26
+ `provider: ${result.provider ?? "none"}`,
27
+ `baseline: ${result.baseline.status}`,
28
+ `current: ${result.current.status}`,
29
+ `quality: ${result.current.qualitySignal ?? result.baseline.qualitySignal ?? "none"}`,
30
+ `delta: ${result.current.delta ?? "none"}`,
31
+ `next: ${result.next ?? "none"}`,
32
+ "",
33
+ ].join("\n");
34
+ }
@@ -0,0 +1,30 @@
1
+ import { evaluateStructuralQuality } from "../core/structural-quality/service.js";
2
+
3
+ export async function runQualityVerify({ target, packageRoot, taskId, timeoutMs, authorityContext, runtimeContext } = {}) {
4
+ return evaluateStructuralQuality({
5
+ target,
6
+ packageRoot,
7
+ taskId,
8
+ timeoutMs,
9
+ authorityContext,
10
+ runtimeContext,
11
+ });
12
+ }
13
+
14
+ export function formatQualityVerifyResult(result) {
15
+ return [
16
+ "FORGELOOP STRUCTURAL QUALITY VERIFICATION",
17
+ `status: ${result.evaluation?.status ?? result.status}`,
18
+ `cycle: ${result.evaluation?.verificationCycle ?? "none"}`,
19
+ `attempt: ${result.evaluation?.attempt ?? "none"}`,
20
+ `provider: ${result.evaluation?.provider?.id ?? "none"}`,
21
+ `provider-version: ${result.evaluation?.provider?.version ?? "unknown"}`,
22
+ `quality: ${result.evaluation?.currentSignal ?? result.evaluation?.snapshot?.qualitySignal ?? "none"}`,
23
+ `delta: ${result.evaluation?.comparison?.qualityDelta ?? "none"}`,
24
+ `bottleneck: ${result.evaluation?.snapshot?.bottleneck ?? "none"}`,
25
+ `artifact: ${result.evaluation?.artifactRef ?? "none"}`,
26
+ `check: ${result.check?.status ?? "not-recorded"}`,
27
+ `next: ${result.evaluation?.status === "FAIL" ? "DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION" : result.evaluation?.status === "BLOCKED" ? "RESOLVE_STRUCTURAL_QUALITY_BLOCKER" : "none"}`,
28
+ "",
29
+ ].join("\n");
30
+ }
@@ -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
  }
@@ -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",
@@ -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";
@@ -132,6 +135,19 @@ export const COMMAND_EXECUTORS = {
132
135
  const result = await runPreflight({ target, packageRoot, strict: options.strict, taskId: options.taskId });
133
136
  return { result, exitCode: result.status === "READY" ? 0 : 1 };
134
137
  },
138
+ "quality-baseline": async ({ target, packageRoot, options, runtimeContext }) => {
139
+ const result = await runQualityBaseline({ target, packageRoot, taskId: options.taskId, replace: options.replace, timeoutMs: options.timeoutMs, runtimeContext });
140
+ return { result, exitCode: ["CAPTURED", "EXISTING", "REPLACED", "NOT_REQUESTED"].includes(result.status) ? 0 : 1 };
141
+ },
142
+ "quality-verify": async ({ target, packageRoot, options, authorityContext, runtimeContext }) => {
143
+ const result = await runQualityVerify({ target, packageRoot, taskId: options.taskId, timeoutMs: options.timeoutMs, authorityContext, runtimeContext });
144
+ const status = result.evaluation?.status ?? result.status;
145
+ return { result, exitCode: ["PASS", "NOT_OBSERVED", "CONVERGED", "NOT_REQUESTED"].includes(status) ? 0 : 1 };
146
+ },
147
+ "quality-status": async ({ target, packageRoot, options }) => ({
148
+ result: await runQualityStatus({ target, packageRoot, taskId: options.taskId }),
149
+ exitCode: 0,
150
+ }),
135
151
  advance: async ({ target, packageRoot, options, authorityContext, runtimeContext }) => ({
136
152
  result: await runAdvance({
137
153
  target,