@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.
Files changed (93) 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/AGENT_COMPATIBILITY.md +15 -0
  5. package/CLAUDE.md +6 -0
  6. package/DELEGATION_PROTOCOL.md +6 -0
  7. package/DOCS_INDEX.md +9 -2
  8. package/ENG/accessibility-eng.md +12 -2
  9. package/ENG/design-code-eng.md +22 -1
  10. package/LOOP_ENGINEERING.md +41 -0
  11. package/LOOP_SYSTEM_DESIGN.md +33 -0
  12. package/ORCHESTRATOR_INTEGRATION.md +9 -0
  13. package/PROTOCOL_INTEGRATION.md +57 -0
  14. package/QUALITY_SCORECARD.md +2 -0
  15. package/README.md +51 -0
  16. package/TERMINOLOGY.md +12 -0
  17. package/THREAT_MODEL.md +48 -0
  18. package/completions/_forgeloop +5 -1
  19. package/completions/forgeloop.bash +9 -1
  20. package/completions/forgeloop.fish +27 -1
  21. package/docs/ADVISORY_CONTEXT.md +174 -0
  22. package/docs/AGENT_PROTOCOL_SUMMARY.md +33 -2
  23. package/docs/ARTIFACT_REFERENCE.md +142 -0
  24. package/docs/CLI_REFERENCE.md +124 -1
  25. package/docs/CROSS_HARNESS_CONTINUITY.md +85 -0
  26. package/docs/DOCUMENTATION_GUIDE.md +7 -0
  27. package/docs/GETTING_STARTED.md +22 -0
  28. package/docs/KNOWLEDGE_SOURCES.md +171 -0
  29. package/docs/MCP.md +17 -1
  30. package/docs/RECIPES.md +111 -0
  31. package/docs/RELEASE_CHECKLIST.md +14 -0
  32. package/docs/STRUCTURAL_QUALITY.md +350 -0
  33. package/docs/TROUBLESHOOTING.md +161 -2
  34. package/docs/UNIVERSAL_INTEGRATION.md +60 -0
  35. package/package.json +4 -1
  36. package/schemas/config.schema.json +46 -0
  37. package/schemas/handoff-envelope.schema.json +1 -0
  38. package/schemas/preflight.schema.json +2 -1
  39. package/schemas/structural-quality.schema.json +175 -0
  40. package/scripts/check-changelog-freshness.mjs +27 -3
  41. package/scripts/generate-agent-protocol-summary.mjs +18 -0
  42. package/src/cli.js +24 -0
  43. package/src/commands/handoff-accept.js +36 -0
  44. package/src/commands/handoff-list.js +28 -2
  45. package/src/commands/handoff-show.js +27 -2
  46. package/src/commands/quality-baseline.js +28 -0
  47. package/src/commands/quality-status.js +34 -0
  48. package/src/commands/quality-verify.js +30 -0
  49. package/src/commands/reconcile-continuity.js +4 -0
  50. package/src/core/advisory-context/constants.js +74 -0
  51. package/src/core/advisory-context/provider.js +287 -0
  52. package/src/core/advisory-context/service.js +140 -0
  53. package/src/core/artifact-registry.js +12 -0
  54. package/src/core/audit.js +38 -0
  55. package/src/core/bundles.js +134 -1
  56. package/src/core/cli-command-definitions.js +62 -0
  57. package/src/core/command-executors.js +28 -0
  58. package/src/core/command-input.js +23 -1
  59. package/src/core/completion-artifacts.js +2 -0
  60. package/src/core/completion.js +42 -0
  61. package/src/core/config.js +3 -0
  62. package/src/core/continuity-lint.js +89 -0
  63. package/src/core/continuity-reconciliation.js +16 -0
  64. package/src/core/continuity.js +10 -11
  65. package/src/core/error-codes.js +186 -0
  66. package/src/core/events.js +32 -0
  67. package/src/core/execution-profile-context.js +15 -1
  68. package/src/core/filesystem.js +34 -3
  69. package/src/core/handoff-acceptance.js +277 -0
  70. package/src/core/handoff.js +41 -8
  71. package/src/core/inspect.js +64 -0
  72. package/src/core/integration-invocation-policy.js +34 -2
  73. package/src/core/integration-resources.js +38 -1
  74. package/src/core/next-action-model.js +11 -1
  75. package/src/core/next-action-phases.js +84 -5
  76. package/src/core/phase.js +9 -1
  77. package/src/core/portable-context.js +103 -0
  78. package/src/core/preflight.js +33 -0
  79. package/src/core/protocol-info.js +33 -2
  80. package/src/core/runtime-context.js +58 -0
  81. package/src/core/schema-validation.js +1 -0
  82. package/src/core/structural-quality/artifacts.js +329 -0
  83. package/src/core/structural-quality/constants.js +67 -0
  84. package/src/core/structural-quality/policy.js +227 -0
  85. package/src/core/structural-quality/provider.js +287 -0
  86. package/src/core/structural-quality/sentrux-mcp.js +477 -0
  87. package/src/core/structural-quality/service.js +1138 -0
  88. package/src/core/structural-quality/source-fingerprint.js +112 -0
  89. package/src/core/structural-quality/status.js +3 -0
  90. package/src/core/task-paths.js +24 -0
  91. package/src/core/templates.js +1 -0
  92. package/src/integration.d.ts +141 -0
  93. package/src/integration.js +36 -0
@@ -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
+ }
@@ -0,0 +1,227 @@
1
+ import { canonicalFingerprint } from "../artifacts.js";
2
+ import {
3
+ E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID,
4
+ } from "../error-codes.js";
5
+ import {
6
+ STRUCTURAL_QUALITY_DEFAULT_DIMENSION_BUDGETS,
7
+ STRUCTURAL_QUALITY_DEFAULT_OPTIMIZATION,
8
+ STRUCTURAL_QUALITY_MODES,
9
+ STRUCTURAL_QUALITY_ROOT_CAUSES,
10
+ STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN,
11
+ structuralQualityError,
12
+ } from "./constants.js";
13
+ import { structuralQualityProviderCompatibility } from "./provider.js";
14
+
15
+ const POLICY_KEYS = new Set([
16
+ "mode",
17
+ "provider",
18
+ "maxRegressionPoints",
19
+ "dimensionBudgets",
20
+ "forbidNewCycles",
21
+ "minQualitySignal",
22
+ "minimums",
23
+ "optimization",
24
+ ]);
25
+
26
+ function plainObject(value, label) {
27
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
28
+ throw structuralQualityError(E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, `${label} must be an object`);
29
+ }
30
+ return value;
31
+ }
32
+
33
+ function integer(value, label, { min = 0, max = 10_000 } = {}) {
34
+ if (!Number.isInteger(value) || value < min || value > max) {
35
+ throw structuralQualityError(
36
+ E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID,
37
+ `${label} must be an integer between ${min} and ${max}`,
38
+ );
39
+ }
40
+ return value;
41
+ }
42
+
43
+ function normalizeBudgets(value, label) {
44
+ const source = value === undefined ? {} : plainObject(value, label);
45
+ const unknown = Object.keys(source).find((key) => !STRUCTURAL_QUALITY_ROOT_CAUSES.includes(key));
46
+ if (unknown) {
47
+ throw structuralQualityError(E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, `${label} contains unknown root cause: ${unknown}`);
48
+ }
49
+ return Object.fromEntries(STRUCTURAL_QUALITY_ROOT_CAUSES.map((cause) => {
50
+ const raw = source[cause];
51
+ if (raw === null || raw === undefined) {
52
+ return [cause, STRUCTURAL_QUALITY_DEFAULT_DIMENSION_BUDGETS[cause] ?? null];
53
+ }
54
+ return [cause, integer(raw, `${label}.${cause}`)];
55
+ }));
56
+ }
57
+
58
+ function normalizeMinimums(value) {
59
+ const source = value === undefined ? {} : plainObject(value, "structuralQuality.minimums");
60
+ const unknown = Object.keys(source).find((key) => !STRUCTURAL_QUALITY_ROOT_CAUSES.includes(key));
61
+ if (unknown) {
62
+ throw structuralQualityError(E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, `structuralQuality.minimums contains unknown root cause: ${unknown}`);
63
+ }
64
+ return Object.fromEntries(Object.entries(source).map(([cause, minimum]) => [
65
+ cause,
66
+ integer(minimum, `structuralQuality.minimums.${cause}`),
67
+ ]));
68
+ }
69
+
70
+ function normalizeOptimization(value) {
71
+ const source = value === undefined ? {} : plainObject(value, "structuralQuality.optimization");
72
+ const unknown = Object.keys(source).find((key) => !["mode", "maxExtraEvaluations", "minGainPoints"].includes(key));
73
+ if (unknown) {
74
+ throw structuralQualityError(E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, `structuralQuality.optimization contains unknown property: ${unknown}`);
75
+ }
76
+ const mode = source.mode ?? STRUCTURAL_QUALITY_DEFAULT_OPTIMIZATION.mode;
77
+ if (!["off", "bounded"].includes(mode)) {
78
+ throw structuralQualityError(E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, "structuralQuality.optimization.mode must be off or bounded");
79
+ }
80
+ const maxExtraEvaluations = source.maxExtraEvaluations ?? STRUCTURAL_QUALITY_DEFAULT_OPTIMIZATION.maxExtraEvaluations;
81
+ integer(maxExtraEvaluations, "structuralQuality.optimization.maxExtraEvaluations", { min: 0, max: 2 });
82
+ const minGainPoints = source.minGainPoints ?? STRUCTURAL_QUALITY_DEFAULT_OPTIMIZATION.minGainPoints;
83
+ integer(minGainPoints, "structuralQuality.optimization.minGainPoints", { min: 1, max: 10_000 });
84
+ return { mode, maxExtraEvaluations, minGainPoints };
85
+ }
86
+
87
+ export function normalizeStructuralQualityConfig(input) {
88
+ if (input === undefined || input === null) return undefined;
89
+ const source = plainObject(input, "structuralQuality");
90
+ const unknown = Object.keys(source).find((key) => !POLICY_KEYS.has(key));
91
+ if (unknown) {
92
+ throw structuralQualityError(E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, `structuralQuality contains unknown property: ${unknown}`);
93
+ }
94
+ const mode = source.mode ?? "observe";
95
+ if (!STRUCTURAL_QUALITY_MODES.includes(mode)) {
96
+ throw structuralQualityError(E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, `Unknown structural quality mode: ${mode}`);
97
+ }
98
+ const provider = source.provider ?? "sentrux";
99
+ if (typeof provider !== "string" || !STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(provider)) {
100
+ throw structuralQualityError(E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, "structuralQuality.provider must be a lower-case provider ID");
101
+ }
102
+ const maxRegressionPoints = source.maxRegressionPoints ?? 0;
103
+ integer(maxRegressionPoints, "structuralQuality.maxRegressionPoints");
104
+ const minQualitySignal = source.minQualitySignal === null || source.minQualitySignal === undefined
105
+ ? null
106
+ : integer(source.minQualitySignal, "structuralQuality.minQualitySignal");
107
+ if (source.forbidNewCycles !== undefined && typeof source.forbidNewCycles !== "boolean") {
108
+ throw structuralQualityError(E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID, "structuralQuality.forbidNewCycles must be boolean");
109
+ }
110
+ const policy = {
111
+ mode,
112
+ provider,
113
+ maxRegressionPoints,
114
+ dimensionBudgets: normalizeBudgets(source.dimensionBudgets, "structuralQuality.dimensionBudgets"),
115
+ forbidNewCycles: source.forbidNewCycles ?? true,
116
+ minQualitySignal,
117
+ minimums: normalizeMinimums(source.minimums),
118
+ optimization: normalizeOptimization(source.optimization),
119
+ };
120
+ return Object.freeze({
121
+ ...policy,
122
+ dimensionBudgets: Object.freeze(policy.dimensionBudgets),
123
+ minimums: Object.freeze(policy.minimums),
124
+ optimization: Object.freeze(policy.optimization),
125
+ });
126
+ }
127
+
128
+ function snapshotOf(value) {
129
+ return value?.snapshot && typeof value.snapshot === "object" ? value.snapshot : value;
130
+ }
131
+
132
+ function assertComparableInputs(baseline, current) {
133
+ const reasons = [];
134
+ const baselineProvider = baseline?.provider;
135
+ const currentProvider = current?.provider;
136
+ const baselineScope = baseline?.scope;
137
+ const currentScope = current?.scope;
138
+ for (const provider of [baselineProvider, currentProvider]) {
139
+ if (provider?.id === "sentrux" && provider?.version !== undefined && provider?.version !== null) {
140
+ const compatibility = structuralQualityProviderCompatibility(provider);
141
+ if (!compatibility.supported) reasons.push("PROVIDER_VERSION_UNSUPPORTED");
142
+ }
143
+ }
144
+ if (baselineProvider?.id !== undefined && currentProvider?.id !== undefined
145
+ && baselineProvider.id !== currentProvider.id) reasons.push("PROVIDER_ID_CHANGED");
146
+ if (baselineProvider?.measurementModel !== undefined && currentProvider?.measurementModel !== undefined
147
+ && baselineProvider.measurementModel !== currentProvider.measurementModel) reasons.push("MEASUREMENT_MODEL_MISMATCH");
148
+ if (baselineProvider?.compatibilityKey !== undefined && currentProvider?.compatibilityKey !== undefined
149
+ && baselineProvider.compatibilityKey !== currentProvider.compatibilityKey) reasons.push("COMPATIBILITY_KEY_CHANGED");
150
+ if (baselineProvider?.version !== undefined && currentProvider?.version !== undefined
151
+ && baselineProvider.version !== currentProvider.version) {
152
+ const sameCompat = baselineProvider?.compatibilityKey && currentProvider?.compatibilityKey
153
+ && baselineProvider.compatibilityKey === currentProvider.compatibilityKey;
154
+ if (!sameCompat) {
155
+ reasons.push("PROVIDER_VERSION_CHANGED");
156
+ }
157
+ }
158
+ if (baselineScope?.providerConfigFingerprint !== undefined && currentScope?.providerConfigFingerprint !== undefined
159
+ && baselineScope.providerConfigFingerprint !== currentScope.providerConfigFingerprint) reasons.push("PROVIDER_CONFIG_CHANGED");
160
+ if (baseline?.bindings?.policyFingerprint && current?.bindings?.policyFingerprint
161
+ && baseline.bindings.policyFingerprint !== current.bindings.policyFingerprint) reasons.push("POLICY_CHANGED");
162
+ if (baseline?.bindings?.scopeFingerprint && current?.bindings?.scopeFingerprint
163
+ && baseline.bindings.scopeFingerprint !== current.bindings.scopeFingerprint) reasons.push("SCOPE_CHANGED");
164
+ return reasons;
165
+ }
166
+
167
+ export function compareStructuralQuality({ baseline, current, policy } = {}) {
168
+ const normalizedPolicy = normalizeStructuralQualityConfig(policy ?? { mode: "gate", provider: "sentrux" });
169
+ const baselineSnapshot = snapshotOf(baseline);
170
+ const currentSnapshot = snapshotOf(current);
171
+ const incompatibilities = assertComparableInputs(baseline, current);
172
+ const rootCauseDeltas = Object.fromEntries(STRUCTURAL_QUALITY_ROOT_CAUSES.map((cause) => [
173
+ cause,
174
+ Number.isInteger(currentSnapshot?.rootCauses?.[cause]?.score) && Number.isInteger(baselineSnapshot?.rootCauses?.[cause]?.score)
175
+ ? currentSnapshot.rootCauses[cause].score - baselineSnapshot.rootCauses[cause].score
176
+ : null,
177
+ ]));
178
+ const qualityDelta = Number.isInteger(currentSnapshot?.qualitySignal) && Number.isInteger(baselineSnapshot?.qualitySignal)
179
+ ? currentSnapshot.qualitySignal - baselineSnapshot.qualitySignal
180
+ : null;
181
+ const failedConditions = [];
182
+ const reasonCodes = [...incompatibilities];
183
+ if (incompatibilities.length === 0) {
184
+ if (qualityDelta === null || qualityDelta < -normalizedPolicy.maxRegressionPoints) {
185
+ failedConditions.push("qualitySignal");
186
+ }
187
+ for (const cause of STRUCTURAL_QUALITY_ROOT_CAUSES) {
188
+ const budget = normalizedPolicy.dimensionBudgets[cause];
189
+ if (budget !== null && budget !== undefined) {
190
+ const delta = rootCauseDeltas[cause];
191
+ if (delta === null || delta < -budget) failedConditions.push(cause);
192
+ }
193
+ const minimum = normalizedPolicy.minimums[cause];
194
+ if (minimum > 0 && (!Number.isInteger(currentSnapshot?.rootCauses?.[cause]?.score)
195
+ || currentSnapshot.rootCauses[cause].score < minimum)) failedConditions.push(`${cause}:minimum`);
196
+ }
197
+ if (normalizedPolicy.minQualitySignal !== null
198
+ && (!Number.isInteger(currentSnapshot?.qualitySignal) || currentSnapshot.qualitySignal < normalizedPolicy.minQualitySignal)) {
199
+ failedConditions.push("qualitySignal:minimum");
200
+ }
201
+ if (normalizedPolicy.forbidNewCycles
202
+ && Number.isFinite(baselineSnapshot?.rootCauses?.acyclicity?.raw)
203
+ && Number.isFinite(currentSnapshot?.rootCauses?.acyclicity?.raw)
204
+ && currentSnapshot.rootCauses.acyclicity.raw > baselineSnapshot.rootCauses.acyclicity.raw) {
205
+ failedConditions.push("acyclicity:new-cycles");
206
+ }
207
+ if (failedConditions.length > 0) reasonCodes.push("E_STRUCTURAL_QUALITY_REGRESSION");
208
+ } else {
209
+ if (incompatibilities.includes("MEASUREMENT_MODEL_MISMATCH")) {
210
+ reasonCodes.push("E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH");
211
+ }
212
+ reasonCodes.push("E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE");
213
+ }
214
+ const sortedReasons = [...new Set(reasonCodes)].sort();
215
+ return {
216
+ comparable: incompatibilities.length === 0,
217
+ qualityDelta,
218
+ rootCauseDeltas,
219
+ failedConditions: [...new Set(failedConditions)].sort(),
220
+ status: incompatibilities.length > 0 ? "NOT_OBSERVED" : failedConditions.length > 0 ? "FAIL" : "PASS",
221
+ reasonCodes: sortedReasons,
222
+ };
223
+ }
224
+
225
+ export function structuralQualityPolicyFingerprint(policy) {
226
+ return canonicalFingerprint(normalizeStructuralQualityConfig(policy));
227
+ }