@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,1138 @@
1
+ import { canonicalFingerprint, readJsonArtifact } from "../artifacts.js";
2
+ import { readConfig } from "../config.js";
3
+ import { readContract } from "../contract.js";
4
+ import { readPersistedRoute } from "../route-artifact.js";
5
+ import { readWorkState } from "../work-state.js";
6
+ import { evaluatePreflight, validatePersistedPreflight } from "../preflight.js";
7
+ import { taskArtifactPath } from "../task-paths.js";
8
+ import {
9
+ E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH,
10
+ E_STRUCTURAL_QUALITY_BASELINE_MISSING,
11
+ E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE,
12
+ E_STRUCTURAL_QUALITY_EVIDENCE_STALE,
13
+ E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH,
14
+ E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE,
15
+ E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE,
16
+ E_STRUCTURAL_QUALITY_SOURCE_DRIFT,
17
+ E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE,
18
+ } from "../error-codes.js";
19
+ import {
20
+ STRUCTURAL_QUALITY_CHECK_ID,
21
+ STRUCTURAL_QUALITY_DEFAULT_TIMEOUT_MS,
22
+ STRUCTURAL_QUALITY_MEASUREMENT_MODEL,
23
+ STRUCTURAL_QUALITY_REQUIREMENT,
24
+ structuralQualityError,
25
+ } from "./constants.js";
26
+ import { normalizeStructuralQualityConfig, compareStructuralQuality, structuralQualityPolicyFingerprint } from "./policy.js";
27
+ import { normalizeStructuralQualityDetection, normalizeStructuralQualitySnapshot, providerInputFor, resolveStructuralQualityProvider, structuralQualityProviderCompatibility } from "./provider.js";
28
+ import {
29
+ assertStructuralQualityBindings,
30
+ listStructuralQualityEvaluations,
31
+ readStructuralQualityBaseline,
32
+ validateStructuralQualityArtifact,
33
+ writeStructuralQualityBaseline,
34
+ writeStructuralQualityEvaluation,
35
+ } from "./artifacts.js";
36
+ import { appendProtocolEvent } from "../events.js";
37
+ import { getPackageRoot } from "../templates.js";
38
+ import { withTaskMutation } from "../task-command.js";
39
+ import { findTaskById } from "../task-discovery.js";
40
+ import { currentChangedPaths } from "../repository.js";
41
+ import { assertClaimsCoverChangedPaths } from "../task-scope.js";
42
+ import { computeMaterialSourceFingerprint } from "./source-fingerprint.js";
43
+
44
+ function qualityError(code, message, artifacts = []) {
45
+ return structuralQualityError(code, message, artifacts);
46
+ }
47
+
48
+ function isRecord(value) {
49
+ return value !== null && typeof value === "object" && !Array.isArray(value);
50
+ }
51
+
52
+ async function loadOptionalConfig(target, packageRoot) {
53
+ try {
54
+ return await readConfig(target, packageRoot);
55
+ } catch (error) {
56
+ if (error.code === "ARTIFACT_MISSING") return null;
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ function configuredPolicy(config) {
62
+ if (!config?.structuralQuality) return null;
63
+ return normalizeStructuralQualityConfig(config.structuralQuality);
64
+ }
65
+
66
+ async function scopeIdentity(target, provider = null, providerId = null) {
67
+ let providerConfigFingerprint = null;
68
+ let architectureRulesFingerprint = null;
69
+ const effectiveProviderId = provider?.id ?? providerId;
70
+ if (provider && typeof provider.scopeBinding === "function") {
71
+ const binding = await provider.scopeBinding({ projectPath: target });
72
+ providerConfigFingerprint = binding?.providerConfigFingerprint ?? null;
73
+ architectureRulesFingerprint = binding?.architectureRulesFingerprint ?? null;
74
+ }
75
+ const scope = {
76
+ kind: "PROJECT",
77
+ projectRoot: ".",
78
+ providerConfigFingerprint,
79
+ ...(architectureRulesFingerprint !== null ? { architectureRulesFingerprint } : {}),
80
+ };
81
+ return {
82
+ scope,
83
+ // Architecture rules are a separate policy sensor. They are retained for
84
+ // provenance, but must not make identical Structural Quality measurements
85
+ // incomparable when a project rule changes.
86
+ scopeFingerprint: canonicalFingerprint({
87
+ providerId: effectiveProviderId,
88
+ kind: scope.kind,
89
+ projectRoot: scope.projectRoot,
90
+ providerConfigFingerprint: scope.providerConfigFingerprint,
91
+ }),
92
+ };
93
+ }
94
+
95
+ async function resolveProviderForContext({ target, taskId, policy, runtimeContext, timeoutMs, maxOutputBytes } = {}) {
96
+ if (!policy || policy.mode === "off") return null;
97
+ try {
98
+ return await resolveStructuralQualityProvider({
99
+ providerName: policy.provider,
100
+ target,
101
+ taskId,
102
+ timeoutMs,
103
+ maxOutputBytes,
104
+ runtimeContext,
105
+ });
106
+ } catch (error) {
107
+ if (error.code === E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE) return null;
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ function providerMetadata(provider, detection) {
113
+ const metadata = provider?.metadata ?? provider;
114
+ const id = metadata?.id ?? detection.providerId;
115
+ const version = metadata?.version ?? detection.providerVersion ?? null;
116
+ return {
117
+ id,
118
+ version,
119
+ transport: metadata?.transport ?? detection.transport ?? "runtime",
120
+ executionMode: metadata?.executionMode ?? "runtime-context",
121
+ measurementModel: metadata?.measurementModel ?? detection.measurementModel ?? STRUCTURAL_QUALITY_MEASUREMENT_MODEL,
122
+ compatibilityKey: metadata?.compatibilityKey ?? detection.compatibilityKey ?? null,
123
+ };
124
+ }
125
+
126
+ function providerDetection(value, providerId) {
127
+ return normalizeStructuralQualityDetection(value, { providerId, id: providerId, transport: "runtime" });
128
+ }
129
+
130
+ function normalizeScanResult(result, provider, detection, projectPath) {
131
+ const source = isRecord(result) ? result : { snapshot: result };
132
+ const snapshot = normalizeStructuralQualitySnapshot(source.snapshot ?? source, { projectPath });
133
+ const metadata = providerMetadata(source.provider, detection);
134
+ if (metadata.id !== provider.id) {
135
+ throw qualityError(E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, "Structural-quality scan returned a different provider identity");
136
+ }
137
+ return {
138
+ snapshot,
139
+ provider: metadata,
140
+ detection: providerDetection(source.detection ?? detection, provider.id),
141
+ providerScopeBinding: source.providerScopeBinding ?? null,
142
+ };
143
+ }
144
+
145
+ export async function resolveStructuralQualityContext({
146
+ target,
147
+ packageRoot = getPackageRoot(),
148
+ taskId,
149
+ runtimeContext,
150
+ timeoutMs,
151
+ maxOutputBytes,
152
+ resolveProvider = true,
153
+ } = {}) {
154
+ if (typeof taskId !== "string" || taskId.trim() === "") {
155
+ throw qualityError(E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, "Structural-quality operations require a task ID");
156
+ }
157
+ const state = await readWorkState(target, { packageRoot, taskId });
158
+ if (!state) throw qualityError(E_STRUCTURAL_QUALITY_BASELINE_MISSING, "Structural-quality operations require task work-state", [taskArtifactPath(taskId, "state")]);
159
+ const contract = await readContract(target, packageRoot, { taskId });
160
+ const route = await readPersistedRoute(target, packageRoot, { taskId });
161
+ const config = await loadOptionalConfig(target, packageRoot);
162
+ const policy = configuredPolicy(config);
163
+ const provider = resolveProvider
164
+ ? await resolveProviderForContext({ target, taskId, policy, runtimeContext, timeoutMs, maxOutputBytes })
165
+ : null;
166
+ const scopeData = await scopeIdentity(target, provider, policy?.provider ?? null);
167
+ return {
168
+ state,
169
+ contract,
170
+ route,
171
+ config,
172
+ policy,
173
+ provider,
174
+ providerId: provider?.id ?? policy?.provider ?? null,
175
+ scope: scopeData.scope,
176
+ scopeFingerprint: scopeData.scopeFingerprint,
177
+ };
178
+ }
179
+
180
+ async function taskInputs(target, packageRoot, taskId, options = {}) {
181
+ return resolveStructuralQualityContext({ target, packageRoot, taskId, ...options });
182
+ }
183
+
184
+ function baseBindings(inputs, policyFingerprint, baselineFingerprint = null, sourceMaterialFingerprint = null) {
185
+ return {
186
+ contractFingerprint: inputs.contract.fingerprint,
187
+ routeFingerprint: inputs.route.fingerprint,
188
+ policyFingerprint,
189
+ scopeFingerprint: inputs.scopeFingerprint,
190
+ baselineFingerprint,
191
+ sourceMaterialFingerprint,
192
+ stateRevision: inputs.state.revision ?? 0,
193
+ };
194
+ }
195
+
196
+ function statusForComparison(policy, comparison) {
197
+ if (comparison.status === "PASS") return "PASS";
198
+ if (comparison.status === "FAIL") return "FAIL";
199
+ return policy.mode === "gate" ? "BLOCKED" : "NOT_OBSERVED";
200
+ }
201
+
202
+ function checkProjection(policy, evaluation) {
203
+ if (evaluation.status === "PASS") return { status: "passed", evidenceKind: "OBSERVED" };
204
+ if (evaluation.status === "FAIL") return { status: "failed", evidenceKind: "OBSERVED" };
205
+ if (policy.mode === "gate" && evaluation.status === "BLOCKED") return { status: "blocked", evidenceKind: "BLOCKED" };
206
+ return { status: "not-run", evidenceKind: "NOT_VERIFIED" };
207
+ }
208
+
209
+ function evaluationDetails(evaluation) {
210
+ const comparison = evaluation.comparison ?? {};
211
+ return {
212
+ verificationCycle: evaluation.verificationCycle,
213
+ attempt: evaluation.attempt,
214
+ artifactRef: evaluation.artifactRef,
215
+ artifactFingerprint: evaluation.artifactFingerprint,
216
+ baselineSignal: evaluation.baselineSignal ?? null,
217
+ currentSignal: evaluation.currentSignal ?? null,
218
+ delta: comparison.qualityDelta ?? null,
219
+ bottleneck: evaluation.snapshot?.bottleneck ?? null,
220
+ rootCauseDeltas: comparison.rootCauseDeltas ?? {},
221
+ reasonCodes: [...(evaluation.reasonCodes ?? [])],
222
+ failedConditions: [...(comparison.failedConditions ?? [])],
223
+ };
224
+ }
225
+
226
+ async function assertReadyPreflight(target, packageRoot, taskId) {
227
+ const preflightPath = taskArtifactPath(taskId, "preflight");
228
+ const persisted = await readJsonArtifact(target, preflightPath, "preflight", packageRoot);
229
+ const current = await evaluatePreflight({ target, packageRoot, taskId });
230
+ const errors = validatePersistedPreflight(persisted.value, current);
231
+ if (current.status !== "READY" || errors.length > 0) {
232
+ const first = errors[0] ?? current.errors?.[0];
233
+ throw qualityError(first?.code ?? "E_PREFLIGHT_NOT_READY", first?.message ?? "A READY preflight is required before structural-quality baseline capture", [preflightPath]);
234
+ }
235
+ }
236
+
237
+ async function invokeProvider({ target, taskId, policy, provider: suppliedProvider = null, runtimeContext, timeoutMs, maxOutputBytes }) {
238
+ const provider = suppliedProvider ?? await resolveStructuralQualityProvider({
239
+ providerName: policy.provider,
240
+ target,
241
+ taskId,
242
+ timeoutMs,
243
+ maxOutputBytes,
244
+ runtimeContext,
245
+ });
246
+ const input = providerInputFor({
247
+ projectPath: target,
248
+ taskId,
249
+ timeoutMs: timeoutMs ?? STRUCTURAL_QUALITY_DEFAULT_TIMEOUT_MS,
250
+ maxOutputBytes: maxOutputBytes ?? 2 * 1024 * 1024,
251
+ });
252
+ if (typeof provider.observe === "function") {
253
+ const result = await provider.observe(input);
254
+ return normalizeScanResult(result, provider, result.detection, target);
255
+ }
256
+ let detection;
257
+ try {
258
+ detection = providerDetection(await provider.detect(input), provider.id);
259
+ } catch (error) {
260
+ detection = providerDetection({
261
+ available: false,
262
+ providerId: provider.id,
263
+ providerVersion: null,
264
+ transport: "runtime",
265
+ measurementModel: STRUCTURAL_QUALITY_MEASUREMENT_MODEL,
266
+ compatibilityKey: null,
267
+ reasonCode: error.code ?? E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE,
268
+ }, provider.id);
269
+ }
270
+ if (!detection.available) {
271
+ throw qualityError(detection.reasonCode ?? E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE, `Structural-quality provider ${provider.id} is unavailable`);
272
+ }
273
+ const result = await provider.scan(input);
274
+ return normalizeScanResult(result, provider, detection, target);
275
+ }
276
+
277
+ function emptyProjection(policy, reasonCodes = []) {
278
+ return {
279
+ mode: policy?.mode ?? "off",
280
+ provider: policy?.mode && policy.mode !== "off" ? policy.provider : null,
281
+ baseline: { status: policy ? "MISSING" : "NOT_REQUESTED", qualitySignal: null, artifactRef: null, fingerprint: null },
282
+ current: { status: "NOT_OBSERVED", verificationCycle: null, attempt: null, qualitySignal: null, delta: null, bottleneck: null, artifactRef: null },
283
+ comparable: null,
284
+ completionRequired: policy?.mode === "gate",
285
+ reasonCodes: [...new Set(reasonCodes)].sort(),
286
+ next: policy?.mode === "gate" ? "CAPTURE_STRUCTURAL_QUALITY_BASELINE" : null,
287
+ optimization: policy ? optimizationProjection(policy, [], null) : null,
288
+ };
289
+ }
290
+
291
+ export async function captureStructuralQualityBaseline({ target, packageRoot = getPackageRoot(), taskId, replace = false, timeoutMs, maxOutputBytes, runtimeContext } = {}) {
292
+ const inputs = await taskInputs(target, packageRoot, taskId, { runtimeContext, timeoutMs, maxOutputBytes });
293
+ const policy = inputs.policy;
294
+ if (!policy || policy.mode === "off") return { status: "NOT_REQUESTED", mode: "off", provider: null, artifactRef: null };
295
+ if (inputs.state.phase !== "PLANNED") {
296
+ throw qualityError("E_STRUCTURAL_QUALITY_BASELINE_PHASE_INVALID", `Structural-quality baseline capture requires PLANNED; found ${inputs.state.phase}`, [taskArtifactPath(taskId, "state")]);
297
+ }
298
+ await assertReadyPreflight(target, packageRoot, taskId);
299
+ const existing = await readStructuralQualityBaseline(target, taskId, packageRoot);
300
+ if (existing && !replace) {
301
+ return {
302
+ status: "EXISTING",
303
+ mode: policy.mode,
304
+ provider: existing.value.provider,
305
+ artifactRef: existing.path,
306
+ artifactFingerprint: existing.fingerprint,
307
+ baseline: existing.value,
308
+ existing: true,
309
+ identical: true,
310
+ };
311
+ }
312
+ const policyFingerprint = structuralQualityPolicyFingerprint(policy);
313
+ const sourceFingerprintBefore = await computeMaterialSourceFingerprint(target);
314
+ let scan;
315
+ try {
316
+ scan = await invokeProvider({ target, taskId, policy, provider: inputs.provider, runtimeContext, timeoutMs, maxOutputBytes });
317
+ } catch (error) {
318
+ if (policy.mode === "observe") {
319
+ return {
320
+ status: "NOT_OBSERVED",
321
+ mode: policy.mode,
322
+ provider: {
323
+ id: policy.provider,
324
+ version: null,
325
+ transport: "runtime",
326
+ executionMode: "runtime-context",
327
+ measurementModel: STRUCTURAL_QUALITY_MEASUREMENT_MODEL,
328
+ compatibilityKey: null,
329
+ },
330
+ artifactRef: null,
331
+ artifactFingerprint: null,
332
+ reasonCodes: [error.code ?? E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE],
333
+ errorCode: error.code ?? E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE,
334
+ existing: false,
335
+ identical: false,
336
+ };
337
+ }
338
+ throw error;
339
+ }
340
+ const sourceFingerprintAfter = await computeMaterialSourceFingerprint(target);
341
+ if (sourceFingerprintBefore !== sourceFingerprintAfter) {
342
+ if (policy.mode === "observe") {
343
+ return {
344
+ status: "NOT_OBSERVED",
345
+ mode: policy.mode,
346
+ provider: scan.provider,
347
+ artifactRef: null,
348
+ artifactFingerprint: null,
349
+ reasonCodes: [E_STRUCTURAL_QUALITY_SOURCE_DRIFT],
350
+ errorCode: E_STRUCTURAL_QUALITY_SOURCE_DRIFT,
351
+ existing: false,
352
+ identical: false,
353
+ };
354
+ }
355
+ throw qualityError(E_STRUCTURAL_QUALITY_SOURCE_DRIFT, "Source material was mutated during structural-quality baseline capture");
356
+ }
357
+
358
+ const updatedInputs = inputs;
359
+
360
+ const value = {
361
+ schemaVersion: 1,
362
+ protocolVersion: 1,
363
+ role: "BASELINE",
364
+ taskId,
365
+ capturedAt: new Date().toISOString(),
366
+ verificationCycle: null,
367
+ attempt: 1,
368
+ status: "PASS",
369
+ reasonCodes: [],
370
+ bindings: baseBindings(updatedInputs, policyFingerprint, null, sourceFingerprintBefore),
371
+ sourceObservation: {
372
+ beforeFingerprint: sourceFingerprintBefore,
373
+ afterFingerprint: sourceFingerprintAfter,
374
+ stable: true,
375
+ },
376
+ provider: scan.provider,
377
+ detection: scan.detection,
378
+ scope: updatedInputs.scope,
379
+ snapshot: scan.snapshot,
380
+ };
381
+ const written = await writeStructuralQualityBaseline(target, taskId, value, packageRoot, { phase: inputs.state.phase, replace, taskId });
382
+ if (!written.identical) {
383
+ await appendProtocolEvent(target, {
384
+ taskId,
385
+ event: "STRUCTURAL_QUALITY_BASELINE_RECORDED",
386
+ details: {
387
+ artifactRef: written.path,
388
+ artifactFingerprint: written.fingerprint,
389
+ providerId: scan.provider.id,
390
+ providerVersion: scan.provider.version,
391
+ replaced: Boolean(existing),
392
+ supersededFingerprint: existing?.fingerprint ?? null,
393
+ },
394
+ }, packageRoot, { taskId });
395
+ }
396
+ return {
397
+ status: written.existing ? "REPLACED" : "CAPTURED",
398
+ mode: policy.mode,
399
+ provider: scan.provider,
400
+ artifactRef: written.path,
401
+ artifactFingerprint: written.fingerprint,
402
+ baseline: written.value,
403
+ existing: written.existing,
404
+ identical: written.identical,
405
+ };
406
+ }
407
+
408
+ async function persistEvaluationAndCheck({ target, packageRoot, taskId, inputs, persisted, authorityContext, runtimeContext }) {
409
+ const { prepareCompletion, recordCheck } = await import("../completion-artifacts.js");
410
+ const staleRecoveryResult = (freshness, effectiveTaskId = taskId) => ({
411
+ status: inputs.policy.mode === "gate" ? "BLOCKED" : "NOT_OBSERVED",
412
+ mode: inputs.policy.mode,
413
+ evaluation: persisted,
414
+ check: null,
415
+ receipt: null,
416
+ attempts: persisted.attempt,
417
+ reasonCodes: freshness.reasonCodes,
418
+ errorCode: E_STRUCTURAL_QUALITY_EVIDENCE_STALE,
419
+ taskId: effectiveTaskId,
420
+ });
421
+ if (["PASS", "FAIL"].includes(persisted.status)) {
422
+ const initialFreshness = await validateStructuralQualityEvaluationFreshness({
423
+ target,
424
+ packageRoot,
425
+ taskId,
426
+ artifact: persisted,
427
+ inputs,
428
+ runtimeContext,
429
+ });
430
+ if (!initialFreshness.valid) return staleRecoveryResult(initialFreshness);
431
+ }
432
+ const record = (context, evaluation) => recordCheck({
433
+ target,
434
+ packageRoot,
435
+ id: STRUCTURAL_QUALITY_CHECK_ID,
436
+ kind: "structural-quality",
437
+ requirement: STRUCTURAL_QUALITY_REQUIREMENT,
438
+ status: checkProjection(inputs.policy, evaluation).status,
439
+ evidenceKind: checkProjection(inputs.policy, evaluation).evidenceKind,
440
+ result: `structural quality ${evaluation.status}`,
441
+ details: evaluationDetails(evaluation),
442
+ authorityContext,
443
+ runtimeContext,
444
+ taskId: context?.taskId ?? taskId,
445
+ });
446
+ const recorded = await withTaskMutation(target, { taskId, packageRoot }, "quality-verify-check", async (context) => {
447
+ const effectiveTaskId = context?.taskId ?? taskId;
448
+ const evaluations = await listStructuralQualityEvaluations(target, effectiveTaskId, packageRoot);
449
+ const latest = evaluations
450
+ .filter((item) => item.value.verificationCycle === persisted.verificationCycle)
451
+ .at(-1);
452
+ const evaluation = latest && latest.value.attempt > persisted.attempt
453
+ ? { ...latest.value, artifactRef: latest.path, artifactFingerprint: latest.fingerprint }
454
+ : persisted;
455
+ if (["PASS", "FAIL"].includes(evaluation.status)) {
456
+ const freshness = await validateStructuralQualityEvaluationFreshness({
457
+ target,
458
+ packageRoot,
459
+ taskId: effectiveTaskId,
460
+ artifact: evaluation,
461
+ inputs: await resolveStructuralQualityContext({ target, packageRoot, taskId: effectiveTaskId, runtimeContext }),
462
+ runtimeContext,
463
+ });
464
+ if (!freshness.valid) return staleRecoveryResult(freshness, effectiveTaskId);
465
+ }
466
+ const publish = () => record(context, evaluation);
467
+ try {
468
+ return await publish();
469
+ } catch (error) {
470
+ if (error.code !== "E_RECEIPT_MISSING") throw error;
471
+ await prepareCompletion({
472
+ target,
473
+ packageRoot,
474
+ authorityContext,
475
+ runtimeContext,
476
+ taskId: effectiveTaskId,
477
+ });
478
+ return publish();
479
+ }
480
+ });
481
+ return { evaluation: persisted, check: recorded.check, receipt: recorded };
482
+ }
483
+
484
+ function nonPassEvaluation({ taskId, inputs, cycle, attempt, policy, reasonCode, reasonCodes = [], providerId = policy.provider, providerVersion = null, detection = null, baseline = null, sourceObservation = null, sourceMaterialFingerprint = null }) {
485
+ const status = policy.mode === "gate" ? "BLOCKED" : "NOT_OBSERVED";
486
+ const detectionValue = detection ?? {
487
+ available: false,
488
+ providerId,
489
+ providerVersion,
490
+ transport: "runtime",
491
+ measurementModel: STRUCTURAL_QUALITY_MEASUREMENT_MODEL,
492
+ compatibilityKey: null,
493
+ reasonCode,
494
+ };
495
+ const policyFingerprint = structuralQualityPolicyFingerprint(policy);
496
+ return {
497
+ schemaVersion: 1,
498
+ protocolVersion: 1,
499
+ role: "EVALUATION",
500
+ taskId,
501
+ capturedAt: new Date().toISOString(),
502
+ verificationCycle: cycle,
503
+ attempt,
504
+ status,
505
+ reasonCodes: [...new Set([reasonCode, ...reasonCodes])],
506
+ errorCode: reasonCode,
507
+ bindings: baseBindings(inputs, policyFingerprint, baseline?.fingerprint ?? null, sourceMaterialFingerprint),
508
+ ...(sourceObservation ? { sourceObservation } : {}),
509
+ provider: {
510
+ id: providerId,
511
+ version: providerVersion,
512
+ transport: detectionValue.transport,
513
+ executionMode: "runtime-context",
514
+ measurementModel: detectionValue.measurementModel ?? STRUCTURAL_QUALITY_MEASUREMENT_MODEL,
515
+ compatibilityKey: detectionValue.compatibilityKey ?? null,
516
+ },
517
+ detection: detectionValue,
518
+ scope: inputs.scope,
519
+ ...(baseline?.value?.snapshot ? { baselineSignal: baseline.value.snapshot.qualitySignal } : {}),
520
+ comparison: {
521
+ comparable: false,
522
+ qualityDelta: null,
523
+ rootCauseDeltas: Object.fromEntries(["modularity", "acyclicity", "depth", "equality", "redundancy"].map((key) => [key, null])),
524
+ failedConditions: [],
525
+ status: "NOT_OBSERVED",
526
+ reasonCodes: [...new Set([reasonCode, ...reasonCodes])],
527
+ },
528
+ };
529
+ }
530
+
531
+ export async function evaluateStructuralQuality({ target, packageRoot = getPackageRoot(), taskId, timeoutMs, maxOutputBytes, authorityContext, runtimeContext } = {}) {
532
+ const initialInputs = await taskInputs(target, packageRoot, taskId, { runtimeContext, timeoutMs, maxOutputBytes });
533
+ const policy = initialInputs.policy;
534
+ if (!policy || policy.mode === "off") return { status: "NOT_REQUESTED", mode: "off", check: null };
535
+
536
+ if (initialInputs.state.phase !== "VERIFYING") {
537
+ throw qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `Structural-quality verification requires VERIFYING; found ${initialInputs.state.phase}`, [taskArtifactPath(taskId, "state")]);
538
+ }
539
+
540
+ const cycle = initialInputs.state.verificationCycle ?? 1;
541
+ const prior = await listStructuralQualityEvaluations(target, taskId, packageRoot);
542
+ const attemptsInCycle = prior.filter((item) => item.value.verificationCycle === cycle);
543
+ const latestEvaluation = attemptsInCycle.at(-1);
544
+
545
+ // Check if projection reconciliation is needed for an existing evaluation
546
+ if (latestEvaluation) {
547
+ let checkProjected = false;
548
+ try {
549
+ const receiptArtifact = await readJsonArtifact(target, taskArtifactPath(taskId, "receipt"), "execution-receipt", packageRoot);
550
+ const matchingCheck = receiptArtifact.value?.checks?.find((c) => c.id === STRUCTURAL_QUALITY_CHECK_ID);
551
+ if (matchingCheck
552
+ && matchingCheck.details?.artifactRef === latestEvaluation.path
553
+ && matchingCheck.details?.artifactFingerprint === latestEvaluation.fingerprint) {
554
+ checkProjected = true;
555
+ }
556
+ } catch {
557
+ checkProjected = false;
558
+ }
559
+
560
+ if (!checkProjected) {
561
+ // Reconcile and repair the orphaned check projection without rescanning or consuming another attempt
562
+ return persistEvaluationAndCheck({
563
+ target,
564
+ packageRoot,
565
+ taskId,
566
+ inputs: initialInputs,
567
+ persisted: {
568
+ ...latestEvaluation.value,
569
+ artifactRef: latestEvaluation.path,
570
+ artifactFingerprint: latestEvaluation.fingerprint,
571
+ },
572
+ authorityContext,
573
+ runtimeContext,
574
+ });
575
+ }
576
+ }
577
+
578
+ const maxAttempts = 1 + (policy.optimization.mode === "bounded" ? policy.optimization.maxExtraEvaluations : 0);
579
+ if (attemptsInCycle.length >= maxAttempts) {
580
+ const last = attemptsInCycle.at(-1);
581
+ const evalVal = last ? { ...last.value, artifactRef: last.path, artifactFingerprint: last.fingerprint } : null;
582
+ const evaluationStatus = evalVal?.status ?? (policy.mode === "gate" ? "BLOCKED" : "NOT_OBSERVED");
583
+ return {
584
+ status: evaluationStatus === "PASS" ? "CONVERGED" : evaluationStatus,
585
+ mode: policy.mode,
586
+ evaluation: evalVal,
587
+ check: null,
588
+ attempts: attemptsInCycle.length,
589
+ };
590
+ }
591
+
592
+ const attempt = attemptsInCycle.length + 1;
593
+ const reservationEpoch = {
594
+ stateRevision: initialInputs.state.revision ?? 0,
595
+ cycle,
596
+ };
597
+
598
+ const baseline = await readStructuralQualityBaseline(target, taskId, packageRoot);
599
+
600
+ // Execute provider observation OUTSIDE task mutation lock
601
+ const sourceFingerprintBefore = await computeMaterialSourceFingerprint(target);
602
+ let evaluation;
603
+
604
+ if (!baseline) {
605
+ evaluation = nonPassEvaluation({
606
+ taskId,
607
+ inputs: initialInputs,
608
+ cycle,
609
+ attempt,
610
+ policy,
611
+ reasonCode: E_STRUCTURAL_QUALITY_BASELINE_MISSING,
612
+ sourceObservation: { beforeFingerprint: sourceFingerprintBefore, afterFingerprint: sourceFingerprintBefore, stable: true },
613
+ sourceMaterialFingerprint: sourceFingerprintBefore,
614
+ });
615
+ } else {
616
+ try {
617
+ const scopeChanged = baseline.value.bindings?.scopeFingerprint !== initialInputs.scopeFingerprint;
618
+ if (scopeChanged) {
619
+ evaluation = nonPassEvaluation({
620
+ taskId,
621
+ inputs: initialInputs,
622
+ cycle,
623
+ attempt,
624
+ policy,
625
+ reasonCode: E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE,
626
+ reasonCodes: ["PROVIDER_CONFIG_CHANGED"],
627
+ baseline,
628
+ sourceObservation: { beforeFingerprint: sourceFingerprintBefore, afterFingerprint: sourceFingerprintBefore, stable: true },
629
+ sourceMaterialFingerprint: sourceFingerprintBefore,
630
+ });
631
+ evaluation.comparison.reasonCodes = [E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE, "PROVIDER_CONFIG_CHANGED"];
632
+ } else {
633
+ assertStructuralQualityBindings(baseline.value, {
634
+ taskId,
635
+ contractFingerprint: initialInputs.contract.fingerprint,
636
+ routeFingerprint: initialInputs.route.fingerprint,
637
+ policyFingerprint: structuralQualityPolicyFingerprint(policy),
638
+ scopeFingerprint: initialInputs.scopeFingerprint,
639
+ });
640
+ const scan = await invokeProvider({ target, taskId, policy, provider: initialInputs.provider, runtimeContext, timeoutMs, maxOutputBytes });
641
+ const sourceFingerprintAfter = await computeMaterialSourceFingerprint(target);
642
+ const isStable = sourceFingerprintBefore === sourceFingerprintAfter;
643
+
644
+ if (!isStable) {
645
+ evaluation = nonPassEvaluation({
646
+ taskId,
647
+ inputs: initialInputs,
648
+ cycle,
649
+ attempt,
650
+ policy,
651
+ reasonCode: E_STRUCTURAL_QUALITY_SOURCE_DRIFT,
652
+ providerId: scan.provider.id,
653
+ providerVersion: scan.provider.version,
654
+ detection: scan.detection,
655
+ baseline,
656
+ sourceObservation: { beforeFingerprint: sourceFingerprintBefore, afterFingerprint: sourceFingerprintAfter, stable: false },
657
+ sourceMaterialFingerprint: sourceFingerprintBefore,
658
+ });
659
+ } else {
660
+ const updatedInputs = initialInputs;
661
+ const comparison = compareStructuralQuality({
662
+ baseline: baseline.value,
663
+ current: {
664
+ ...scan,
665
+ bindings: baseBindings(updatedInputs, structuralQualityPolicyFingerprint(policy), baseline.fingerprint, sourceFingerprintBefore),
666
+ scope: initialInputs.scope,
667
+ },
668
+ policy,
669
+ });
670
+ const status = statusForComparison(policy, comparison);
671
+ evaluation = {
672
+ schemaVersion: 1,
673
+ protocolVersion: 1,
674
+ role: "EVALUATION",
675
+ taskId,
676
+ capturedAt: new Date().toISOString(),
677
+ verificationCycle: cycle,
678
+ attempt,
679
+ status,
680
+ reasonCodes: comparison.reasonCodes,
681
+ errorCode: null,
682
+ bindings: baseBindings(updatedInputs, structuralQualityPolicyFingerprint(policy), baseline.fingerprint, sourceFingerprintBefore),
683
+ sourceObservation: {
684
+ beforeFingerprint: sourceFingerprintBefore,
685
+ afterFingerprint: sourceFingerprintAfter,
686
+ stable: true,
687
+ },
688
+ provider: scan.provider,
689
+ detection: scan.detection,
690
+ scope: initialInputs.scope,
691
+ baselineSignal: baseline.value.snapshot?.qualitySignal,
692
+ currentSignal: scan.snapshot.qualitySignal,
693
+ snapshot: scan.snapshot,
694
+ comparison,
695
+ };
696
+ }
697
+ }
698
+ } catch (error) {
699
+ const sourceFingerprintAfter = await computeMaterialSourceFingerprint(target).catch(() => sourceFingerprintBefore);
700
+ const code = error.code ?? E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE;
701
+ evaluation = nonPassEvaluation({
702
+ taskId,
703
+ inputs: initialInputs,
704
+ cycle,
705
+ attempt,
706
+ policy,
707
+ reasonCode: code,
708
+ providerId: policy.provider,
709
+ baseline,
710
+ sourceObservation: {
711
+ beforeFingerprint: sourceFingerprintBefore,
712
+ afterFingerprint: sourceFingerprintAfter,
713
+ stable: sourceFingerprintBefore === sourceFingerprintAfter,
714
+ },
715
+ sourceMaterialFingerprint: sourceFingerprintBefore,
716
+ });
717
+ }
718
+ }
719
+
720
+ // Commit under task mutation lock
721
+ const captured = await withTaskMutation(target, { taskId, packageRoot }, "quality-verify-evaluation", async (context) => {
722
+ const lockedTaskId = context?.taskId ?? taskId;
723
+ const inputs = await taskInputs(target, packageRoot, lockedTaskId, { runtimeContext, timeoutMs, maxOutputBytes });
724
+ if (inputs.state.phase !== "VERIFYING") {
725
+ throw qualityError(E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE, `Task phase changed during observation; found ${inputs.state.phase}`);
726
+ }
727
+ if ((inputs.state.verificationCycle ?? 1) !== reservationEpoch.cycle) {
728
+ throw qualityError(E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE, "Task verification cycle changed during observation");
729
+ }
730
+ if (inputs.contract.fingerprint !== initialInputs.contract.fingerprint
731
+ || inputs.route.fingerprint !== initialInputs.route.fingerprint
732
+ || structuralQualityPolicyFingerprint(inputs.policy) !== structuralQualityPolicyFingerprint(initialInputs.policy)
733
+ || inputs.scopeFingerprint !== initialInputs.scopeFingerprint) {
734
+ throw qualityError(E_STRUCTURAL_QUALITY_OBSERVATION_EPOCH_STALE, "Task state configuration changed during observation");
735
+ }
736
+
737
+ const currentAttempts = (await listStructuralQualityEvaluations(target, lockedTaskId, packageRoot))
738
+ .filter((item) => item.value.verificationCycle === cycle);
739
+ const assignedAttempt = currentAttempts.length + 1;
740
+
741
+ if (assignedAttempt > maxAttempts) {
742
+ const last = currentAttempts.at(-1);
743
+ return {
744
+ inputs,
745
+ evaluation: last ? { ...last.value, artifactRef: last.path, artifactFingerprint: last.fingerprint } : null,
746
+ converged: true,
747
+ attempts: currentAttempts.length,
748
+ };
749
+ }
750
+
751
+ const assignedEvaluation = {
752
+ ...evaluation,
753
+ attempt: assignedAttempt,
754
+ bindings: {
755
+ ...evaluation.bindings,
756
+ stateRevision: inputs.state.revision ?? 0,
757
+ },
758
+ };
759
+
760
+ const written = await writeStructuralQualityEvaluation(
761
+ target,
762
+ lockedTaskId,
763
+ assignedEvaluation.verificationCycle,
764
+ assignedEvaluation.attempt,
765
+ assignedEvaluation,
766
+ packageRoot,
767
+ { transactionTaskId: lockedTaskId },
768
+ );
769
+ return {
770
+ inputs,
771
+ evaluation: { ...assignedEvaluation, artifactRef: written.path, artifactFingerprint: written.fingerprint },
772
+ converged: false,
773
+ attempts: assignedAttempt,
774
+ };
775
+ });
776
+
777
+ if (captured.converged) {
778
+ const evaluationStatus = captured.evaluation?.status ?? (policy.mode === "gate" ? "BLOCKED" : "NOT_OBSERVED");
779
+ return {
780
+ status: evaluationStatus === "PASS" ? "CONVERGED" : evaluationStatus,
781
+ mode: policy.mode,
782
+ evaluation: captured.evaluation,
783
+ check: null,
784
+ attempts: captured.attempts,
785
+ };
786
+ }
787
+
788
+ return persistEvaluationAndCheck({
789
+ target,
790
+ packageRoot,
791
+ taskId: captured.inputs.state.taskId,
792
+ inputs: captured.inputs,
793
+ persisted: captured.evaluation,
794
+ authorityContext,
795
+ runtimeContext,
796
+ });
797
+ }
798
+
799
+ async function optimizationScopeStatus(target, packageRoot, taskId) {
800
+ try {
801
+ const task = await findTaskById(target, taskId, packageRoot);
802
+ const claims = task?.effectiveWriteClaims ?? task?.writeClaims ?? [];
803
+ if (!Array.isArray(claims) || claims.length === 0) return { status: "UNAVAILABLE", reasonCodes: ["SCOPE_UNAVAILABLE"] };
804
+ const changed = await currentChangedPaths(target);
805
+ if (changed === null) return { status: "UNAVAILABLE", reasonCodes: ["REPOSITORY_STATUS_UNAVAILABLE"] };
806
+ try {
807
+ assertClaimsCoverChangedPaths(claims, changed);
808
+ return { status: "SAFE", reasonCodes: [] };
809
+ } catch {
810
+ return { status: "UNSAFE", reasonCodes: ["SCOPE_DRIFT"] };
811
+ }
812
+ } catch {
813
+ return { status: "UNAVAILABLE", reasonCodes: ["SCOPE_UNAVAILABLE"] };
814
+ }
815
+ }
816
+
817
+ function optimizationProjection(policy, evaluations, current, scopeStatus = { status: "UNAVAILABLE", reasonCodes: ["SCOPE_UNAVAILABLE"] }) {
818
+ const optimization = {
819
+ mode: policy.optimization.mode,
820
+ maxExtraEvaluations: policy.optimization.maxExtraEvaluations,
821
+ minGainPoints: policy.optimization.minGainPoints,
822
+ attempts: 0,
823
+ gain: null,
824
+ converged: false,
825
+ scope: scopeStatus.status,
826
+ reasonCodes: [...(scopeStatus.reasonCodes ?? [])],
827
+ next: null,
828
+ };
829
+ if (policy.optimization.mode !== "bounded") return optimization;
830
+ const currentValue = current?.value ?? current;
831
+ const cycle = currentValue?.verificationCycle;
832
+ const sameCycle = (evaluations ?? []).filter((item) => item.value.verificationCycle === cycle);
833
+ optimization.attempts = sameCycle.length;
834
+ if (sameCycle.length < 2) {
835
+ if (currentValue?.status === "PASS" && sameCycle.length < 1 + policy.optimization.maxExtraEvaluations && scopeStatus.status === "SAFE") {
836
+ optimization.next = "OPTIONAL_STRUCTURAL_QUALITY_EVALUATION";
837
+ }
838
+ return optimization;
839
+ }
840
+ const previous = sameCycle.at(-2)?.value;
841
+ const latest = sameCycle.at(-1)?.value;
842
+ if (previous?.comparison?.comparable !== true || latest?.comparison?.comparable !== true
843
+ || !Number.isInteger(previous.currentSignal) || !Number.isInteger(latest.currentSignal)) {
844
+ optimization.reasonCodes = [...new Set([...optimization.reasonCodes, "COMPARISON_UNAVAILABLE"])].sort();
845
+ return optimization;
846
+ }
847
+ optimization.gain = latest.currentSignal - previous.currentSignal;
848
+ optimization.converged = optimization.gain < policy.optimization.minGainPoints;
849
+ if (!optimization.converged
850
+ && sameCycle.length < 1 + policy.optimization.maxExtraEvaluations
851
+ && scopeStatus.status === "SAFE"
852
+ && latest.status === "PASS") {
853
+ optimization.next = "OPTIONAL_STRUCTURAL_QUALITY_EVALUATION";
854
+ }
855
+ return optimization;
856
+ }
857
+
858
+ function projectionFromArtifacts(policy, baseline, current, state, evaluations = [], optimizationScope = null, freshness = null, baselineFreshness = null) {
859
+ if (!policy || policy.mode === "off") return emptyProjection(null);
860
+ const projection = emptyProjection(policy);
861
+ projection.persistedStatus = current?.value?.status ?? null;
862
+ projection.freshness = current ? (freshness?.valid === false ? "STALE" : "CURRENT") : "MISSING";
863
+ projection.baselineFreshness = baseline ? (baselineFreshness?.valid === false ? "STALE" : "CURRENT") : "MISSING";
864
+ if (baseline) {
865
+ projection.baseline = {
866
+ status: baseline.value.status === "PASS" ? "OBSERVED" : baseline.value.status,
867
+ qualitySignal: baseline.value.snapshot?.qualitySignal ?? null,
868
+ artifactRef: baseline.path,
869
+ fingerprint: baseline.fingerprint,
870
+ };
871
+ }
872
+ if (current) {
873
+ const currentStatus = freshness?.valid === false ? statusForComparison(policy, { status: "BLOCKED" }) : current.value.status;
874
+ projection.current = {
875
+ status: currentStatus,
876
+ verificationCycle: current.value.verificationCycle,
877
+ attempt: current.value.attempt,
878
+ qualitySignal: current.value.snapshot?.qualitySignal ?? null,
879
+ delta: current.value.comparison?.qualityDelta ?? null,
880
+ bottleneck: current.value.snapshot?.bottleneck ?? null,
881
+ artifactRef: current.path,
882
+ };
883
+ projection.comparable = current.value.comparison?.comparable ?? null;
884
+ projection.reasonCodes = [...new Set([...(current.value.reasonCodes ?? []), ...(freshness?.reasonCodes ?? [])])].sort();
885
+ } else if (!baseline) {
886
+ projection.reasonCodes = [E_STRUCTURAL_QUALITY_BASELINE_MISSING];
887
+ }
888
+ projection.optimization = optimizationProjection(policy, evaluations, current, optimizationScope ?? undefined);
889
+ if (freshness?.valid === false) {
890
+ projection.next = policy.mode === "gate" ? "VERIFY_STRUCTURAL_QUALITY" : "OPTIONAL_VERIFY_STRUCTURAL_QUALITY";
891
+ } else if (baselineFreshness?.valid === false) {
892
+ projection.next = policy.mode === "gate" ? "RESOLVE_STRUCTURAL_QUALITY_BLOCKER" : null;
893
+ } else if (!baseline) {
894
+ projection.next = policy.mode === "gate"
895
+ ? "CAPTURE_STRUCTURAL_QUALITY_BASELINE"
896
+ : (policy.mode === "observe" ? "OPTIONAL_CAPTURE_STRUCTURAL_QUALITY_BASELINE" : null);
897
+ } else if (!current || current.value.verificationCycle !== (state?.verificationCycle ?? 1)) {
898
+ projection.next = policy.mode === "gate"
899
+ ? "VERIFY_STRUCTURAL_QUALITY"
900
+ : (policy.mode === "observe" ? "OPTIONAL_VERIFY_STRUCTURAL_QUALITY" : null);
901
+ } else if (current.value.status === "FAIL") {
902
+ projection.next = "DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION";
903
+ } else if (current.value.status === "BLOCKED") {
904
+ projection.next = "RESOLVE_STRUCTURAL_QUALITY_BLOCKER";
905
+ } else {
906
+ projection.next = null;
907
+ }
908
+ return projection;
909
+ }
910
+
911
+ export async function projectStructuralQualityStatus({ target, packageRoot = getPackageRoot(), taskId, runtimeContext } = {}) {
912
+ const config = await loadOptionalConfig(target, packageRoot);
913
+ const policy = configuredPolicy(config);
914
+ if (!policy || policy.mode === "off") return emptyProjection(null);
915
+ let state = null;
916
+ try { state = await readWorkState(target, { packageRoot, taskId }); } catch { /* status remains read-only and structured */ }
917
+ let baseline = null;
918
+ let current = null;
919
+ let evaluations = [];
920
+ let inputs = null;
921
+ try {
922
+ inputs = await resolveStructuralQualityContext({ target, packageRoot, taskId, runtimeContext });
923
+ } catch (error) {
924
+ return { ...emptyProjection(policy, [error.code ?? E_STRUCTURAL_QUALITY_EVIDENCE_STALE]), baseline: { status: "INVALID", qualitySignal: null, artifactRef: null, fingerprint: null } };
925
+ }
926
+ try { baseline = await readStructuralQualityBaseline(target, taskId, packageRoot); } catch (error) {
927
+ return { ...emptyProjection(policy, [error.code ?? E_STRUCTURAL_QUALITY_EVIDENCE_STALE]), baseline: { status: "INVALID", qualitySignal: null, artifactRef: null, fingerprint: null } };
928
+ }
929
+ try {
930
+ evaluations = await listStructuralQualityEvaluations(target, taskId, packageRoot);
931
+ current = evaluations.at(-1) ?? null;
932
+ } catch (error) {
933
+ return { ...projectionFromArtifacts(policy, baseline, null, state), reasonCodes: [error.code ?? E_STRUCTURAL_QUALITY_EVIDENCE_STALE] };
934
+ }
935
+ const baselineFreshness = baseline
936
+ ? await validateStructuralQualityEvaluationFreshness({ target, packageRoot, taskId, artifact: baseline, inputs, runtimeContext })
937
+ : null;
938
+ const currentFreshness = current
939
+ ? await validateStructuralQualityEvaluationFreshness({ target, packageRoot, taskId, artifact: current, inputs, runtimeContext })
940
+ : null;
941
+ const optimizationScope = policy.optimization.mode === "bounded" && current?.value?.status === "PASS"
942
+ ? await optimizationScopeStatus(target, packageRoot, taskId)
943
+ : null;
944
+ return projectionFromArtifacts(policy, baseline, current, state, evaluations, optimizationScope, currentFreshness, baselineFreshness);
945
+ }
946
+
947
+ export async function validateStructuralQualityCheckProvenance(check, { target, packageRoot = getPackageRoot(), taskId, state, contract, route, config, runtimeContext } = {}) {
948
+ if (check?.kind !== "structural-quality") return [];
949
+ const details = check.details ?? {};
950
+ const artifactRef = details.artifactRef;
951
+ if (typeof artifactRef !== "string" || !artifactRef) return [qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "Structural-quality check is missing artifactRef")];
952
+ try {
953
+ const artifact = await readJsonArtifact(target, artifactRef, "structural-quality", packageRoot);
954
+ const policy = configuredPolicy(config ?? await loadOptionalConfig(target, packageRoot));
955
+ if (!policy || policy.mode === "off") return [qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "Structural-quality check is configured off")];
956
+ const active = await resolveStructuralQualityContext({
957
+ target,
958
+ packageRoot,
959
+ taskId,
960
+ runtimeContext,
961
+ });
962
+ const freshness = await validateStructuralQualityEvaluationFreshness({
963
+ target,
964
+ packageRoot,
965
+ taskId,
966
+ artifact,
967
+ inputs: { ...active, contract: contract ?? active.contract, route: route ?? active.route, policy },
968
+ runtimeContext,
969
+ check,
970
+ });
971
+ return freshness.errors;
972
+ } catch (error) {
973
+ return [error.code ? error : qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, error.message)];
974
+ }
975
+ }
976
+
977
+ function validateBindingErrors(value, expected) {
978
+ const errors = [];
979
+ const bindings = value.bindings ?? {};
980
+ if (expected.taskId !== undefined && value.taskId !== expected.taskId) {
981
+ errors.push(qualityError(E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, "Structural-quality task ID does not match the active task"));
982
+ }
983
+ for (const key of ["contractFingerprint", "routeFingerprint", "policyFingerprint", "scopeFingerprint"]) {
984
+ if (expected[key] !== undefined && bindings[key] !== expected[key]) errors.push(qualityError(E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, `Structural-quality ${key} is stale`));
985
+ }
986
+ if (expected.providerId && value.provider?.id !== expected.providerId) errors.push(qualityError(E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, "Structural-quality provider binding is stale"));
987
+ if (expected.baselineFingerprint !== undefined && value.role === "EVALUATION" && bindings.baselineFingerprint !== expected.baselineFingerprint) errors.push(qualityError(E_STRUCTURAL_QUALITY_BASELINE_BINDING_MISMATCH, "Structural-quality baseline binding is stale"));
988
+ if (expected.verificationCycle !== undefined && value.role === "EVALUATION" && value.verificationCycle !== expected.verificationCycle) errors.push(qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "Structural-quality evaluation is from a stale verification cycle"));
989
+ return errors;
990
+ }
991
+
992
+ function providerCompatibilityErrors(baselineProvider, currentProvider) {
993
+ const errors = [];
994
+ for (const provider of [baselineProvider, currentProvider]) {
995
+ if (provider?.id !== "sentrux" || provider.version === undefined || provider.version === null) continue;
996
+ if (!structuralQualityProviderCompatibility(provider).supported) {
997
+ errors.push(qualityError("E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED", "Structural-quality provider version is not in the verified compatibility table"));
998
+ }
999
+ }
1000
+ if (!baselineProvider || !currentProvider) return errors;
1001
+ if (baselineProvider.measurementModel && currentProvider.measurementModel
1002
+ && baselineProvider.measurementModel !== currentProvider.measurementModel) {
1003
+ errors.push(qualityError(E_STRUCTURAL_QUALITY_MEASUREMENT_MODEL_MISMATCH, "Structural-quality measurement model differs from the captured baseline"));
1004
+ }
1005
+ if (baselineProvider.compatibilityKey && currentProvider.compatibilityKey
1006
+ && baselineProvider.compatibilityKey !== currentProvider.compatibilityKey) {
1007
+ errors.push(qualityError(E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE, "Structural-quality compatibility key differs from the captured baseline"));
1008
+ }
1009
+ if (baselineProvider.version !== currentProvider.version) {
1010
+ const sameCompatibility = baselineProvider.compatibilityKey
1011
+ && currentProvider.compatibilityKey
1012
+ && baselineProvider.compatibilityKey === currentProvider.compatibilityKey;
1013
+ if (!sameCompatibility) {
1014
+ errors.push(qualityError(E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE, "Structural-quality provider version differs from the captured baseline"));
1015
+ }
1016
+ }
1017
+ return errors;
1018
+ }
1019
+
1020
+ /**
1021
+ * Validates persisted evidence against the active task without invoking a
1022
+ * provider observation. This is the only freshness decision used by status,
1023
+ * provenance, and projection recovery.
1024
+ */
1025
+ export async function validateStructuralQualityEvaluationFreshness({
1026
+ target,
1027
+ packageRoot = getPackageRoot(),
1028
+ taskId,
1029
+ artifact,
1030
+ inputs = null,
1031
+ runtimeContext,
1032
+ check = null,
1033
+ } = {}) {
1034
+ const value = artifact?.value ?? artifact;
1035
+ const errors = [];
1036
+ if (!value || typeof value !== "object") {
1037
+ return { valid: false, errors: [qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "Structural-quality evidence is missing")], reasonCodes: [E_STRUCTURAL_QUALITY_EVIDENCE_STALE] };
1038
+ }
1039
+ try {
1040
+ validateStructuralQualityArtifact(value, artifact?.path ?? "structural-quality artifact");
1041
+ } catch (error) {
1042
+ errors.push(error.code ? error : qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, error.message));
1043
+ }
1044
+
1045
+ let active = inputs;
1046
+ if (!active) {
1047
+ try {
1048
+ active = await resolveStructuralQualityContext({ target, packageRoot, taskId, runtimeContext });
1049
+ } catch (error) {
1050
+ errors.push(error.code ? error : qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, error.message));
1051
+ }
1052
+ }
1053
+ if (active) {
1054
+ errors.push(...validateBindingErrors(value, {
1055
+ taskId,
1056
+ contractFingerprint: active.contract.fingerprint,
1057
+ routeFingerprint: active.route.fingerprint,
1058
+ policyFingerprint: active.policy ? structuralQualityPolicyFingerprint(active.policy) : undefined,
1059
+ scopeFingerprint: active.scopeFingerprint,
1060
+ providerId: active.policy?.provider,
1061
+ verificationCycle: value.role === "EVALUATION" ? active.state.verificationCycle : undefined,
1062
+ }));
1063
+ }
1064
+
1065
+ let baseline = null;
1066
+ if (value.role === "EVALUATION") {
1067
+ try {
1068
+ baseline = await readStructuralQualityBaseline(target, taskId, packageRoot);
1069
+ if (!baseline) errors.push(qualityError(E_STRUCTURAL_QUALITY_BASELINE_MISSING, "Structural-quality evaluation has no captured baseline"));
1070
+ } catch (error) {
1071
+ errors.push(error.code ? error : qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, error.message));
1072
+ }
1073
+ if (baseline) {
1074
+ try {
1075
+ validateStructuralQualityArtifact(baseline.value, baseline.path);
1076
+ } catch (error) {
1077
+ errors.push(error.code ? error : qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, error.message));
1078
+ }
1079
+ if (active) {
1080
+ errors.push(...validateBindingErrors(baseline.value, {
1081
+ taskId,
1082
+ contractFingerprint: active.contract.fingerprint,
1083
+ routeFingerprint: active.route.fingerprint,
1084
+ policyFingerprint: active.policy ? structuralQualityPolicyFingerprint(active.policy) : undefined,
1085
+ scopeFingerprint: active.scopeFingerprint,
1086
+ providerId: active.policy?.provider,
1087
+ }));
1088
+ }
1089
+ errors.push(...validateBindingErrors(value, { baselineFingerprint: baseline.fingerprint }));
1090
+ errors.push(...providerCompatibilityErrors(baseline.value.provider, value.provider));
1091
+ }
1092
+ } else {
1093
+ errors.push(...providerCompatibilityErrors(value.provider, value.provider));
1094
+ }
1095
+
1096
+ if (["BASELINE", "EVALUATION"].includes(value.role) && value.bindings?.sourceMaterialFingerprint) {
1097
+ try {
1098
+ const currentSourceFingerprint = await computeMaterialSourceFingerprint(target);
1099
+ if (currentSourceFingerprint !== value.bindings.sourceMaterialFingerprint) {
1100
+ errors.push(qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, `Structural-quality ${value.role.toLowerCase()} source material has changed since capture`));
1101
+ }
1102
+ } catch (error) {
1103
+ errors.push(error.code ? error : qualityError(E_STRUCTURAL_QUALITY_SOURCE_FINGERPRINT_UNAVAILABLE, error.message));
1104
+ }
1105
+ }
1106
+ if (check) {
1107
+ if (!active?.policy) errors.push(qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "Structural-quality policy is unavailable"));
1108
+ else if (check.status !== checkProjection(active.policy, value).status) {
1109
+ errors.push(qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "Structural-quality check status does not match its persisted evaluation"));
1110
+ }
1111
+ if (artifact?.fingerprint && check.details?.artifactFingerprint !== artifact.fingerprint) {
1112
+ errors.push(qualityError(E_STRUCTURAL_QUALITY_EVIDENCE_STALE, "Structural-quality check fingerprint does not match its artifact"));
1113
+ }
1114
+ }
1115
+ return {
1116
+ valid: errors.length === 0,
1117
+ errors,
1118
+ reasonCodes: [...new Set(errors.map((error) => error.code ?? E_STRUCTURAL_QUALITY_EVIDENCE_STALE))].sort(),
1119
+ };
1120
+ }
1121
+
1122
+ export async function assertStructuralQualityExecutionReady({ target, packageRoot = getPackageRoot(), taskId, runtimeContext } = {}) {
1123
+ const config = await loadOptionalConfig(target, packageRoot);
1124
+ const policy = configuredPolicy(config);
1125
+ if (!policy || policy.mode !== "gate") return { required: false, baseline: null };
1126
+ const baseline = await readStructuralQualityBaseline(target, taskId, packageRoot);
1127
+ if (!baseline) throw qualityError(E_STRUCTURAL_QUALITY_BASELINE_MISSING, "Gate mode requires a captured structural-quality baseline before EXECUTING", [taskArtifactPath(taskId, "structuralQuality")]);
1128
+ const inputs = await taskInputs(target, packageRoot, taskId, { runtimeContext });
1129
+ const errors = validateBindingErrors(baseline.value, {
1130
+ contractFingerprint: inputs.contract.fingerprint,
1131
+ routeFingerprint: inputs.route.fingerprint,
1132
+ policyFingerprint: structuralQualityPolicyFingerprint(policy),
1133
+ scopeFingerprint: inputs.scopeFingerprint,
1134
+ providerId: policy.provider,
1135
+ });
1136
+ if (errors.length > 0) throw errors[0];
1137
+ return { required: true, baseline };
1138
+ }