@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.
- package/.cursor/rules/project-loop.mdc +6 -3
- package/.github/copilot-instructions.md +5 -0
- package/AGENTS.md +6 -0
- package/CLAUDE.md +6 -0
- package/DOCS_INDEX.md +5 -0
- package/ENG/accessibility-eng.md +12 -2
- package/ENG/design-code-eng.md +22 -1
- package/LOOP_ENGINEERING.md +28 -0
- package/PROTOCOL_INTEGRATION.md +26 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +11 -0
- package/THREAT_MODEL.md +24 -0
- package/completions/_forgeloop +4 -1
- package/completions/forgeloop.bash +7 -1
- package/completions/forgeloop.fish +19 -1
- package/docs/AGENT_PROTOCOL_SUMMARY.md +6 -1
- package/docs/ARTIFACT_REFERENCE.md +128 -0
- package/docs/CLI_REFERENCE.md +84 -1
- package/docs/KNOWLEDGE_SOURCES.md +161 -0
- package/docs/MCP.md +1 -1
- package/docs/RECIPES.md +31 -0
- package/docs/STRUCTURAL_QUALITY.md +350 -0
- package/docs/TROUBLESHOOTING.md +107 -0
- package/package.json +3 -1
- package/schemas/config.schema.json +46 -0
- package/schemas/preflight.schema.json +2 -1
- package/schemas/structural-quality.schema.json +175 -0
- package/src/cli.js +18 -0
- 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/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 +45 -0
- package/src/core/command-executors.js +16 -0
- package/src/core/command-input.js +12 -0
- 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/error-codes.js +73 -0
- package/src/core/filesystem.js +18 -3
- package/src/core/inspect.js +64 -0
- package/src/core/integration-invocation-policy.js +15 -0
- package/src/core/integration-resources.js +17 -0
- 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/preflight.js +33 -0
- package/src/core/protocol-info.js +15 -0
- package/src/core/runtime-context.js +27 -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 +25 -0
- package/src/integration.js +14 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
E_STRUCTURAL_QUALITY_PROVIDER_INVALID,
|
|
5
|
+
E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED,
|
|
6
|
+
E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE,
|
|
7
|
+
} from "../error-codes.js";
|
|
8
|
+
import {
|
|
9
|
+
STRUCTURAL_QUALITY_MAX_DIAGNOSTIC_STRING,
|
|
10
|
+
STRUCTURAL_QUALITY_MAX_DIAGNOSTICS,
|
|
11
|
+
STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN,
|
|
12
|
+
STRUCTURAL_QUALITY_ROOT_CAUSES,
|
|
13
|
+
structuralQualityError,
|
|
14
|
+
STRUCTURAL_QUALITY_SENTRUX_VERIFIED_VERSIONS,
|
|
15
|
+
} from "./constants.js";
|
|
16
|
+
|
|
17
|
+
const SECRET_KEY = /(token|secret|password|passwd|api[-_]?key|authorization|cookie|private[-_]?key|credential)/iu;
|
|
18
|
+
const DETECTION_TRANSPORT = "mcp-stdio";
|
|
19
|
+
|
|
20
|
+
export function structuralQualityProviderCompatibility({ id, version, measurementModel, compatibilityKey } = {}) {
|
|
21
|
+
if (id !== "sentrux") return { supported: true, measurementModel, compatibilityKey };
|
|
22
|
+
if (!STRUCTURAL_QUALITY_SENTRUX_VERIFIED_VERSIONS.includes(version)) {
|
|
23
|
+
return {
|
|
24
|
+
supported: false,
|
|
25
|
+
reasonCode: E_STRUCTURAL_QUALITY_PROVIDER_VERSION_UNSUPPORTED,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
supported: true,
|
|
30
|
+
measurementModel: "structural-root-causes-v1",
|
|
31
|
+
compatibilityKey: "sentrux-structural-root-causes-v1",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isRecord(value) {
|
|
36
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function providerError(code, message, artifacts = []) {
|
|
40
|
+
return structuralQualityError(code, message, artifacts);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requiredString(value, label, { allowEmpty = false } = {}) {
|
|
44
|
+
if (typeof value !== "string" || (!allowEmpty && value.trim() === "")) {
|
|
45
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} must be a non-empty string`);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function score(value, label) {
|
|
51
|
+
if (!Number.isInteger(value) || value < 0 || value > 10_000) {
|
|
52
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} must be an integer between 0 and 10000`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function rawScore(value, label) {
|
|
58
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
59
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} must be a finite number`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function nonNegativeIntegerOrNull(value, label) {
|
|
65
|
+
if (value === undefined || value === null) return null;
|
|
66
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
67
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} must be a non-negative integer or null`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function relativePortablePath(value, projectPath, label) {
|
|
73
|
+
if (typeof value !== "string") return value;
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!trimmed) return trimmed;
|
|
76
|
+
if (!path.isAbsolute(trimmed)) return trimmed.replaceAll("\\", "/");
|
|
77
|
+
if (!projectPath) {
|
|
78
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} must not contain an absolute path`);
|
|
79
|
+
}
|
|
80
|
+
const relative = path.relative(path.resolve(projectPath), path.resolve(trimmed));
|
|
81
|
+
if (!relative || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
82
|
+
return relative.replaceAll("\\", "/") || ".";
|
|
83
|
+
}
|
|
84
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} contains a path outside the project target`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function boundedDiagnosticValue(value, projectPath, label, depth = 0) {
|
|
88
|
+
if (depth > 8) throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} is too deeply nested`);
|
|
89
|
+
if (typeof value === "string") {
|
|
90
|
+
if (value.length > STRUCTURAL_QUALITY_MAX_DIAGNOSTIC_STRING) {
|
|
91
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} exceeds the diagnostic string limit`);
|
|
92
|
+
}
|
|
93
|
+
return relativePortablePath(value, projectPath, label);
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) return value;
|
|
96
|
+
if (Array.isArray(value)) {
|
|
97
|
+
if (value.length > STRUCTURAL_QUALITY_MAX_DIAGNOSTICS) {
|
|
98
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} exceeds the diagnostic count limit`);
|
|
99
|
+
}
|
|
100
|
+
return value.map((item, index) => boundedDiagnosticValue(item, projectPath, `${label}[${index}]`, depth + 1));
|
|
101
|
+
}
|
|
102
|
+
if (!isRecord(value)) {
|
|
103
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} contains an unsupported value`);
|
|
104
|
+
}
|
|
105
|
+
const output = {};
|
|
106
|
+
for (const [key, item] of Object.entries(value)) {
|
|
107
|
+
if (SECRET_KEY.test(key)) {
|
|
108
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `${label} contains a secret-like field`);
|
|
109
|
+
}
|
|
110
|
+
output[key] = boundedDiagnosticValue(item, projectPath, `${label}.${key}`, depth + 1);
|
|
111
|
+
}
|
|
112
|
+
return output;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function rootCauseInput(raw, cause) {
|
|
116
|
+
const source = raw?.rootCauses?.[cause]
|
|
117
|
+
?? raw?.root_causes?.[cause]
|
|
118
|
+
?? raw?.rootCauseScores?.[cause]
|
|
119
|
+
?? raw?.root_cause_scores?.[cause];
|
|
120
|
+
if (typeof source === "number") return { score: source, raw: source };
|
|
121
|
+
if (!isRecord(source)) {
|
|
122
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `scan.rootCauses.${cause} is required`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
score: source.score ?? source.normalizedScore ?? source.normalized_score,
|
|
126
|
+
raw: source.raw ?? source.value ?? source.metric,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function canonicalBottleneck(rootCauses) {
|
|
131
|
+
return STRUCTURAL_QUALITY_ROOT_CAUSES.reduce((best, cause) => (
|
|
132
|
+
best === null || rootCauses[cause].score < rootCauses[best].score ? cause : best
|
|
133
|
+
), null);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeStatistics(raw) {
|
|
137
|
+
const statistics = raw?.statistics ?? {};
|
|
138
|
+
const source = raw?.scan ?? raw;
|
|
139
|
+
return {
|
|
140
|
+
files: nonNegativeIntegerOrNull(statistics.files ?? source.files ?? source.fileCount ?? source.file_count, "snapshot.statistics.files"),
|
|
141
|
+
lines: nonNegativeIntegerOrNull(statistics.lines ?? source.lines ?? source.lineCount ?? source.line_count, "snapshot.statistics.lines"),
|
|
142
|
+
importEdges: nonNegativeIntegerOrNull(statistics.importEdges ?? statistics.import_edges ?? source.importEdges ?? source.import_edges ?? source.importEdgeCount, "snapshot.statistics.importEdges"),
|
|
143
|
+
crossModuleEdges: nonNegativeIntegerOrNull(statistics.crossModuleEdges ?? statistics.cross_module_edges ?? source.crossModuleEdges ?? source.cross_module_edges, "snapshot.statistics.crossModuleEdges"),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function normalizeStructuralQualityDetection(raw = {}, defaults = {}) {
|
|
148
|
+
const source = isRecord(raw) ? raw : {};
|
|
149
|
+
const providerId = source.providerId ?? source.provider_id ?? defaults.providerId ?? defaults.id;
|
|
150
|
+
requiredString(providerId, "detection.providerId");
|
|
151
|
+
if (!STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(providerId)) {
|
|
152
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "detection.providerId must be a lower-case provider ID");
|
|
153
|
+
}
|
|
154
|
+
const providerVersion = source.providerVersion ?? source.provider_version ?? source.version ?? defaults.providerVersion ?? defaults.version ?? null;
|
|
155
|
+
if (providerVersion !== null && typeof providerVersion !== "string") {
|
|
156
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "detection.providerVersion must be a string or null");
|
|
157
|
+
}
|
|
158
|
+
const transport = source.transport ?? defaults.transport ?? DETECTION_TRANSPORT;
|
|
159
|
+
requiredString(transport, "detection.transport");
|
|
160
|
+
const measurementModel = source.measurementModel ?? source.measurement_model ?? defaults.measurementModel ?? defaults.measurement_model ?? "structural-root-causes-v1";
|
|
161
|
+
requiredString(measurementModel, "detection.measurementModel");
|
|
162
|
+
const compatibilityKey = source.compatibilityKey ?? source.compatibility_key ?? defaults.compatibilityKey ?? defaults.compatibility_key ?? null;
|
|
163
|
+
if (compatibilityKey !== null && typeof compatibilityKey !== "string") {
|
|
164
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "detection.compatibilityKey must be a string or null");
|
|
165
|
+
}
|
|
166
|
+
const reasonCode = source.reasonCode ?? source.reason_code ?? null;
|
|
167
|
+
if (reasonCode !== null && typeof reasonCode !== "string") {
|
|
168
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "detection.reasonCode must be a string or null");
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
available: source.available === true,
|
|
172
|
+
providerId,
|
|
173
|
+
providerVersion,
|
|
174
|
+
transport,
|
|
175
|
+
measurementModel,
|
|
176
|
+
compatibilityKey,
|
|
177
|
+
reasonCode,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function normalizeStructuralQualitySnapshot(raw, { projectPath = null } = {}) {
|
|
182
|
+
const source = raw?.snapshot && isRecord(raw.snapshot) ? raw.snapshot : raw;
|
|
183
|
+
if (!isRecord(source)) throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "provider scan result must be an object");
|
|
184
|
+
const qualitySignal = source.qualitySignal ?? source.quality_signal ?? source.signal;
|
|
185
|
+
const rootCauses = Object.fromEntries(STRUCTURAL_QUALITY_ROOT_CAUSES.map((cause) => {
|
|
186
|
+
const input = rootCauseInput(source, cause);
|
|
187
|
+
return [cause, {
|
|
188
|
+
score: score(input.score, `snapshot.rootCauses.${cause}.score`),
|
|
189
|
+
raw: rawScore(input.raw, `snapshot.rootCauses.${cause}.raw`),
|
|
190
|
+
}];
|
|
191
|
+
}));
|
|
192
|
+
const snapshot = {
|
|
193
|
+
qualitySignal: score(qualitySignal, "snapshot.qualitySignal"),
|
|
194
|
+
bottleneck: canonicalBottleneck(rootCauses),
|
|
195
|
+
rootCauses,
|
|
196
|
+
statistics: normalizeStatistics(source),
|
|
197
|
+
diagnostics: source.diagnostics === undefined || source.diagnostics === null
|
|
198
|
+
? null
|
|
199
|
+
: boundedDiagnosticValue(source.diagnostics, projectPath, "snapshot.diagnostics"),
|
|
200
|
+
};
|
|
201
|
+
if (source.bottleneck !== undefined && source.bottleneck !== snapshot.bottleneck) {
|
|
202
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `snapshot.bottleneck must be the canonical lowest-score root cause (${snapshot.bottleneck})`);
|
|
203
|
+
}
|
|
204
|
+
return snapshot;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function assertStructuralQualityProvider(provider) {
|
|
208
|
+
if (!isRecord(provider)) {
|
|
209
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Structural-quality provider must be an object");
|
|
210
|
+
}
|
|
211
|
+
if (typeof provider.id !== "string" || !STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(provider.id)) {
|
|
212
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Structural-quality provider id must be a lower-case provider ID");
|
|
213
|
+
}
|
|
214
|
+
if (typeof provider.observe !== "function" && (typeof provider.detect !== "function" || typeof provider.scan !== "function")) {
|
|
215
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Structural-quality provider must expose observe(input) or detect(input) and scan(input)");
|
|
216
|
+
}
|
|
217
|
+
return provider;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function freezeProviderInput(input = {}) {
|
|
221
|
+
const source = isRecord(input) ? input : {};
|
|
222
|
+
const projectPath = requiredString(source.projectPath ?? source.target, "provider input.projectPath");
|
|
223
|
+
const taskId = requiredString(source.taskId, "provider input.taskId");
|
|
224
|
+
const timeoutMs = source.timeoutMs ?? 120_000;
|
|
225
|
+
const maxOutputBytes = source.maxOutputBytes ?? 2 * 1024 * 1024;
|
|
226
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 0) throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "provider input.timeoutMs must be a non-negative integer");
|
|
227
|
+
if (!Number.isInteger(maxOutputBytes) || maxOutputBytes < 1) throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "provider input.maxOutputBytes must be a positive integer");
|
|
228
|
+
return Object.freeze({ projectPath, taskId, timeoutMs, maxOutputBytes });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function createStructuralQualityProviderRegistry({ providers = {}, builtIns = {} } = {}) {
|
|
232
|
+
if (!isRecord(providers) || !isRecord(builtIns)) {
|
|
233
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, "Structural-quality provider registry entries must be objects");
|
|
234
|
+
}
|
|
235
|
+
const custom = new Map();
|
|
236
|
+
for (const [id, provider] of Object.entries(providers)) {
|
|
237
|
+
if (!STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(id) || id === "sentrux") {
|
|
238
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `Invalid or reserved provider ID: ${id}`);
|
|
239
|
+
}
|
|
240
|
+
custom.set(id, provider);
|
|
241
|
+
}
|
|
242
|
+
const builtInEntries = new Map(Object.entries(builtIns));
|
|
243
|
+
return Object.freeze({
|
|
244
|
+
async resolve(name, input) {
|
|
245
|
+
const providerOrFactory = custom.get(name) ?? builtInEntries.get(name);
|
|
246
|
+
if (!providerOrFactory) {
|
|
247
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE, `Structural-quality provider is unavailable: ${name}`);
|
|
248
|
+
}
|
|
249
|
+
const providerInput = freezeProviderInput(input);
|
|
250
|
+
const provider = typeof providerOrFactory === "function"
|
|
251
|
+
? await providerOrFactory(providerInput)
|
|
252
|
+
: providerOrFactory;
|
|
253
|
+
const asserted = assertStructuralQualityProvider(provider);
|
|
254
|
+
if (asserted.id !== name) {
|
|
255
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `Structural-quality provider identity ${asserted.id} does not match registry key ${name}`);
|
|
256
|
+
}
|
|
257
|
+
return asserted;
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function resolveStructuralQualityProvider({ providerName = "sentrux", target, taskId, timeoutMs, maxOutputBytes, runtimeContext } = {}) {
|
|
263
|
+
if (typeof providerName !== "string" || !STRUCTURAL_QUALITY_PROVIDER_ID_PATTERN.test(providerName)) {
|
|
264
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_INVALID, `Invalid structural-quality provider ID: ${providerName}`);
|
|
265
|
+
}
|
|
266
|
+
const configured = runtimeContext?.structuralQualityProviders;
|
|
267
|
+
const custom = configured instanceof Map ? Object.fromEntries(configured.entries()) : configured ?? {};
|
|
268
|
+
if (providerName !== "sentrux" && !Object.prototype.hasOwnProperty.call(custom, providerName)) {
|
|
269
|
+
throw providerError(E_STRUCTURAL_QUALITY_PROVIDER_UNAVAILABLE, `Structural-quality provider is unavailable: ${providerName}`);
|
|
270
|
+
}
|
|
271
|
+
const builtIns = {
|
|
272
|
+
sentrux: async (input) => {
|
|
273
|
+
const { createSentruxStructuralQualityProvider } = await import("./sentrux-mcp.js");
|
|
274
|
+
return createSentruxStructuralQualityProvider(input);
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
return createStructuralQualityProviderRegistry({ providers: custom, builtIns }).resolve(providerName, {
|
|
278
|
+
projectPath: target,
|
|
279
|
+
taskId,
|
|
280
|
+
timeoutMs,
|
|
281
|
+
maxOutputBytes,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function providerInputFor({ projectPath, taskId, timeoutMs, maxOutputBytes } = {}) {
|
|
286
|
+
return freezeProviderInput({ projectPath, taskId, timeoutMs, maxOutputBytes });
|
|
287
|
+
}
|