@cassiomc1/forgeloop 1.8.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/project-loop.mdc +6 -3
- package/.github/copilot-instructions.md +5 -0
- package/AGENTS.md +6 -0
- package/AGENT_COMPATIBILITY.md +15 -0
- package/CLAUDE.md +6 -0
- package/DELEGATION_PROTOCOL.md +6 -0
- package/DOCS_INDEX.md +9 -2
- package/ENG/accessibility-eng.md +12 -2
- package/ENG/design-code-eng.md +22 -1
- package/LOOP_ENGINEERING.md +41 -0
- package/LOOP_SYSTEM_DESIGN.md +33 -0
- package/ORCHESTRATOR_INTEGRATION.md +9 -0
- package/PROTOCOL_INTEGRATION.md +57 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +51 -0
- package/TERMINOLOGY.md +12 -0
- package/THREAT_MODEL.md +48 -0
- package/completions/_forgeloop +5 -1
- package/completions/forgeloop.bash +9 -1
- package/completions/forgeloop.fish +27 -1
- package/docs/ADVISORY_CONTEXT.md +174 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +33 -2
- package/docs/ARTIFACT_REFERENCE.md +142 -0
- package/docs/CLI_REFERENCE.md +124 -1
- package/docs/CROSS_HARNESS_CONTINUITY.md +85 -0
- package/docs/DOCUMENTATION_GUIDE.md +7 -0
- package/docs/GETTING_STARTED.md +22 -0
- package/docs/KNOWLEDGE_SOURCES.md +171 -0
- package/docs/MCP.md +17 -1
- package/docs/RECIPES.md +111 -0
- package/docs/RELEASE_CHECKLIST.md +14 -0
- package/docs/STRUCTURAL_QUALITY.md +350 -0
- package/docs/TROUBLESHOOTING.md +161 -2
- package/docs/UNIVERSAL_INTEGRATION.md +60 -0
- package/package.json +4 -1
- package/schemas/config.schema.json +46 -0
- package/schemas/handoff-envelope.schema.json +1 -0
- package/schemas/preflight.schema.json +2 -1
- package/schemas/structural-quality.schema.json +175 -0
- package/scripts/check-changelog-freshness.mjs +27 -3
- package/scripts/generate-agent-protocol-summary.mjs +18 -0
- package/src/cli.js +24 -0
- package/src/commands/handoff-accept.js +36 -0
- package/src/commands/handoff-list.js +28 -2
- package/src/commands/handoff-show.js +27 -2
- package/src/commands/quality-baseline.js +28 -0
- package/src/commands/quality-status.js +34 -0
- package/src/commands/quality-verify.js +30 -0
- package/src/commands/reconcile-continuity.js +4 -0
- package/src/core/advisory-context/constants.js +74 -0
- package/src/core/advisory-context/provider.js +287 -0
- package/src/core/advisory-context/service.js +140 -0
- package/src/core/artifact-registry.js +12 -0
- package/src/core/audit.js +38 -0
- package/src/core/bundles.js +134 -1
- package/src/core/cli-command-definitions.js +62 -0
- package/src/core/command-executors.js +28 -0
- package/src/core/command-input.js +23 -1
- package/src/core/completion-artifacts.js +2 -0
- package/src/core/completion.js +42 -0
- package/src/core/config.js +3 -0
- package/src/core/continuity-lint.js +89 -0
- package/src/core/continuity-reconciliation.js +16 -0
- package/src/core/continuity.js +10 -11
- package/src/core/error-codes.js +186 -0
- package/src/core/events.js +32 -0
- package/src/core/execution-profile-context.js +15 -1
- package/src/core/filesystem.js +34 -3
- package/src/core/handoff-acceptance.js +277 -0
- package/src/core/handoff.js +41 -8
- package/src/core/inspect.js +64 -0
- package/src/core/integration-invocation-policy.js +34 -2
- package/src/core/integration-resources.js +38 -1
- package/src/core/next-action-model.js +11 -1
- package/src/core/next-action-phases.js +84 -5
- package/src/core/phase.js +9 -1
- package/src/core/portable-context.js +103 -0
- package/src/core/preflight.js +33 -0
- package/src/core/protocol-info.js +33 -2
- package/src/core/runtime-context.js +58 -0
- package/src/core/schema-validation.js +1 -0
- package/src/core/structural-quality/artifacts.js +329 -0
- package/src/core/structural-quality/constants.js +67 -0
- package/src/core/structural-quality/policy.js +227 -0
- package/src/core/structural-quality/provider.js +287 -0
- package/src/core/structural-quality/sentrux-mcp.js +477 -0
- package/src/core/structural-quality/service.js +1138 -0
- package/src/core/structural-quality/source-fingerprint.js +112 -0
- package/src/core/structural-quality/status.js +3 -0
- package/src/core/task-paths.js +24 -0
- package/src/core/templates.js +1 -0
- package/src/integration.d.ts +141 -0
- package/src/integration.js +36 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ARTIFACT_PATHS, readJsonArtifact } from "./artifacts.js";
|
|
2
|
-
import { taskArtifactPath } from "./task-paths.js";
|
|
2
|
+
import { taskArtifactPath, taskStructuralQualityDirectory } from "./task-paths.js";
|
|
3
3
|
import { completionIdentityErrors, evaluateCompletion } from "./completion.js";
|
|
4
4
|
import { readContract } from "./contract.js";
|
|
5
5
|
import { evaluatePreflight, validatePersistedPreflight } from "./preflight.js";
|
|
@@ -20,6 +20,7 @@ import { listActions } from "./actions.js";
|
|
|
20
20
|
import { listApprovals } from "./approvals.js";
|
|
21
21
|
import { loadPolicyIdentity } from "./policy-engine.js";
|
|
22
22
|
import { evaluateContinuityNextAction } from "./next-action-continuity.js";
|
|
23
|
+
import { projectStructuralQualityStatus } from "./structural-quality/service.js";
|
|
23
24
|
|
|
24
25
|
export const PHASES_REQUIRING_EXECUTION_CHRONOLOGY = new Set([
|
|
25
26
|
"EXECUTING",
|
|
@@ -34,6 +35,24 @@ export function phaseRequiresExecutionChronology(phase) {
|
|
|
34
35
|
return PHASES_REQUIRING_EXECUTION_CHRONOLOGY.has(phase);
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
function structuralQualityOptionalActions(quality, taskId) {
|
|
39
|
+
if (quality?.mode !== "observe") return [];
|
|
40
|
+
if (quality.baseline?.status !== "OBSERVED") {
|
|
41
|
+
return [{
|
|
42
|
+
action: NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE,
|
|
43
|
+
command: `forgeloop quality-baseline --task ${taskId} --json`,
|
|
44
|
+
}];
|
|
45
|
+
}
|
|
46
|
+
if (quality.freshness === "STALE" || quality.current?.verificationCycle === null
|
|
47
|
+
|| quality.current?.verificationCycle === undefined) {
|
|
48
|
+
return [{
|
|
49
|
+
action: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
|
|
50
|
+
command: `forgeloop quality-verify --task ${taskId} --json`,
|
|
51
|
+
}];
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
export async function resolveNextActionPhase({
|
|
38
57
|
target,
|
|
39
58
|
packageRoot,
|
|
@@ -434,7 +453,7 @@ export async function resolveNextActionPhase({
|
|
|
434
453
|
let executionPrerequisites = null;
|
|
435
454
|
if (phaseNeedsChronology) {
|
|
436
455
|
try {
|
|
437
|
-
executionPrerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId });
|
|
456
|
+
executionPrerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId, runtimeContext });
|
|
438
457
|
} catch (error) {
|
|
439
458
|
return result({
|
|
440
459
|
...context,
|
|
@@ -506,7 +525,17 @@ export async function resolveNextActionPhase({
|
|
|
506
525
|
return decision(context, NEXT_ACTIONS.PLAN, artifactError("PHASE_DESIGNING", "Required gates are ready for planning"));
|
|
507
526
|
}
|
|
508
527
|
if (state.phase === "PLANNED") {
|
|
509
|
-
const
|
|
528
|
+
const quality = await projectStructuralQualityStatus({ target, packageRoot, taskId: explicitTaskId ?? state.taskId, runtimeContext });
|
|
529
|
+
if (quality.mode === "gate" && quality.baseline.status !== "OBSERVED") {
|
|
530
|
+
return result({
|
|
531
|
+
...context,
|
|
532
|
+
nextAction: NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE,
|
|
533
|
+
commands: [commandFor(NEXT_ACTIONS.CAPTURE_STRUCTURAL_QUALITY_BASELINE).replace("<id>", state.taskId)],
|
|
534
|
+
reasons: [artifactError("E_STRUCTURAL_QUALITY_BASELINE_MISSING", "Gate mode requires a structural-quality baseline before execution", [quality.baseline.artifactRef ?? taskArtifactPath(state.taskId, "structuralQuality")])],
|
|
535
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "structuralQuality")],
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
const prerequisites = await evaluateStartExecutionPrerequisites({ target, state, packageRoot, taskId: explicitTaskId, runtimeContext });
|
|
510
539
|
if (prerequisites.errors.length > 0) {
|
|
511
540
|
const preflightOnly = prerequisites.errors.every((error) => error.code.startsWith("E_PREFLIGHT_")
|
|
512
541
|
|| (error.code === "E_PHASE_CHRONOLOGY_INVALID"
|
|
@@ -530,7 +559,14 @@ export async function resolveNextActionPhase({
|
|
|
530
559
|
missingArtifacts: preflightArtifact.missingArtifacts,
|
|
531
560
|
});
|
|
532
561
|
}
|
|
533
|
-
return decision(
|
|
562
|
+
return decision(
|
|
563
|
+
context,
|
|
564
|
+
NEXT_ACTIONS.START_EXECUTION,
|
|
565
|
+
artifactError("PHASE_PLANNED", "The persisted preflight is READY"),
|
|
566
|
+
[],
|
|
567
|
+
[],
|
|
568
|
+
structuralQualityOptionalActions(quality, state.taskId),
|
|
569
|
+
);
|
|
534
570
|
}
|
|
535
571
|
if (state.phase === "EXECUTING") {
|
|
536
572
|
const continuityAction = await evaluateContinuityNextAction({ target, packageRoot, context });
|
|
@@ -538,6 +574,41 @@ export async function resolveNextActionPhase({
|
|
|
538
574
|
return decision(context, NEXT_ACTIONS.ENTER_VERIFYING, artifactError("PHASE_EXECUTING", "Execution is complete enough to enter verification"));
|
|
539
575
|
}
|
|
540
576
|
if (state.phase === "VERIFYING") {
|
|
577
|
+
const quality = await projectStructuralQualityStatus({ target, packageRoot, taskId: explicitTaskId ?? state.taskId, runtimeContext });
|
|
578
|
+
if (quality.mode === "gate") {
|
|
579
|
+
if (quality.baseline.status !== "OBSERVED") {
|
|
580
|
+
return result({
|
|
581
|
+
...context,
|
|
582
|
+
nextAction: NEXT_ACTIONS.RESOLVE_STRUCTURAL_QUALITY_BLOCKER,
|
|
583
|
+
reasons: [artifactError("E_STRUCTURAL_QUALITY_BASELINE_MISSING", "Structural-quality gate cannot verify without its immutable baseline", [taskArtifactPath(state.taskId, "structuralQuality")])],
|
|
584
|
+
requiredArtifacts: [taskArtifactPath(state.taskId, "structuralQuality")],
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (!quality.current.artifactRef || quality.current.verificationCycle !== (state.verificationCycle ?? 1)) {
|
|
588
|
+
return result({
|
|
589
|
+
...context,
|
|
590
|
+
nextAction: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
|
|
591
|
+
commands: [commandFor(NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY).replace("<id>", state.taskId)],
|
|
592
|
+
reasons: [artifactError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "The current verification cycle has no structural-quality evaluation", [taskStructuralQualityDirectory(state.taskId)])],
|
|
593
|
+
requiredArtifacts: [taskStructuralQualityDirectory(state.taskId)],
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (quality.freshness === "STALE") {
|
|
597
|
+
return result({
|
|
598
|
+
...context,
|
|
599
|
+
nextAction: NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY,
|
|
600
|
+
commands: [commandFor(NEXT_ACTIONS.VERIFY_STRUCTURAL_QUALITY).replace("<id>", state.taskId)],
|
|
601
|
+
reasons: [artifactError("E_STRUCTURAL_QUALITY_EVIDENCE_STALE", "Structural-quality evidence is stale; fresh verification is required before review", [quality.current.artifactRef])],
|
|
602
|
+
requiredArtifacts: [taskStructuralQualityDirectory(state.taskId)],
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
if (quality.current.status === "FAIL") {
|
|
606
|
+
return decision(context, NEXT_ACTIONS.DIAGNOSE_STRUCTURAL_QUALITY_REGRESSION, artifactError("E_STRUCTURAL_QUALITY_REGRESSION", "Structural-quality verification detected a regression", [quality.current.artifactRef]));
|
|
607
|
+
}
|
|
608
|
+
if (quality.current.status === "BLOCKED") {
|
|
609
|
+
return decision(context, NEXT_ACTIONS.RESOLVE_STRUCTURAL_QUALITY_BLOCKER, artifactError("E_STRUCTURAL_QUALITY_EVALUATION_INCOMPARABLE", "Structural-quality verification is blocked", [quality.current.artifactRef]));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
541
612
|
const invalidChecks = checkListReasons(state);
|
|
542
613
|
if (invalidChecks.length > 0) {
|
|
543
614
|
return result({
|
|
@@ -655,7 +726,14 @@ export async function resolveNextActionPhase({
|
|
|
655
726
|
});
|
|
656
727
|
}
|
|
657
728
|
if (readiness.ready) {
|
|
658
|
-
return decision(
|
|
729
|
+
return decision(
|
|
730
|
+
context,
|
|
731
|
+
NEXT_ACTIONS.ENTER_REVIEWING,
|
|
732
|
+
artifactError("EVIDENCE_COVERED", "All required observed verification evidence is covered"),
|
|
733
|
+
[],
|
|
734
|
+
[],
|
|
735
|
+
structuralQualityOptionalActions(quality, state.taskId),
|
|
736
|
+
);
|
|
659
737
|
}
|
|
660
738
|
const uncovered = [...readiness.invalid, ...readiness.partial, ...readiness.missing];
|
|
661
739
|
return result({
|
|
@@ -674,6 +752,7 @@ export async function resolveNextActionPhase({
|
|
|
674
752
|
[stateRel],
|
|
675
753
|
)),
|
|
676
754
|
requiredArtifacts: requiredArtifacts,
|
|
755
|
+
optionalActions: structuralQualityOptionalActions(quality, state.taskId),
|
|
677
756
|
});
|
|
678
757
|
}
|
|
679
758
|
if (state.phase === "DIAGNOSING") {
|
package/src/core/phase.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
buildInformationGainProjection,
|
|
27
27
|
evaluateStructuredDiagnosticStall,
|
|
28
28
|
} from "./information-gain-projection.js";
|
|
29
|
+
import { assertStructuralQualityExecutionReady } from "./structural-quality/service.js";
|
|
29
30
|
|
|
30
31
|
function phaseError(code, message, artifacts = []) {
|
|
31
32
|
const error = new Error(message);
|
|
@@ -134,9 +135,16 @@ async function assertPhasePrerequisites(target, state, toPhase, packageRoot, aut
|
|
|
134
135
|
throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${routeRel}: ${error.message}`, [routeRel]);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
138
|
+
if (toPhase === "EXECUTING" && scopedTaskId) {
|
|
139
|
+
try {
|
|
140
|
+
await assertStructuralQualityExecutionReady({ target, packageRoot, taskId: scopedTaskId, runtimeContext });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw phaseError(error.code, error.message, error.artifacts);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
137
145
|
if (hasExecutionStarted(toPhase)) {
|
|
138
146
|
try {
|
|
139
|
-
await assertExecutionPrerequisites({ target, state, packageRoot, ...options, taskId: scopedTaskId });
|
|
147
|
+
await assertExecutionPrerequisites({ target, state, packageRoot, ...options, taskId: scopedTaskId, runtimeContext });
|
|
140
148
|
} catch (error) {
|
|
141
149
|
throw phaseError(error.code, error.message, error.artifacts);
|
|
142
150
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { assertJsonLimits } from "./json-safety.js";
|
|
2
|
+
import { assertSecretFree } from "./receipt.js";
|
|
3
|
+
import { E_PORTABLE_CONTEXT_INVALID } from "./error-codes.js";
|
|
4
|
+
|
|
5
|
+
export class PortableContextError extends Error {
|
|
6
|
+
constructor(message, { code = E_PORTABLE_CONTEXT_INVALID, cause } = {}) {
|
|
7
|
+
super(message, cause !== undefined ? { cause } : undefined);
|
|
8
|
+
this.name = "PortableContextError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PORTABLE_SECRET_PATTERNS = [
|
|
14
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
|
|
15
|
+
/(?:^|\s)(?:sk|ghp|glpat|xox[baprs])-[-_a-z0-9]{8,}/i,
|
|
16
|
+
/(?:AKIA|ASIA)[A-Z0-9]{12,}/,
|
|
17
|
+
/(?:bearer\s+[-_a-z0-9\.]{4,})/i,
|
|
18
|
+
/(?:authorization:\s*bearer)/i,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function containsSecretPattern(str) {
|
|
22
|
+
return PORTABLE_SECRET_PATTERNS.some((pattern) => pattern.test(str));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function checkObjectForSecrets(value, location = "$") {
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
if (containsSecretPattern(value)) {
|
|
28
|
+
throw new Error(`${location}: secret-like value is not allowed`);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!value || typeof value !== "object") return;
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
value.forEach((item, index) => checkObjectForSecrets(item, `${location}[${index}]`));
|
|
35
|
+
} else {
|
|
36
|
+
for (const [key, child] of Object.entries(value)) {
|
|
37
|
+
checkObjectForSecrets(child, `${location}.${key}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalizePortableText(
|
|
43
|
+
value,
|
|
44
|
+
{
|
|
45
|
+
label = "portable text",
|
|
46
|
+
maxLength,
|
|
47
|
+
optional = false,
|
|
48
|
+
} = {},
|
|
49
|
+
) {
|
|
50
|
+
if (value === undefined || value === null) {
|
|
51
|
+
if (optional) return null;
|
|
52
|
+
throw new PortableContextError(`${label} is required`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
56
|
+
throw new PortableContextError(`${label} must be a non-empty string`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof maxLength === "number" && value.length > maxLength) {
|
|
60
|
+
throw new PortableContextError(`${label} exceeds the ${maxLength}-character limit`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (/\p{Cc}/u.test(value)) {
|
|
64
|
+
throw new PortableContextError(`${label} contains control characters`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function assertPortableContextSafe(
|
|
71
|
+
value,
|
|
72
|
+
{
|
|
73
|
+
label = "portable context",
|
|
74
|
+
} = {},
|
|
75
|
+
) {
|
|
76
|
+
try {
|
|
77
|
+
assertJsonLimits(value, label);
|
|
78
|
+
assertSecretFree(value);
|
|
79
|
+
checkObjectForSecrets(value, label);
|
|
80
|
+
return value;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code === E_PORTABLE_CONTEXT_INVALID) {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
throw new PortableContextError(
|
|
86
|
+
`${label} failed safety verification: ${error.message}`,
|
|
87
|
+
{ code: E_PORTABLE_CONTEXT_INVALID, cause: error },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function deepFreeze(object) {
|
|
93
|
+
if (object === null || typeof object !== "object" || Object.isFrozen(object)) {
|
|
94
|
+
return object;
|
|
95
|
+
}
|
|
96
|
+
Object.freeze(object);
|
|
97
|
+
for (const value of Object.values(object)) {
|
|
98
|
+
if (value !== null && typeof value === "object") {
|
|
99
|
+
deepFreeze(value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return object;
|
|
103
|
+
}
|
package/src/core/preflight.js
CHANGED
|
@@ -28,6 +28,7 @@ import { PROFILE_PATH } from "./target-layout.js";
|
|
|
28
28
|
import { readPersistedRoute } from "./route-artifact.js";
|
|
29
29
|
import { validateEventLedger } from "./events.js";
|
|
30
30
|
import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
|
|
31
|
+
import { normalizeStructuralQualityConfig } from "./structural-quality/policy.js";
|
|
31
32
|
|
|
32
33
|
const PREVIEW_DECISION_LIMIT = 10;
|
|
33
34
|
const PREVIEW_DECISION_MAX_LENGTH = 240;
|
|
@@ -42,6 +43,17 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
|
|
|
42
43
|
const contract = await loadContract(target, packageRoot, errors, { taskId, contractPath });
|
|
43
44
|
const route = await loadRoute(target, packageRoot, errors, { taskId, routePath });
|
|
44
45
|
const config = await optionalConfig(target, packageRoot, errors);
|
|
46
|
+
if (config.structuralQuality !== undefined) {
|
|
47
|
+
try {
|
|
48
|
+
normalizeStructuralQualityConfig(config.structuralQuality);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
errors.push(issue(
|
|
51
|
+
error.code ?? "E_STRUCTURAL_QUALITY_CONFIGURATION_INVALID",
|
|
52
|
+
error.message,
|
|
53
|
+
[PROJECT_ARTIFACT_PATHS.config],
|
|
54
|
+
));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
45
57
|
const effectiveStrict = strict || config.complianceMode === "strict";
|
|
46
58
|
if (effectiveStrict && profile.status !== "verified") {
|
|
47
59
|
errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", [PROFILE_PATH]));
|
|
@@ -111,6 +123,26 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
|
|
|
111
123
|
|
|
112
124
|
const sortedErrors = sortIssues(errors);
|
|
113
125
|
const effectiveTaskId = taskId ?? contract?.value?.taskId ?? state?.taskId ?? "unknown";
|
|
126
|
+
let structuralQuality = {
|
|
127
|
+
mode: config.structuralQuality?.mode ?? "off",
|
|
128
|
+
provider: config.structuralQuality?.mode && config.structuralQuality.mode !== "off"
|
|
129
|
+
? config.structuralQuality.provider ?? "sentrux"
|
|
130
|
+
: null,
|
|
131
|
+
baseline: { status: config.structuralQuality?.mode && config.structuralQuality.mode !== "off" ? "MISSING" : "NOT_REQUESTED" },
|
|
132
|
+
current: { status: "NOT_OBSERVED" },
|
|
133
|
+
comparable: null,
|
|
134
|
+
completionRequired: config.structuralQuality?.mode === "gate",
|
|
135
|
+
reasonCodes: [],
|
|
136
|
+
next: config.structuralQuality?.mode === "gate" ? "CAPTURE_STRUCTURAL_QUALITY_BASELINE" : null,
|
|
137
|
+
};
|
|
138
|
+
if (effectiveTaskId !== "unknown") {
|
|
139
|
+
try {
|
|
140
|
+
const { projectStructuralQualityStatus } = await import("./structural-quality/service.js");
|
|
141
|
+
structuralQuality = await projectStructuralQualityStatus({ target, packageRoot, taskId: effectiveTaskId });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
structuralQuality.reasonCodes = [error.code ?? "E_STRUCTURAL_QUALITY_EVIDENCE_STALE"];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
114
146
|
return {
|
|
115
147
|
schemaVersion: 1,
|
|
116
148
|
protocolVersion: 1,
|
|
@@ -140,6 +172,7 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
|
|
|
140
172
|
},
|
|
141
173
|
} : {}),
|
|
142
174
|
...(sources ? { sources: { status: "valid", fingerprint: null } } : {}),
|
|
175
|
+
structuralQuality,
|
|
143
176
|
};
|
|
144
177
|
}
|
|
145
178
|
|
|
@@ -83,6 +83,21 @@ export function protocolInfo({ packageVersion = null } = {}) {
|
|
|
83
83
|
informationGainV2: true,
|
|
84
84
|
strategyOscillationDetection: true,
|
|
85
85
|
},
|
|
86
|
+
structuralQuality: {
|
|
87
|
+
version: 1,
|
|
88
|
+
supported: true,
|
|
89
|
+
schemaVersion: 1,
|
|
90
|
+
modes: ["off", "observe", "gate"],
|
|
91
|
+
builtInProviders: ["sentrux"],
|
|
92
|
+
commands: ["quality-baseline", "quality-verify", "quality-status"],
|
|
93
|
+
resource: "task/structural-quality",
|
|
94
|
+
providerNeutral: true,
|
|
95
|
+
defaultProvider: "sentrux",
|
|
96
|
+
transport: "mcp-stdio",
|
|
97
|
+
rootCauses: ["modularity", "acyclicity", "depth", "equality", "redundancy"],
|
|
98
|
+
baselineImmutableAfterExecution: true,
|
|
99
|
+
optimizationMaxExtraEvaluations: 2,
|
|
100
|
+
},
|
|
86
101
|
durableActions: {
|
|
87
102
|
version: 1,
|
|
88
103
|
supported: true,
|
|
@@ -145,11 +160,27 @@ export function protocolInfo({ packageVersion = null } = {}) {
|
|
|
145
160
|
rebinding: "EXPLICIT_ONLY",
|
|
146
161
|
},
|
|
147
162
|
canonicalHandoffs: {
|
|
148
|
-
version:
|
|
163
|
+
version: 2,
|
|
149
164
|
supported: true,
|
|
150
165
|
immutable: true,
|
|
151
|
-
actorControlledIntent: true,
|
|
152
166
|
lifecycleAuthority: false,
|
|
167
|
+
evidenceAuthority: false,
|
|
168
|
+
exactlyOnceAcceptance: true,
|
|
169
|
+
acceptanceLedgerBacked: true,
|
|
170
|
+
acceptanceCommand: "handoff-accept",
|
|
171
|
+
acceptanceStatuses: ["OPEN", "ACCEPTED", "UNBOUND", "INCONSISTENT"],
|
|
172
|
+
},
|
|
173
|
+
advisoryContextProviders: {
|
|
174
|
+
version: 1,
|
|
175
|
+
supported: true,
|
|
176
|
+
providerNeutral: true,
|
|
177
|
+
integrationApiOnly: true,
|
|
178
|
+
lazy: true,
|
|
179
|
+
optIn: true,
|
|
180
|
+
persistedByForgeLoop: false,
|
|
181
|
+
lifecycleAuthority: false,
|
|
182
|
+
evidenceAuthority: false,
|
|
183
|
+
executable: false,
|
|
153
184
|
},
|
|
154
185
|
responsibilityConstraints: {
|
|
155
186
|
version: 1,
|
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
isVerificationExecutionAdapter,
|
|
4
4
|
normalizeVerificationExecutionPolicy,
|
|
5
5
|
} from "./verification-execution.js";
|
|
6
|
+
import { STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN } from "./structural-quality/constants.js";
|
|
7
|
+
import { E_ADVISORY_CONTEXT_PROVIDER_INVALID } from "./error-codes.js";
|
|
8
|
+
import { assertAdvisoryContextProviderIdentity } from "./advisory-context/provider.js";
|
|
6
9
|
|
|
7
10
|
export const AUTHORITY_TRUST_MODES = Object.freeze(["NONE", "HOST_ATTESTED"]);
|
|
8
11
|
|
|
@@ -105,5 +108,60 @@ export function createForgeLoopContext(options = {}) {
|
|
|
105
108
|
}
|
|
106
109
|
context.usageProvider = options.usageProvider;
|
|
107
110
|
}
|
|
111
|
+
if (options?.structuralQualityProviders !== undefined) {
|
|
112
|
+
const configured = options.structuralQualityProviders instanceof Map
|
|
113
|
+
? Object.fromEntries(options.structuralQualityProviders.entries())
|
|
114
|
+
: options.structuralQualityProviders;
|
|
115
|
+
if (!configured || typeof configured !== "object" || Array.isArray(configured)) {
|
|
116
|
+
const error = new Error("structuralQualityProviders must be an object or Map");
|
|
117
|
+
error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
const providers = {};
|
|
121
|
+
for (const [id, provider] of Object.entries(configured)) {
|
|
122
|
+
if (!STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(id) || id === "sentrux") {
|
|
123
|
+
const error = new Error(`Invalid or reserved structural-quality provider ID: ${id}`);
|
|
124
|
+
error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
if (typeof provider !== "function"
|
|
128
|
+
&& (!provider || typeof provider !== "object" || Array.isArray(provider))) {
|
|
129
|
+
const error = new Error(`Structural-quality provider ${id} must be an object or factory`);
|
|
130
|
+
error.code = "E_STRUCTURAL_QUALITY_PROVIDER_INVALID";
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
providers[id] = provider;
|
|
134
|
+
}
|
|
135
|
+
context.structuralQualityProviders = Object.freeze(providers);
|
|
136
|
+
}
|
|
137
|
+
if (options?.advisoryContextProviders !== undefined) {
|
|
138
|
+
const configured = options.advisoryContextProviders instanceof Map
|
|
139
|
+
? Object.fromEntries(options.advisoryContextProviders.entries())
|
|
140
|
+
: options.advisoryContextProviders;
|
|
141
|
+
if (!configured || typeof configured !== "object" || Array.isArray(configured)) {
|
|
142
|
+
const error = new Error("advisoryContextProviders must be an object or Map");
|
|
143
|
+
error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
const providers = {};
|
|
147
|
+
for (const [id, provider] of Object.entries(configured)) {
|
|
148
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(id)) {
|
|
149
|
+
const error = new Error(`Invalid advisory-context provider ID: ${id}`);
|
|
150
|
+
error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
if (typeof provider !== "function"
|
|
154
|
+
&& (!provider || typeof provider !== "object" || Array.isArray(provider))) {
|
|
155
|
+
const error = new Error(`Advisory-context provider ${id} must be an object or factory`);
|
|
156
|
+
error.code = E_ADVISORY_CONTEXT_PROVIDER_INVALID;
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
if (typeof provider !== "function") {
|
|
160
|
+
assertAdvisoryContextProviderIdentity(provider, id);
|
|
161
|
+
}
|
|
162
|
+
providers[id] = provider;
|
|
163
|
+
}
|
|
164
|
+
context.advisoryContextProviders = Object.freeze(providers);
|
|
165
|
+
}
|
|
108
166
|
return Object.freeze(context);
|
|
109
167
|
}
|
|
@@ -54,6 +54,7 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
|
|
|
54
54
|
"execution-profile-benchmark-scenario",
|
|
55
55
|
"execution-profile-benchmark-run",
|
|
56
56
|
"execution-profile-benchmark-aggregate",
|
|
57
|
+
"structural-quality",
|
|
57
58
|
]);
|
|
58
59
|
|
|
59
60
|
export class SchemaValidationError extends Error {
|