@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
package/src/core/phase.js CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  buildInformationGainProjection,
27
27
  evaluateStructuredDiagnosticStall,
28
28
  } from "./information-gain-projection.js";
29
+ import { assertStructuralQualityExecutionReady } from "./structural-quality/service.js";
29
30
 
30
31
  function phaseError(code, message, artifacts = []) {
31
32
  const error = new Error(message);
@@ -134,9 +135,16 @@ async function assertPhasePrerequisites(target, state, toPhase, packageRoot, aut
134
135
  throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${routeRel}: ${error.message}`, [routeRel]);
135
136
  }
136
137
  }
138
+ if (toPhase === "EXECUTING" && scopedTaskId) {
139
+ try {
140
+ await assertStructuralQualityExecutionReady({ target, packageRoot, taskId: scopedTaskId, runtimeContext });
141
+ } catch (error) {
142
+ throw phaseError(error.code, error.message, error.artifacts);
143
+ }
144
+ }
137
145
  if (hasExecutionStarted(toPhase)) {
138
146
  try {
139
- await assertExecutionPrerequisites({ target, state, packageRoot, ...options, taskId: scopedTaskId });
147
+ await assertExecutionPrerequisites({ target, state, packageRoot, ...options, taskId: scopedTaskId, runtimeContext });
140
148
  } catch (error) {
141
149
  throw phaseError(error.code, error.message, error.artifacts);
142
150
  }
@@ -28,6 +28,7 @@ import { PROFILE_PATH } from "./target-layout.js";
28
28
  import { readPersistedRoute } from "./route-artifact.js";
29
29
  import { validateEventLedger } from "./events.js";
30
30
  import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
31
+ import { normalizeStructuralQualityConfig } from "./structural-quality/policy.js";
31
32
 
32
33
  const PREVIEW_DECISION_LIMIT = 10;
33
34
  const PREVIEW_DECISION_MAX_LENGTH = 240;
@@ -42,6 +43,17 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
42
43
  const contract = await loadContract(target, packageRoot, errors, { taskId, contractPath });
43
44
  const route = await loadRoute(target, packageRoot, errors, { taskId, routePath });
44
45
  const config = await optionalConfig(target, packageRoot, errors);
46
+ if (config.structuralQuality !== undefined) {
47
+ try {
48
+ normalizeStructuralQualityConfig(config.structuralQuality);
49
+ } catch (error) {
50
+ errors.push(issue(
51
+ error.code ?? "E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID",
52
+ error.message,
53
+ [PROJECT_ARTIFACT_PATHS.config],
54
+ ));
55
+ }
56
+ }
45
57
  const effectiveStrict = strict || config.complianceMode === "strict";
46
58
  if (effectiveStrict && profile.status !== "verified") {
47
59
  errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", [PROFILE_PATH]));
@@ -111,6 +123,26 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
111
123
 
112
124
  const sortedErrors = sortIssues(errors);
113
125
  const effectiveTaskId = taskId ?? contract?.value?.taskId ?? state?.taskId ?? "unknown";
126
+ let structuralQuality = {
127
+ mode: config.structuralQuality?.mode ?? "off",
128
+ provider: config.structuralQuality?.mode && config.structuralQuality.mode !== "off"
129
+ ? config.structuralQuality.provider ?? "sentrux"
130
+ : null,
131
+ baseline: { status: config.structuralQuality?.mode && config.structuralQuality.mode !== "off" ? "MISSING" : "NOT_REQUESTED" },
132
+ current: { status: "NOT_OBSERVED" },
133
+ comparable: null,
134
+ completionRequired: config.structuralQuality?.mode === "gate",
135
+ reasonCodes: [],
136
+ next: config.structuralQuality?.mode === "gate" ? "CAPTURE_STRUCTURAL_QUALITY_BASELINE" : null,
137
+ };
138
+ if (effectiveTaskId !== "unknown") {
139
+ try {
140
+ const { projectStructuralQualityStatus } = await import("./structural-quality/service.js");
141
+ structuralQuality = await projectStructuralQualityStatus({ target, packageRoot, taskId: effectiveTaskId });
142
+ } catch (error) {
143
+ structuralQuality.reasonCodes = [error.code ?? "E_STRUCTURAL_QUALITY_EVIDENCE_STALE"];
144
+ }
145
+ }
114
146
  return {
115
147
  schemaVersion: 1,
116
148
  protocolVersion: 1,
@@ -140,6 +172,7 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
140
172
  },
141
173
  } : {}),
142
174
  ...(sources ? { sources: { status: "valid", fingerprint: null } } : {}),
175
+ structuralQuality,
143
176
  };
144
177
  }
145
178
 
@@ -83,6 +83,21 @@ export function protocolInfo({ packageVersion = null } = {}) {
83
83
  informationGainV2: true,
84
84
  strategyOscillationDetection: true,
85
85
  },
86
+ structuralQuality: {
87
+ version: 1,
88
+ supported: true,
89
+ schemaVersion: 1,
90
+ modes: ["off", "observe", "gate"],
91
+ builtInProviders: ["sentrux"],
92
+ commands: ["quality-baseline", "quality-verify", "quality-status"],
93
+ resource: "task/structural-quality",
94
+ providerNeutral: true,
95
+ defaultProvider: "sentrux",
96
+ transport: "mcp-stdio",
97
+ rootCauses: ["modularity", "acyclicity", "depth", "equality", "redundancy"],
98
+ baselineImmutableAfterExecution: true,
99
+ optimizationMaxExtraEvaluations: 2,
100
+ },
86
101
  durableActions: {
87
102
  version: 1,
88
103
  supported: true,
@@ -3,6 +3,7 @@ import {
3
3
  isVerificationExecutionAdapter,
4
4
  normalizeVerificationExecutionPolicy,
5
5
  } from "./verification-execution.js";
6
+ import { STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN } from "./structural-quality/constants.js";
6
7
 
7
8
  export const AUTHORITY_TRUST_MODES = Object.freeze(["NONE", "HOST_ATTESTED"]);
8
9
 
@@ -105,5 +106,31 @@ export function createForgeLoopContext(options = {}) {
105
106
  }
106
107
  context.usageProvider = options.usageProvider;
107
108
  }
109
+ if (options?.structuralQualityProviders !== undefined) {
110
+ const configured = options.structuralQualityProviders instanceof Map
111
+ ? Object.fromEntries(options.structuralQualityProviders.entries())
112
+ : options.structuralQualityProviders;
113
+ if (!configured || typeof configured !== "object" || Array.isArray(configured)) {
114
+ const error = new Error("structuralQualityProviders must be an object or Map");
115
+ error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
116
+ throw error;
117
+ }
118
+ const providers = {};
119
+ for (const [id, provider] of Object.entries(configured)) {
120
+ if (!STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(id) || id === "sentrux") {
121
+ const error = new Error(`Invalid or reserved structural-quality provider ID: ${id}`);
122
+ error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
123
+ throw error;
124
+ }
125
+ if (typeof provider !== "function"
126
+ && (!provider || typeof provider !== "object" || Array.isArray(provider))) {
127
+ const error = new Error(`Structural-quality provider ${id} must be an object or factory`);
128
+ error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
129
+ throw error;
130
+ }
131
+ providers[id] = provider;
132
+ }
133
+ context.structuralQualityProviders = Object.freeze(providers);
134
+ }
108
135
  return Object.freeze(context);
109
136
  }
@@ -54,6 +54,7 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
54
54
  "execution-profile-benchmark-scenario",
55
55
  "execution-profile-benchmark-run",
56
56
  "execution-profile-benchmark-aggregate",
57
+ "structural-quality",
57
58
  ]);
58
59
 
59
60
  export class SchemaValidationError extends Error {
@@ -0,0 +1,329 @@
1
+ import { readdir } from "node:fs/promises";
2
+
3
+ import {
4
+ canonicalFingerprint,
5
+ readJsonArtifact,
6
+ writeJsonArtifact,
7
+ } from "../artifacts.js";
8
+ import { fileExists, ensureWithin, assertSafePath } from "../filesystem.js";
9
+ import {
10
+ E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH,
11
+ E_STRUCTURAL_QUALITY_BASELINE_EXISTS,
12
+ E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID,
13
+ E_STRUCTURAL_QUALITY_EVIDENCE_STALE,
14
+ } from "../error-codes.js";
15
+ import { getPackageRoot } from "../templates.js";
16
+ import {
17
+ taskStructuralQualityBaselinePath,
18
+ taskStructuralQualityDirectory,
19
+ taskStructuralQualityEvaluationPath,
20
+ taskStructuralQualityEvaluationsDirectory,
21
+ } from "../task-paths.js";
22
+ import { STRUCTURAL_QUALITY_ROOT_CAUSES, structuralQualityError } from "./constants.js";
23
+ import { normalizeStructuralQualityDetection, normalizeStructuralQualitySnapshot } from "./provider.js";
24
+
25
+ const PRE_EXECUTION_PHASES = new Set([
26
+ "RECEIVED",
27
+ "DISCOVERING",
28
+ "CONTRACT_READY",
29
+ "ROUTED",
30
+ "DESIGNING",
31
+ "PLANNED",
32
+ ]);
33
+
34
+ function qualityArtifactError(code, message, artifacts = []) {
35
+ return structuralQualityError(code, message, artifacts);
36
+ }
37
+
38
+ function normalizeCycle(value, label) {
39
+ if (!Number.isInteger(value) || value < 1) {
40
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} must be a positive integer`);
41
+ }
42
+ return value;
43
+ }
44
+
45
+ function normalizeAttempt(value) {
46
+ if (!Number.isInteger(value) || value < 1) {
47
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "structural-quality attempt must be a positive integer");
48
+ }
49
+ return value;
50
+ }
51
+
52
+ function readFailure(error, relativePath, missingCode) {
53
+ if (error?.code === "ARTIFACT_MISSING") {
54
+ return qualityArtifactError(missingCode, `Structural-quality artifact is missing: ${relativePath}`, [relativePath]);
55
+ }
56
+ if (error?.code?.startsWith("E_STRUCTURAL_QUALITY")) return error;
57
+ return qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `Structural-quality artifact is invalid: ${error.message}`, [relativePath]);
58
+ }
59
+
60
+ export function validateStructuralQualityArtifact(value, label = "structural-quality artifact") {
61
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
62
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} must be an object`);
63
+ }
64
+ if (value.role === "BASELINE") {
65
+ if (value.verificationCycle !== null) throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} baseline verificationCycle must be null`);
66
+ if (value.status !== "PASS") throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} baseline status must be PASS`);
67
+ if (value.bindings?.baselineFingerprint !== undefined && value.bindings.baselineFingerprint !== null) {
68
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, `${label} baselineFingerprint must be null`);
69
+ }
70
+ } else if (value.role === "EVALUATION") {
71
+ if (!Number.isInteger(value.verificationCycle) || value.verificationCycle < 1) {
72
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} evaluation verificationCycle must be positive`);
73
+ }
74
+ if (!Number.isInteger(value.attempt) || value.attempt < 1) {
75
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} evaluation attempt must be positive`);
76
+ }
77
+ if (!Array.isArray(value.reasonCodes) || (value.status !== "PASS" && value.reasonCodes.length === 0)) {
78
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} non-PASS evaluations require a reason code`);
79
+ }
80
+ if (!value.comparison || typeof value.comparison !== "object") {
81
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} evaluation comparison is required`);
82
+ }
83
+ const comparisonStatus = value.comparison.status;
84
+ if (value.status === "PASS" && comparisonStatus !== "PASS") {
85
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} PASS status requires a PASS comparison`);
86
+ }
87
+ if (value.status === "FAIL" && comparisonStatus !== "FAIL") {
88
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} FAIL status requires a FAIL comparison`);
89
+ }
90
+ if (["BLOCKED", "NOT_OBSERVED"].includes(value.status) && comparisonStatus === "PASS") {
91
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} unavailable evaluation cannot contain a PASS comparison`);
92
+ }
93
+ } else {
94
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} role must be BASELINE or EVALUATION`);
95
+ }
96
+ const observed = value.role === "BASELINE" || ["PASS", "FAIL"].includes(value.status);
97
+ const sourceFingerprint = value.bindings?.sourceMaterialFingerprint;
98
+ if (observed) {
99
+ if (typeof sourceFingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(sourceFingerprint)) {
100
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} observed evidence requires sourceMaterialFingerprint`);
101
+ }
102
+ if (!value.sourceObservation || value.sourceObservation.stable !== true) {
103
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} observed evidence requires a stable sourceObservation`);
104
+ }
105
+ }
106
+ if (!value.detection || typeof value.detection !== "object" || Array.isArray(value.detection)) {
107
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} detection metadata is required`);
108
+ }
109
+ if (value.provider?.id !== value.detection?.providerId) {
110
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, `${label} provider and detection IDs differ`);
111
+ }
112
+ if (value.sourceObservation !== undefined && value.sourceObservation !== null) {
113
+ if (typeof value.sourceObservation !== "object" || Array.isArray(value.sourceObservation)) {
114
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} sourceObservation must be an object`);
115
+ }
116
+ if (typeof value.sourceObservation.beforeFingerprint !== "string"
117
+ || typeof value.sourceObservation.afterFingerprint !== "string"
118
+ || typeof value.sourceObservation.stable !== "boolean") {
119
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} sourceObservation requires beforeFingerprint, afterFingerprint, and stable`);
120
+ }
121
+ if (value.sourceObservation.stable === true
122
+ && (value.sourceObservation.beforeFingerprint !== value.sourceObservation.afterFingerprint
123
+ || (typeof sourceFingerprint === "string" && value.sourceObservation.beforeFingerprint !== sourceFingerprint))) {
124
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} stable sourceObservation does not match its source binding`);
125
+ }
126
+ if (!value.sourceObservation.stable && ["PASS", "FAIL"].includes(value.status)) {
127
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} unstable sourceObservation cannot produce observed evidence`);
128
+ }
129
+ }
130
+ normalizeStructuralQualityDetection(value.detection, {
131
+ providerId: value.provider?.id,
132
+ version: value.provider?.version,
133
+ transport: value.provider?.transport,
134
+ measurementModel: value.provider?.measurementModel,
135
+ compatibilityKey: value.provider?.compatibilityKey,
136
+ });
137
+ if (value.snapshot !== undefined) normalizeStructuralQualitySnapshot(value.snapshot);
138
+ if (["PASS", "FAIL"].includes(value.status) && value.snapshot === undefined) {
139
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `${label} observed status requires a normalized snapshot`);
140
+ }
141
+ return value;
142
+ }
143
+
144
+ export function structuralQualityArtifactRef(taskId, relativePath) {
145
+ const expectedRoot = taskStructuralQualityDirectory(taskId).replaceAll("\\", "/");
146
+ const portable = String(relativePath).replaceAll("\\", "/");
147
+ if (portable.startsWith("/")
148
+ || portable.split("/").some((part) => part === "..")
149
+ || (portable !== expectedRoot && !portable.startsWith(`${expectedRoot}/`))) {
150
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `Artifact reference escapes the structural-quality directory: ${relativePath}`, [portable]);
151
+ }
152
+ return portable;
153
+ }
154
+
155
+ export async function readStructuralQualityBaseline(target, taskId, packageRoot = getPackageRoot()) {
156
+ const relativePath = taskStructuralQualityBaselinePath(taskId);
157
+ try {
158
+ const artifact = await readJsonArtifact(target, relativePath, "structural-quality", packageRoot);
159
+ validateStructuralQualityArtifact(artifact.value, relativePath);
160
+ return artifact;
161
+ } catch (error) {
162
+ if (error?.code === "ARTIFACT_MISSING") return null;
163
+ throw readFailure(error, relativePath, E_STRUCTURAL_QUALITY_EVIDENCE_STALE);
164
+ }
165
+ }
166
+
167
+ export async function writeStructuralQualityBaseline(
168
+ target,
169
+ taskId,
170
+ value,
171
+ packageRoot = getPackageRoot(),
172
+ { phase = "PLANNED", replace = false, taskId: transactionTaskId = taskId } = {},
173
+ ) {
174
+ const relativePath = taskStructuralQualityBaselinePath(taskId);
175
+ if (!PRE_EXECUTION_PHASES.has(phase)) {
176
+ throw qualityArtifactError(
177
+ E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID,
178
+ `Structural-quality baselines cannot be written or replaced in ${phase}`,
179
+ [relativePath],
180
+ );
181
+ }
182
+ const existing = await readStructuralQualityBaseline(target, taskId, packageRoot);
183
+ const fingerprint = canonicalFingerprint(value);
184
+ if (existing) {
185
+ if (existing.fingerprint === fingerprint) {
186
+ return { ...existing, existing: true, identical: true };
187
+ }
188
+ if (!replace) {
189
+ throw qualityArtifactError(
190
+ E_STRUCTURAL_QUALITY_BASELINE_EXISTS,
191
+ "The structural-quality baseline already exists and is immutable without --replace before EXECUTING",
192
+ [relativePath],
193
+ );
194
+ }
195
+ }
196
+ const written = await writeJsonArtifact(
197
+ target,
198
+ relativePath,
199
+ value,
200
+ "structural-quality",
201
+ packageRoot,
202
+ { taskId: transactionTaskId, operation: "structural-quality-baseline" },
203
+ );
204
+ return { ...written, existing: Boolean(existing), identical: false };
205
+ }
206
+
207
+ export async function listStructuralQualityEvaluations(target, taskId, packageRoot = getPackageRoot()) {
208
+ const relativeDirectory = taskStructuralQualityEvaluationsDirectory(taskId);
209
+ await assertSafePath(target, relativeDirectory);
210
+ const absoluteDirectory = ensureWithin(target, relativeDirectory);
211
+ if (!(await fileExists(absoluteDirectory))) return [];
212
+ let entries;
213
+ try {
214
+ entries = await readdir(absoluteDirectory, { withFileTypes: true });
215
+ } catch (error) {
216
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `Unable to list structural-quality evaluations: ${error.message}`, [relativeDirectory]);
217
+ }
218
+ const candidates = entries
219
+ .filter((entry) => entry.isFile())
220
+ .map((entry) => {
221
+ const match = /^cycle-(\d+)-attempt-(\d+)\.json$/u.exec(entry.name);
222
+ return match ? { name: entry.name, verificationCycle: Number(match[1]), attempt: Number(match[2]) } : null;
223
+ })
224
+ .filter(Boolean)
225
+ .filter((item) => item.verificationCycle >= 1 && item.attempt >= 1)
226
+ .sort((left, right) => left.verificationCycle - right.verificationCycle || left.attempt - right.attempt);
227
+ const result = [];
228
+ for (const item of candidates) {
229
+ const relativePath = taskStructuralQualityEvaluationPath(taskId, item.verificationCycle, item.attempt);
230
+ try {
231
+ const artifact = await readJsonArtifact(target, relativePath, "structural-quality", packageRoot);
232
+ validateStructuralQualityArtifact(artifact.value, relativePath);
233
+ if (artifact.value.role !== "EVALUATION"
234
+ || artifact.value.verificationCycle !== item.verificationCycle
235
+ || artifact.value.attempt !== item.attempt) {
236
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `Evaluation filename does not match its typed cycle/attempt: ${relativePath}`, [relativePath]);
237
+ }
238
+ result.push(artifact);
239
+ } catch (error) {
240
+ throw readFailure(error, relativePath, E_STRUCTURAL_QUALITY_EVIDENCE_STALE);
241
+ }
242
+ }
243
+ return result;
244
+ }
245
+
246
+ export async function readStructuralQualityEvaluation(target, taskId, verificationCycle, attempt, packageRoot = getPackageRoot()) {
247
+ const relativePath = taskStructuralQualityEvaluationPath(taskId, normalizeCycle(verificationCycle, "verificationCycle"), normalizeAttempt(attempt));
248
+ try {
249
+ const artifact = await readJsonArtifact(target, relativePath, "structural-quality", packageRoot);
250
+ validateStructuralQualityArtifact(artifact.value, relativePath);
251
+ if (artifact.value.role !== "EVALUATION"
252
+ || artifact.value.verificationCycle !== verificationCycle
253
+ || artifact.value.attempt !== attempt) {
254
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `Evaluation identity does not match ${relativePath}`, [relativePath]);
255
+ }
256
+ return artifact;
257
+ } catch (error) {
258
+ throw readFailure(error, relativePath, E_STRUCTURAL_QUALITY_EVIDENCE_STALE);
259
+ }
260
+ }
261
+
262
+ export async function writeStructuralQualityEvaluation(target, taskId, verificationCycle, attempt, value, packageRoot = getPackageRoot(), { transactionTaskId = taskId } = {}) {
263
+ const cycle = normalizeCycle(verificationCycle, "verificationCycle");
264
+ const normalizedAttempt = normalizeAttempt(attempt);
265
+ const relativePath = taskStructuralQualityEvaluationPath(taskId, cycle, normalizedAttempt);
266
+ if (value.role !== "EVALUATION" || value.verificationCycle !== cycle || value.attempt !== normalizedAttempt) {
267
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `Evaluation identity does not match ${relativePath}`, [relativePath]);
268
+ }
269
+ validateStructuralQualityArtifact(value, relativePath);
270
+ const existing = await fileExists(ensureWithin(target, relativePath));
271
+ if (existing) {
272
+ const current = await readStructuralQualityEvaluation(target, taskId, cycle, normalizedAttempt, packageRoot);
273
+ if (current.fingerprint === canonicalFingerprint(value)) return { ...current, existing: true, identical: true };
274
+ throw qualityArtifactError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `Evaluation attempt is immutable: ${relativePath}`, [relativePath]);
275
+ }
276
+ const written = await writeJsonArtifact(
277
+ target,
278
+ relativePath,
279
+ value,
280
+ "structural-quality",
281
+ packageRoot,
282
+ { taskId: transactionTaskId, operation: "structural-quality-evaluation" },
283
+ );
284
+ return { ...written, existing: false, identical: false };
285
+ }
286
+
287
+ export function validateStructuralQualityBindings(value, expected = {}) {
288
+ const errors = [];
289
+ const bindings = value?.bindings ?? {};
290
+ const fields = ["contractFingerprint", "routeFingerprint", "policyFingerprint", "scopeFingerprint"];
291
+ for (const field of fields) {
292
+ if (expected[field] !== undefined && bindings[field] !== expected[field]) {
293
+ errors.push({
294
+ code: E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH,
295
+ message: `Structural-quality ${field} does not match the active task binding`,
296
+ });
297
+ }
298
+ }
299
+ if (expected.providerId !== undefined && value.provider?.id !== expected.providerId) {
300
+ errors.push({ code: E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, message: "Structural-quality provider ID does not match the active policy" });
301
+ }
302
+ if (expected.providerVersion !== undefined && expected.providerVersion !== null && value.provider?.version !== expected.providerVersion) {
303
+ errors.push({ code: E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, message: "Structural-quality provider version does not match the baseline" });
304
+ }
305
+ if (expected.baselineFingerprint !== undefined && bindings.baselineFingerprint !== expected.baselineFingerprint) {
306
+ errors.push({ code: E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, message: "Structural-quality evaluation does not bind the active baseline" });
307
+ }
308
+ if (expected.verificationCycle !== undefined && value.verificationCycle !== expected.verificationCycle) {
309
+ errors.push({ code: E_STRUCTURAL_QUALITY_EVIDENCE_STALE, message: "Structural-quality evidence belongs to a different verification cycle" });
310
+ }
311
+ return errors;
312
+ }
313
+
314
+ export function assertStructuralQualityBindings(value, expected = {}) {
315
+ const errors = validateStructuralQualityBindings(value, expected);
316
+ if (errors.length > 0) {
317
+ const artifactRef = expected.artifactRef ?? null;
318
+ throw qualityArtifactError(
319
+ errors[0].code,
320
+ errors[0].message,
321
+ artifactRef ? [structuralQualityArtifactRef(expected.taskId ?? value.taskId, artifactRef)] : [],
322
+ );
323
+ }
324
+ return value;
325
+ }
326
+
327
+ export function structuralQualityRootCauseNames() {
328
+ return [...STRUCTURAL_QUALITY_ROOT_CAUSES];
329
+ }
@@ -0,0 +1,67 @@
1
+ import { E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID } from "../error-codes.js";
2
+
3
+ export const STRUCTURAL_QUALITY_ROOT_CAUSES = Object.freeze([
4
+ "modularity",
5
+ "acyclicity",
6
+ "depth",
7
+ "equality",
8
+ "redundancy",
9
+ ]);
10
+
11
+ export const STRUCTURAL_QUALITY_MODES = Object.freeze(["off", "observe", "gate"]);
12
+ export const STRUCTURAL_QUALITY_STATUSES = Object.freeze(["PASS", "FAIL", "BLOCKED", "NOT_OBSERVED"]);
13
+ export const STRUCTURAL_QUALITY_CHECK_ID = "structural-quality";
14
+ export const STRUCTURAL_QUALITY_REQUIREMENT = "Structural quality must not regress beyond the configured budget";
15
+ export const STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/u;
16
+ export const STRUCTURAL_QUALITY_SENTRUX_MIN_VERSION = "0.5.5";
17
+ // Kept as an alias for early callers that used the misspelled constant before
18
+ // the public provider name was finalized.
19
+ export const STRUCTURAL_QUALITY_SENTRYX_MIN_VERSION = STRUCTURAL_QUALITY_SENTRUX_MIN_VERSION;
20
+ export const STRUCTURAL_QUALITY_DEFAULT_TIMEOUT_MS = 120_000;
21
+ export const STRUCTURAL_QUALITY_MAX_TIMEOUT_MS = 300_000;
22
+ export const STRUCTURAL_QUALITY_MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
23
+ export const STRUCTURAL_QUALITY_MAX_DIAGNOSTICS = 50;
24
+ export const STRUCTURAL_QUALITY_MAX_DIAGNOSTIC_STRING = 4096;
25
+ export const STRUCTURAL_QUALITY_MAX_EXTRA_EVALUATIONS = 2;
26
+
27
+ export const STRUCTURAL_QUALITY_MEASUREMENT_MODEL = "structural-root-causes-v1";
28
+ export const STRUCTURAL_QUALITY_SENTRUX_COMPATIBILITY_KEY = "sentrux-structural-root-causes-v1";
29
+ export const STRUCTURAL_QUALITY_SENTRUX_VERIFIED_VERSIONS = Object.freeze(["0.5.5", "0.5.6", "0.5.7"]);
30
+
31
+ export const STRUCTURAL_QUALITY_DEFAULT_DIMENSION_BUDGETS = Object.freeze(
32
+ Object.fromEntries(STRUCTURAL_QUALITY_ROOT_CAUSES.map((cause) => [cause, null])),
33
+ );
34
+
35
+ export const STRUCTURAL_QUALITY_DEFAULT_OPTIMIZATION = Object.freeze({
36
+ mode: "off",
37
+ maxExtraEvaluations: STRUCTURAL_QUALITY_MAX_EXTRA_EVALUATIONS,
38
+ minGainPoints: 25,
39
+ });
40
+
41
+ export const STRUCTURAL_QUALITY_DEFAULT_GATE_POLICY = Object.freeze({
42
+ mode: "gate",
43
+ provider: "sentrux",
44
+ maxRegressionPoints: 0,
45
+ dimensionBudgets: STRUCTURAL_QUALITY_DEFAULT_DIMENSION_BUDGETS,
46
+ forbidNewCycles: true,
47
+ minQualitySignal: null,
48
+ minimums: Object.freeze({}),
49
+ optimization: STRUCTURAL_QUALITY_DEFAULT_OPTIMIZATION,
50
+ });
51
+
52
+ export function structuralQualityError(code, message, artifacts = []) {
53
+ const error = new Error(message);
54
+ error.code = code;
55
+ error.artifacts = artifacts;
56
+ return error;
57
+ }
58
+
59
+ export function assertStructuralQualityMode(mode) {
60
+ if (!STRUCTURAL_QUALITY_MODES.includes(mode)) {
61
+ throw structuralQualityError(
62
+ E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID,
63
+ `structuralQuality.mode must be one of ${STRUCTURAL_QUALITY_MODES.join(", ")}`,
64
+ );
65
+ }
66
+ return mode;
67
+ }