@cassiomc1/forgeloop 1.13.0 → 1.14.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/AGENT_COMPATIBILITY.md +8 -0
- package/DOCS_INDEX.md +35 -3
- package/ENG/nodejs-backend-development-eng.md +2 -2
- package/ENG/sec-code-eng.md +7 -7
- package/EXECUTION_STATE.md +12 -0
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/README.md +40 -25
- package/TERMINOLOGY.md +2 -0
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +19 -1
- package/completions/forgeloop.bash +37 -1
- package/completions/forgeloop.fish +123 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +27 -2
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +123 -0
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +366 -6
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +32 -11
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +35 -8
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +9 -0
- package/docs/RELEASE_CHECKLIST.md +38 -5
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +198 -1
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +750 -5
- package/docs/protocol-requirements.json +24 -0
- package/package.json +30 -3
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +6 -6
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +4 -1
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +313 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +282 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +108 -0
- package/src/commands/checkpoint-revalidate.js +176 -0
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +264 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +15 -0
- package/src/commands/gate-record.js +205 -0
- package/src/commands/gate-revalidate.js +137 -0
- package/src/commands/model-route.js +32 -0
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +17 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +10 -1
- package/src/core/artifact-registry.js +24 -0
- package/src/core/audit-ux.js +514 -0
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +241 -0
- package/src/core/command-executors.js +110 -0
- package/src/core/command-input.js +115 -43
- package/src/core/completion-artifacts.js +14 -5
- package/src/core/completion.js +4 -6
- package/src/core/config.js +3 -0
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/contract-bootstrap-recovery.js +655 -0
- package/src/core/contract-revision.js +210 -0
- package/src/core/decision/artifact.js +69 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +101 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +130 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +156 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +30 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/error-codes.js +278 -0
- package/src/core/events.js +226 -57
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +14 -0
- package/src/core/execution-profile.js +63 -38
- package/src/core/gate-provenance.js +124 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +130 -42
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +7 -2
- package/src/core/next-action.js +35 -7
- package/src/core/phase.js +128 -10
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +128 -52
- package/src/core/recovery-history.js +1 -0
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +15 -1
- package/src/core/router.js +67 -1
- package/src/core/runtime-context.js +118 -61
- package/src/core/schema-validation.js +3 -0
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/sources.js +37 -0
- package/src/core/task-claim-state.js +201 -1
- package/src/core/task-conflict-inspection.js +31 -5
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/templates.js +3 -0
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +90 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +11 -7
- package/src/core/transaction.js +1 -0
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { FAILURE_CLASSES, DIAGNOSIS_STRATEGIES, REVIEW_FOCUSES } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
function strings(value) {
|
|
4
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim()) : [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function text(input) {
|
|
8
|
+
return [input?.message, input?.error, input?.failure, input?.objective, ...(input?.paths ?? []), ...(input?.risks ?? [])]
|
|
9
|
+
.filter((value) => typeof value === "string").join(" ").toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function unique(values) { return [...new Set(values)]; }
|
|
13
|
+
|
|
14
|
+
function validateRecommendation(recommendation, allowed, label) {
|
|
15
|
+
if (recommendation === undefined || recommendation === null) return null;
|
|
16
|
+
const values = typeof recommendation === "string" ? [recommendation]
|
|
17
|
+
: recommendation && typeof recommendation === "object" && !Array.isArray(recommendation)
|
|
18
|
+
? Object.entries(recommendation).filter(([, value]) => value === true).map(([key]) => key)
|
|
19
|
+
: strings(recommendation);
|
|
20
|
+
const unknown = values.filter((value) => !allowed.includes(value));
|
|
21
|
+
if (unknown.length > 0) {
|
|
22
|
+
const error = new Error(`${label} contains unknown semantic values: ${unknown.join(", ")}`);
|
|
23
|
+
error.code = "E_DECISION_RESULT_INVALID";
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
return unique(values);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function projectFailureTriage({ input = {}, semanticRecommendation = null } = {}) {
|
|
30
|
+
const candidate = semanticRecommendation?.failureClass ?? semanticRecommendation;
|
|
31
|
+
const value = validateRecommendation(candidate, FAILURE_CLASSES, "failure recommendation");
|
|
32
|
+
const inferred = /timeout|timed out/.test(text(input)) ? "timeout"
|
|
33
|
+
: /network|dns|connection|environment/.test(text(input)) ? "network/environment"
|
|
34
|
+
: /ownership|protocol|ledger|phase/.test(text(input)) ? "ownership/protocol mismatch"
|
|
35
|
+
: "unknown";
|
|
36
|
+
return {
|
|
37
|
+
decisionKind: "FAILURE_TRIAGE",
|
|
38
|
+
questionSetId: "failure-v1",
|
|
39
|
+
candidates: [...FAILURE_CLASSES],
|
|
40
|
+
ranked: unique([...(value ?? []), inferred]),
|
|
41
|
+
semanticRequired: true,
|
|
42
|
+
unknownEscalates: !value,
|
|
43
|
+
authority: "SEMANTIC_DECISION",
|
|
44
|
+
evidenceAuthority: "NONE",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function projectDiagnosisPriority({ input = {}, semanticRecommendation = null } = {}) {
|
|
49
|
+
const candidate = semanticRecommendation?.priority ?? semanticRecommendation;
|
|
50
|
+
const value = validateRecommendation(candidate, DIAGNOSIS_STRATEGIES, "diagnosis recommendation");
|
|
51
|
+
const inferred = /race|concurr|lock/.test(text(input)) ? "inspect concurrency path"
|
|
52
|
+
: /state|ledger|phase|fingerprint/.test(text(input)) ? "inspect state transition"
|
|
53
|
+
: /depend|package|module/.test(text(input)) ? "inspect dependency behavior"
|
|
54
|
+
: "run narrower experiment";
|
|
55
|
+
return {
|
|
56
|
+
decisionKind: "DIAGNOSIS_PRIORITY",
|
|
57
|
+
questionSetId: "diagnosis-v1",
|
|
58
|
+
candidates: [...DIAGNOSIS_STRATEGIES],
|
|
59
|
+
ranked: unique([...(value ?? []), inferred]),
|
|
60
|
+
previouslyFailed: strings(input.previouslyFailed),
|
|
61
|
+
semanticRequired: true,
|
|
62
|
+
unknownEscalates: !value,
|
|
63
|
+
authority: "SEMANTIC_DECISION",
|
|
64
|
+
evidenceAuthority: "NONE",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function projectReviewPlan({ input = {}, semanticRecommendation = null } = {}) {
|
|
69
|
+
const candidate = semanticRecommendation?.reviewFocus ?? semanticRecommendation;
|
|
70
|
+
const value = validateRecommendation(candidate, REVIEW_FOCUSES, "review recommendation");
|
|
71
|
+
const body = text(input);
|
|
72
|
+
const mandatory = ["needs_full_diff_context"];
|
|
73
|
+
if (/security|secret|credential|auth|permission/.test(body)) mandatory.push("needs_security_review");
|
|
74
|
+
if (/api|public|compatib/.test(body)) mandatory.push("needs_api_compatibility_review");
|
|
75
|
+
if (/race|concurr|lock/.test(body)) mandatory.push("needs_concurrency_review");
|
|
76
|
+
if (/package|release|dependency/.test(body)) mandatory.push("needs_package_review", "needs_dependency_review");
|
|
77
|
+
if (/doc|readme|guide/.test(body)) mandatory.push("needs_docs_review");
|
|
78
|
+
if (/migration|schema|database/.test(body)) mandatory.push("needs_migration_review");
|
|
79
|
+
if (/performance|timeout|latency/.test(body)) mandatory.push("needs_performance_review");
|
|
80
|
+
if (input.fullDiff === true || body.includes("full diff")) mandatory.push("needs_full_diff_context");
|
|
81
|
+
const ranked = unique([...mandatory, ...(value ?? [])]);
|
|
82
|
+
if (ranked.length === 0) ranked.push("needs_full_diff_context");
|
|
83
|
+
if (input.primaryModel === true || input.ambiguity === true) ranked.push("needs_primary_model");
|
|
84
|
+
return {
|
|
85
|
+
decisionKind: "REVIEW_PLAN",
|
|
86
|
+
questionSetId: "review-v1",
|
|
87
|
+
mandatory: unique(mandatory),
|
|
88
|
+
ranked: unique(ranked),
|
|
89
|
+
semanticRequired: true,
|
|
90
|
+
unknownEscalates: !value,
|
|
91
|
+
authority: "SEMANTIC_DECISION",
|
|
92
|
+
evidenceAuthority: "NONE",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DECISION_ERROR_CODES, decisionError } from "../decision/errors.js";
|
|
2
|
+
import { projectDiagnosisPriority, projectFailureTriage, projectReviewPlan } from "./projection.js";
|
|
3
|
+
|
|
4
|
+
const PROJECTORS = Object.freeze({
|
|
5
|
+
failure: projectFailureTriage,
|
|
6
|
+
diagnosis: projectDiagnosisPriority,
|
|
7
|
+
review: projectReviewPlan,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export function runSemanticPlan({ kind, input = {}, semanticRecommendation = null } = {}) {
|
|
11
|
+
const projector = PROJECTORS[kind];
|
|
12
|
+
if (!projector) throw decisionError(DECISION_ERROR_CODES.QUESTION_SET_UNKNOWN, `Unknown semantic plan kind: ${kind}`);
|
|
13
|
+
return projector({ input, semanticRecommendation });
|
|
14
|
+
}
|
|
15
|
+
|
package/src/core/sources.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import { CONTRACT_PRESET_IDS } from "./contract-presets.js";
|
|
1
2
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
2
3
|
|
|
4
|
+
const CONTRACT_PRESET_PREFIX = "contract-preset:";
|
|
5
|
+
const BUILTIN_CONTRACT_PRESET_REFS = new Set(
|
|
6
|
+
CONTRACT_PRESET_IDS.map((presetId) => `${CONTRACT_PRESET_PREFIX}${presetId}`),
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
export function isBuiltinContractPresetRef(ref) {
|
|
10
|
+
return typeof ref === "string" && BUILTIN_CONTRACT_PRESET_REFS.has(ref);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function externalContractSourceRefs(refs = []) {
|
|
14
|
+
return refs.filter((ref) => !isBuiltinContractPresetRef(ref));
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
export const SOURCE_REGISTRY_SCHEMA_VERSION = 1;
|
|
4
18
|
export const SOURCE_KINDS = Object.freeze([
|
|
5
19
|
"user-request",
|
|
@@ -84,3 +98,26 @@ export function assertSourceProvenance(registry, refs = [], { expectedKind } = {
|
|
|
84
98
|
}
|
|
85
99
|
return true;
|
|
86
100
|
}
|
|
101
|
+
|
|
102
|
+
export function assertContractPresetRefs(refs = []) {
|
|
103
|
+
for (const ref of refs) {
|
|
104
|
+
if (typeof ref === "string" && ref.startsWith(CONTRACT_PRESET_PREFIX) && !isBuiltinContractPresetRef(ref)) {
|
|
105
|
+
const error = new Error(`Unknown built-in contract preset source: ${ref}`);
|
|
106
|
+
error.code = "E_PROFILE_SOURCE_UNKNOWN";
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function assertContractSourceProvenance(registry, refs = []) {
|
|
114
|
+
assertContractPresetRefs(refs);
|
|
115
|
+
const externalRefs = externalContractSourceRefs(refs);
|
|
116
|
+
if (externalRefs.length === 0) return true;
|
|
117
|
+
if (!registry) {
|
|
118
|
+
const error = new Error("External contract source references require a source registry");
|
|
119
|
+
error.code = "E_PROFILE_SOURCE_MISSING";
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
return assertSourceProvenance(registry, externalRefs);
|
|
123
|
+
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
E_TASK_RECOVERY_INCONSISTENT,
|
|
7
7
|
} from "./error-codes.js";
|
|
8
8
|
import { validateCompletionOwnershipProof } from "./completion-ownership.js";
|
|
9
|
-
import { validateEventLedger } from "./events.js";
|
|
9
|
+
import { validateEventLedger, validateStateLedgerCoherence } from "./events.js";
|
|
10
10
|
import { classifyRecoveryHistory } from "./recovery-history.js";
|
|
11
11
|
import { readTaskDescriptor } from "./task-descriptor.js";
|
|
12
12
|
import { normalizeWriteClaims } from "./task-scope.js";
|
|
@@ -15,6 +15,35 @@ import {
|
|
|
15
15
|
validateTaskRecoveryConsistency,
|
|
16
16
|
} from "./task-recovery.js";
|
|
17
17
|
import { readWorkState } from "./work-state.js";
|
|
18
|
+
import { readContract } from "./contract.js";
|
|
19
|
+
import { readPersistedRoute } from "./route-artifact.js";
|
|
20
|
+
import {
|
|
21
|
+
CONTRACT_BOOTSTRAP_REPAIR_EVENT,
|
|
22
|
+
resolveEffectiveContractBootstrapRepairAnchor,
|
|
23
|
+
resolveCanonicalPostRepairCheckpointBinding,
|
|
24
|
+
resolveCanonicalPostRepairRouteBinding,
|
|
25
|
+
sameCanonicalGuideList,
|
|
26
|
+
} from "./contract-bootstrap-recovery.js";
|
|
27
|
+
import { canonicalFingerprint } from "./artifacts.js";
|
|
28
|
+
import { stateIdentityErrors } from "./completion-relationships.js";
|
|
29
|
+
import { routeCheckpointMustMatchCurrentRoute } from "./resumability.js";
|
|
30
|
+
|
|
31
|
+
const REPAIR_PHASE_EVENTS = Object.freeze({
|
|
32
|
+
CONTRACT_READY: "CONTRACT_VALIDATED",
|
|
33
|
+
ROUTED: "ROUTE_VALIDATED",
|
|
34
|
+
DESIGNING: "DESIGN_GATE_STARTED",
|
|
35
|
+
PLANNED: "PLAN_RECORDED",
|
|
36
|
+
EXECUTING: "EXECUTION_STARTED",
|
|
37
|
+
VERIFYING: "VERIFICATION_STARTED",
|
|
38
|
+
DIAGNOSING: "VERIFICATION_STARTED",
|
|
39
|
+
CORRECTING: "VERIFICATION_STARTED",
|
|
40
|
+
REVIEWING: "REVIEW_STARTED",
|
|
41
|
+
COMPLETE: "COMPLETION_VALIDATED",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const REPAIR_ROUTE_PHASES = new Set([
|
|
45
|
+
"ROUTED", "DESIGNING", "PLANNED", "EXECUTING", "VERIFYING", "DIAGNOSING", "CORRECTING", "REVIEWING", "COMPLETE",
|
|
46
|
+
]);
|
|
18
47
|
|
|
19
48
|
function ownershipError(message, cause = null) {
|
|
20
49
|
return {
|
|
@@ -24,6 +53,164 @@ function ownershipError(message, cause = null) {
|
|
|
24
53
|
};
|
|
25
54
|
}
|
|
26
55
|
|
|
56
|
+
function repairInvalid(message) {
|
|
57
|
+
return ownershipError(message, { code: "E_CONTRACT_BOOTSTRAP_REPAIR_INVALID" });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function lateRouteCheckpointErrors(events, state, marker) {
|
|
61
|
+
if (routeCheckpointMustMatchCurrentRoute(state.phase) || !REPAIR_PHASE_EVENTS[state.phase]) return [];
|
|
62
|
+
const checkpoint = resolveCanonicalPostRepairCheckpointBinding(events, marker);
|
|
63
|
+
if (!checkpoint) {
|
|
64
|
+
return [repairInvalid("Repaired late-phase state lacks an immutable route checkpoint binding")];
|
|
65
|
+
}
|
|
66
|
+
if (state.routeFingerprint !== checkpoint.routeFingerprint
|
|
67
|
+
|| !sameCanonicalGuideList(state.selectedGuides, checkpoint.selectedGuides)) {
|
|
68
|
+
return [repairInvalid("Late-phase checkpoint route identity was rewritten after the canonical checkpoint binding")];
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function repairArtifactErrors(artifacts) {
|
|
74
|
+
return [
|
|
75
|
+
...(artifacts?.contractError
|
|
76
|
+
? [repairInvalid(`Current contract artifact is invalid after a recorded repair: ${artifacts.contractError}`)]
|
|
77
|
+
: []),
|
|
78
|
+
...(artifacts?.routeError
|
|
79
|
+
? [repairInvalid(`Current route artifact is invalid after a recorded repair: ${artifacts.routeError}`)]
|
|
80
|
+
: []),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function repairAnchorErrors(details, state, artifacts) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
if (state.phase !== details.reconstructedPhase) {
|
|
87
|
+
errors.push(repairInvalid(
|
|
88
|
+
`Work-state phase ${state.phase} does not match repair anchor phase ${details.reconstructedPhase}`,
|
|
89
|
+
));
|
|
90
|
+
}
|
|
91
|
+
if (canonicalFingerprint(state) !== details.reconstructedStateFingerprint) {
|
|
92
|
+
errors.push(repairInvalid("Work-state fingerprint does not match the repair anchor"));
|
|
93
|
+
}
|
|
94
|
+
const stateRouteFingerprint = state.routeFingerprint ?? null;
|
|
95
|
+
const currentRouteFingerprint = artifacts?.route?.fingerprint ?? null;
|
|
96
|
+
if (stateRouteFingerprint !== details.routeFingerprint
|
|
97
|
+
|| currentRouteFingerprint !== details.routeFingerprint) {
|
|
98
|
+
errors.push(repairInvalid("Current route identity does not match the repair anchor"));
|
|
99
|
+
}
|
|
100
|
+
return errors;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function evolvedRepairErrors(anchor, events, state, artifacts) {
|
|
104
|
+
const errors = [];
|
|
105
|
+
const contract = artifacts?.contract ?? null;
|
|
106
|
+
const route = artifacts?.route ?? null;
|
|
107
|
+
if (!contract) errors.push(repairInvalid("Current contract is unavailable after a recorded repair"));
|
|
108
|
+
const routeRequired = REPAIR_ROUTE_PHASES.has(state.phase) || state.routeFingerprint !== undefined;
|
|
109
|
+
if (routeRequired && !route) {
|
|
110
|
+
errors.push(repairInvalid("Current route is required by the evolved task state but is unavailable"));
|
|
111
|
+
}
|
|
112
|
+
if (contract) {
|
|
113
|
+
const routeErrors = stateIdentityErrors({ contract, route, state });
|
|
114
|
+
const checkpointMustMatch = routeCheckpointMustMatchCurrentRoute(state.phase);
|
|
115
|
+
errors.push(...routeErrors
|
|
116
|
+
.filter((error) => checkpointMustMatch || error.code !== "E_ROUTE_GUIDE_MISMATCH")
|
|
117
|
+
.map((error) => repairInvalid(error.message)));
|
|
118
|
+
}
|
|
119
|
+
const currentRouteFingerprint = route?.fingerprint ?? null;
|
|
120
|
+
const binding = currentRouteFingerprint !== anchor.details.routeFingerprint
|
|
121
|
+
? resolveCanonicalPostRepairRouteBinding(events, anchor.sourceMarker, currentRouteFingerprint)
|
|
122
|
+
: null;
|
|
123
|
+
if (currentRouteFingerprint !== anchor.details.routeFingerprint && !binding) {
|
|
124
|
+
errors.push(repairInvalid("Changed route identity lacks a canonical post-repair route binding"));
|
|
125
|
+
}
|
|
126
|
+
errors.push(...lateRouteCheckpointErrors(events, state, anchor.sourceMarker));
|
|
127
|
+
errors.push(...validateStateLedgerCoherence(state, events).map((error) => repairInvalid(error.message)));
|
|
128
|
+
const requiredEvent = REPAIR_PHASE_EVENTS[state.phase];
|
|
129
|
+
if (requiredEvent && !events.some((event) => event.event === requiredEvent)) {
|
|
130
|
+
errors.push(repairInvalid(`Current work-state phase ${state.phase} is not supported by the repair ledger history`));
|
|
131
|
+
}
|
|
132
|
+
return errors;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateContractBootstrapRepairConsistency(taskId, events, state, artifacts) {
|
|
136
|
+
const anchor = resolveEffectiveContractBootstrapRepairAnchor(events);
|
|
137
|
+
if (!anchor) {
|
|
138
|
+
if (!events.some((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT)) return [];
|
|
139
|
+
return [repairInvalid("Contract bootstrap repair marker is invalid")];
|
|
140
|
+
}
|
|
141
|
+
if (!state) return [repairInvalid("Work-state is missing after a recorded contract bootstrap repair")];
|
|
142
|
+
const details = anchor.details;
|
|
143
|
+
const errors = [];
|
|
144
|
+
if (state.taskId !== taskId) errors.push(repairInvalid("Work-state taskId does not match the repaired task"));
|
|
145
|
+
if (!Number.isInteger(state.revision) || state.revision < 0) {
|
|
146
|
+
return [...errors, repairInvalid("Work-state revision is missing or invalid after a recorded repair")];
|
|
147
|
+
}
|
|
148
|
+
const anchorRevision = details.reconstructedStateRevision;
|
|
149
|
+
if (state.revision < anchorRevision) {
|
|
150
|
+
return [...errors, repairInvalid("Work-state revision rolled back behind the repair anchor")];
|
|
151
|
+
}
|
|
152
|
+
if (state.contractFingerprint !== details.contractFingerprint) {
|
|
153
|
+
errors.push(repairInvalid("Work-state contract fingerprint does not match repair-time contract identity"));
|
|
154
|
+
}
|
|
155
|
+
errors.push(...repairArtifactErrors(artifacts));
|
|
156
|
+
return state.revision === anchorRevision
|
|
157
|
+
? [...errors, ...repairAnchorErrors(details, state, artifacts)]
|
|
158
|
+
: [...errors, ...evolvedRepairErrors(anchor, events, state, artifacts)];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Async cross-artifact consistency collector for a recorded contract bootstrap
|
|
163
|
+
* repair. The pure classifier stays filesystem-free; this reads the current
|
|
164
|
+
* task-scoped contract and route artifacts through their canonical readers so
|
|
165
|
+
* schema validation, artifact bounds, task-scoped paths, and fingerprint
|
|
166
|
+
* semantics stay consistent with the rest of the protocol.
|
|
167
|
+
*
|
|
168
|
+
* With no repair marker this performs no extra work.
|
|
169
|
+
*/
|
|
170
|
+
async function collectContractBootstrapRepairConsistency(target, packageRoot, taskId, events, state) {
|
|
171
|
+
const anchor = resolveEffectiveContractBootstrapRepairAnchor(events);
|
|
172
|
+
if (!anchor) return null;
|
|
173
|
+
const details = anchor.details;
|
|
174
|
+
const artifacts = { contract: null, route: null, contractError: null, routeError: null };
|
|
175
|
+
|
|
176
|
+
// Contract binding: the current contract must exist, validate, and bind to
|
|
177
|
+
// the marker and work state.
|
|
178
|
+
try {
|
|
179
|
+
const contract = await readContract(target, packageRoot, { taskId });
|
|
180
|
+
artifacts.contract = contract;
|
|
181
|
+
if (contract.fingerprint !== details.contractFingerprint) {
|
|
182
|
+
artifacts.contractError = `contract fingerprint ${contract.fingerprint} does not match marker contract fingerprint ${details.contractFingerprint}`;
|
|
183
|
+
} else if (state && state.contractFingerprint !== details.contractFingerprint) {
|
|
184
|
+
artifacts.contractError = "work-state contract fingerprint does not match marker contract fingerprint";
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
artifacts.contractError = error.code === "ARTIFACT_MISSING"
|
|
188
|
+
? "contract artifact is missing after a recorded repair"
|
|
189
|
+
: `contract artifact is invalid after a recorded repair: ${error.message}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Route binding is anchored exactly at repair time, then follows the
|
|
193
|
+
// current state/route identity after the checkpoint advances.
|
|
194
|
+
const routeRequired = state && (REPAIR_ROUTE_PHASES.has(state.phase)
|
|
195
|
+
|| state.routeFingerprint !== undefined);
|
|
196
|
+
if (routeRequired || details.routeFingerprint !== null) {
|
|
197
|
+
try {
|
|
198
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId });
|
|
199
|
+
artifacts.route = route;
|
|
200
|
+
if (route.value?.contractFingerprint !== undefined
|
|
201
|
+
&& route.value.contractFingerprint !== details.contractFingerprint) {
|
|
202
|
+
artifacts.routeError = "route artifact contract binding does not match repair anchor contract fingerprint";
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
artifacts.routeError = error.code === "ARTIFACT_MISSING"
|
|
206
|
+
? "route artifact is missing after a routed repair"
|
|
207
|
+
: `route artifact is invalid after a routed repair: ${error.message}`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return artifacts;
|
|
212
|
+
}
|
|
213
|
+
|
|
27
214
|
function appendClaims(target, claims, errors, source) {
|
|
28
215
|
if (claims === undefined || claims === null) return;
|
|
29
216
|
try {
|
|
@@ -85,6 +272,19 @@ export async function collectTaskClaimEvidence(target, {
|
|
|
85
272
|
if (ledger.events.some((event) => event.taskId !== taskId)) {
|
|
86
273
|
errors.push(ownershipError(`Task event ledger contains an event for a different task`));
|
|
87
274
|
}
|
|
275
|
+
if (state) {
|
|
276
|
+
for (const error of validateStateLedgerCoherence(state, ledger.events)) {
|
|
277
|
+
if (![
|
|
278
|
+
"E_CHECKPOINT_REVALIDATION_UNSAFE",
|
|
279
|
+
"E_CONTRACT_REVISION_UNSAFE",
|
|
280
|
+
].includes(error.code)) continue;
|
|
281
|
+
errors.push(ownershipError(`Task state and event ledger are incoherent: ${error.message}`, error));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const repairArtifacts = await collectContractBootstrapRepairConsistency(target, packageRoot, taskId, ledger.events, state);
|
|
286
|
+
const repairConsistencyErrors = validateContractBootstrapRepairConsistency(taskId, ledger.events, state, repairArtifacts);
|
|
287
|
+
errors.push(...repairConsistencyErrors);
|
|
88
288
|
|
|
89
289
|
const history = classifyRecoveryHistory(ledger.events);
|
|
90
290
|
const descriptorClaims = [];
|
|
@@ -64,6 +64,27 @@ function lastMeaningfulActivity(state, events) {
|
|
|
64
64
|
return candidates.sort((left, right) => Date.parse(right.at) - Date.parse(left.at))[0] ?? null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function isInitialTaskLedger(events, taskId) {
|
|
68
|
+
return events.length >= 2
|
|
69
|
+
&& events[0]?.taskId === taskId
|
|
70
|
+
&& events[0]?.event === "TASK_RECEIVED"
|
|
71
|
+
&& events[1]?.taskId === taskId
|
|
72
|
+
&& events[1]?.event === "TRANSACTION_COMMITTED"
|
|
73
|
+
&& events[1]?.details?.operation === "task-create"
|
|
74
|
+
&& !events.slice(2).some((event) => [
|
|
75
|
+
"CONTRACT_VALIDATED", "ROUTE_VALIDATED", "PREFLIGHT_READY", "EXECUTION_STARTED",
|
|
76
|
+
"VERIFICATION_STARTED", "VERIFICATION_RECORDED", "REVIEW_STARTED", "COMPLETION_VALIDATED",
|
|
77
|
+
"TASK_RECOVERY_RECORDED", "OPERATOR_RECOVERY_RECORDED", "TASK_RECOVERY_RESUMED",
|
|
78
|
+
].includes(event.event));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function deriveEarlyPhase(events, taskId) {
|
|
82
|
+
if (!isInitialTaskLedger(events, taskId)) return null;
|
|
83
|
+
return events.some((event) => event.event === "DISCOVERY_STARTED")
|
|
84
|
+
? "DISCOVERING"
|
|
85
|
+
: "RECEIVED";
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
/**
|
|
68
89
|
* Deterministic classification of a potentially conflicting task from
|
|
69
90
|
* machine-readable state only. REVIEWING plus an old timestamp alone is never
|
|
@@ -97,6 +118,7 @@ export function classifyConflictEvidence(evidence, {
|
|
|
97
118
|
totalChecks,
|
|
98
119
|
verificationEvidenceCount,
|
|
99
120
|
recoveryStatus,
|
|
121
|
+
earlyPhase,
|
|
100
122
|
} = evidence;
|
|
101
123
|
|
|
102
124
|
if (!ledgerValid) {
|
|
@@ -110,7 +132,8 @@ export function classifyConflictEvidence(evidence, {
|
|
|
110
132
|
"RECEIVED", "DISCOVERING", "CONTRACT_READY", "ROUTED", "DESIGNING", "PLANNED",
|
|
111
133
|
...POST_EXECUTION_PHASES,
|
|
112
134
|
]);
|
|
113
|
-
|
|
135
|
+
const effectivePhase = phase ?? earlyPhase;
|
|
136
|
+
if (!effectivePhase || !KNOWN_PHASES.has(effectivePhase)) {
|
|
114
137
|
return {
|
|
115
138
|
classification: "INCONSISTENT",
|
|
116
139
|
reasonCodes: ["E_PHASE_UNKNOWN"],
|
|
@@ -151,7 +174,7 @@ export function classifyConflictEvidence(evidence, {
|
|
|
151
174
|
};
|
|
152
175
|
}
|
|
153
176
|
|
|
154
|
-
if (
|
|
177
|
+
if (effectivePhase === "COMPLETE") {
|
|
155
178
|
return {
|
|
156
179
|
classification: "COMPLETE",
|
|
157
180
|
reasonCodes: ["TASK_COMPLETE"],
|
|
@@ -175,7 +198,7 @@ export function classifyConflictEvidence(evidence, {
|
|
|
175
198
|
};
|
|
176
199
|
}
|
|
177
200
|
|
|
178
|
-
if (POST_EXECUTION_PHASES.has(
|
|
201
|
+
if (POST_EXECUTION_PHASES.has(effectivePhase)) {
|
|
179
202
|
if (staleOnlyRepositoryDrift) {
|
|
180
203
|
return {
|
|
181
204
|
classification: "RECOVERABLE",
|
|
@@ -262,6 +285,9 @@ export async function inspectTaskConflictState(target, {
|
|
|
262
285
|
const ledgerErrors = [...ownershipEvidence.ledger.errors, ...coherenceErrors];
|
|
263
286
|
|
|
264
287
|
const repository = await currentRepositoryFingerprint(target);
|
|
288
|
+
const initialTask = !state && ledgerValid && ownershipEvidence.descriptor
|
|
289
|
+
&& isInitialTaskLedger(ledgerEvents, taskId)
|
|
290
|
+
&& !ownershipEvidence.recovery;
|
|
265
291
|
const meaningfulActivity = lastMeaningfulActivity(state, ledgerEvents);
|
|
266
292
|
const recoveryConsistencyErrors = claimProjection.ownershipErrors ?? claimProjection.errors ?? [];
|
|
267
293
|
const healthReasonCodes = [
|
|
@@ -275,7 +301,7 @@ export async function inspectTaskConflictState(target, {
|
|
|
275
301
|
healthy: healthReasonCodes.length === 0,
|
|
276
302
|
healthReasonCodes,
|
|
277
303
|
descriptorHealthy: true,
|
|
278
|
-
phase: state?.phase ?? null,
|
|
304
|
+
phase: state?.phase ?? (initialTask ? deriveEarlyPhase(ledgerEvents, taskId) : null),
|
|
279
305
|
lastUpdated: state?.lastUpdated ?? null,
|
|
280
306
|
lastMeaningfulActivityAt: meaningfulActivity?.at ?? null,
|
|
281
307
|
lastMeaningfulEventType: meaningfulActivity?.type ?? null,
|
|
@@ -283,7 +309,7 @@ export async function inspectTaskConflictState(target, {
|
|
|
283
309
|
lockStatus: lockClassification.status,
|
|
284
310
|
lockId: lockInfo?.lockId ?? null,
|
|
285
311
|
lockExpiresAt: lockClassification.expiresAt ?? null,
|
|
286
|
-
freshnessStatus: freshness?.status ?? "UNKNOWN",
|
|
312
|
+
freshnessStatus: freshness?.status ?? (initialTask ? "FRESH" : "UNKNOWN"),
|
|
287
313
|
freshnessReasons: freshness?.reasons ?? [],
|
|
288
314
|
ledgerValid,
|
|
289
315
|
ledgerLastSeq: ledgerEvents.at(-1)?.seq ?? 0,
|
package/src/core/task-paths.js
CHANGED
|
@@ -27,6 +27,8 @@ export const TASK_ARTIFACT_FILES = Object.freeze({
|
|
|
27
27
|
verificationScope: "verification-scope.json",
|
|
28
28
|
attestations: "attestations",
|
|
29
29
|
structuralQuality: "structural-quality",
|
|
30
|
+
decisions: "decisions",
|
|
31
|
+
testUtility: "test-utility.json",
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
export const POLICY_ROOT = ".forgeloop/policy";
|
|
@@ -106,6 +108,15 @@ export function taskEvaluationPath(taskId, evaluationId) {
|
|
|
106
108
|
return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.evaluations}/${evaluationId}.json`;
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
export function taskDecisionDirectory(taskId) {
|
|
112
|
+
return taskArtifactPath(taskId, "decisions");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function taskDecisionPath(taskId, decisionId) {
|
|
116
|
+
assertArtifactId(decisionId, /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/, "decision ID");
|
|
117
|
+
return `${taskDecisionDirectory(taskId)}/${decisionId}.json`;
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
function assertArtifactId(value, pattern, label) {
|
|
110
121
|
if (typeof value !== "string" || !pattern.test(value)) {
|
|
111
122
|
throw new Error(`Invalid ${label}: ${value}`);
|
|
@@ -217,6 +228,8 @@ export function buildTaskArtifactPaths(taskId) {
|
|
|
217
228
|
actions: `${dir}/${TASK_ARTIFACT_FILES.actions}`,
|
|
218
229
|
approvals: `${dir}/${TASK_ARTIFACT_FILES.approvals}`,
|
|
219
230
|
evaluations: `${dir}/${TASK_ARTIFACT_FILES.evaluations}`,
|
|
231
|
+
decisions: `${dir}/${TASK_ARTIFACT_FILES.decisions}`,
|
|
232
|
+
testUtility: `${dir}/${TASK_ARTIFACT_FILES.testUtility}`,
|
|
220
233
|
usage: `${dir}/${TASK_ARTIFACT_FILES.usage}`,
|
|
221
234
|
lock: taskLockPath(taskId),
|
|
222
235
|
policySnapshot: `${dir}/${TASK_ARTIFACT_FILES.policySnapshot}`,
|
package/src/core/templates.js
CHANGED
|
@@ -88,6 +88,9 @@ export const TEMPLATE_PATHS = [
|
|
|
88
88
|
"schemas/execution-profile-benchmark-run.schema.json",
|
|
89
89
|
"schemas/execution-profile-benchmark-aggregate.schema.json",
|
|
90
90
|
"schemas/structural-quality.schema.json",
|
|
91
|
+
"schemas/semantic-decision.schema.json",
|
|
92
|
+
"schemas/context-plan.schema.json",
|
|
93
|
+
"schemas/test-utility.schema.json",
|
|
91
94
|
];
|
|
92
95
|
|
|
93
96
|
export function getPackageRoot() {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "../artifacts.js";
|
|
2
|
+
import { buildTestUtilityArtifact } from "./utility.js";
|
|
3
|
+
|
|
4
|
+
const FIXTURE_TESTS = Object.freeze([
|
|
5
|
+
{ testId: "protected-security", file: "tests/security.test.js", framework: "node:test", suite: "", name: "security", line: 1, signals: { security: true } },
|
|
6
|
+
{ testId: "protected-public-api", file: "tests/api.test.js", framework: "node:test", suite: "", name: "api", line: 1, signals: { publicApi: true } },
|
|
7
|
+
{ testId: "semantic-duplicate", file: "tests/duplicate.test.js", framework: "node:test", suite: "", name: "duplicate", line: 1, signals: {} },
|
|
8
|
+
{ testId: "unique-branch", file: "tests/required.test.js", framework: "node:test", suite: "", name: "required", line: 1, signals: { requiredBehavior: true } },
|
|
9
|
+
{ testId: "obsolete-test", file: "tests/obsolete.test.js", framework: "node:test", suite: "", name: "obsolete", line: 1, signals: {} },
|
|
10
|
+
{ testId: "slow-required", file: "tests/critical.test.js", framework: "node:test", suite: "", name: "critical", line: 1, signals: { criticalPath: true } },
|
|
11
|
+
{ testId: "flaky-low-signal", file: "tests/flaky.test.js", framework: "node:test", suite: "", name: "flaky", line: 1, signals: {} },
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const CASES = Object.freeze([
|
|
15
|
+
{ id: "protected-security", expected: "KEEP_RISK_GUARD", protected: true },
|
|
16
|
+
{ id: "protected-public-api", expected: "KEEP_UNIQUE", protected: true },
|
|
17
|
+
{ id: "semantic-duplicate", expected: "REDUNDANT_CANDIDATE", protected: false },
|
|
18
|
+
{ id: "unique-branch", expected: "KEEP_REQUIRED", protected: true },
|
|
19
|
+
{ id: "obsolete-test", expected: "OBSOLETE_CANDIDATE", protected: false },
|
|
20
|
+
{ id: "slow-required", expected: "KEEP_RECOVERY_INVARIANT", protected: true },
|
|
21
|
+
{ id: "flaky-low-signal", expected: "FLAKY_LOW_SIGNAL", protected: false },
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function precision(cases, label) {
|
|
25
|
+
const predicted = cases.filter((item) => item.observed === label);
|
|
26
|
+
if (predicted.length === 0) return null;
|
|
27
|
+
return predicted.filter((item) => item.expected === label).length / predicted.length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function recall(cases, label) {
|
|
31
|
+
const expected = cases.filter((item) => item.expected === label);
|
|
32
|
+
if (expected.length === 0) return null;
|
|
33
|
+
return expected.filter((item) => item.observed === label).length / expected.length;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function runTestIntelligenceBenchmark() {
|
|
37
|
+
const candidateIds = ["semantic-duplicate", "obsolete-test", "flaky-low-signal"];
|
|
38
|
+
const decisions = { candidateIds, tests: {
|
|
39
|
+
"semantic-duplicate": { semantic_duplicate: true },
|
|
40
|
+
"obsolete-test": { likely_obsolete: true },
|
|
41
|
+
"flaky-low-signal": { likely_flaky_low_signal: true },
|
|
42
|
+
} };
|
|
43
|
+
const confidence = Object.fromEntries(
|
|
44
|
+
candidateIds.flatMap((id, index) => (
|
|
45
|
+
Object.keys(decisions.tests[id]).map((dimension) => [`test_${index}_${dimension}`, 0.95])
|
|
46
|
+
)),
|
|
47
|
+
);
|
|
48
|
+
const observedTests = buildTestUtilityArtifact({ taskId: "benchmark", inventory: { schemaVersion: 1, source: "FIXTURE", tests: FIXTURE_TESTS }, semanticDecision: { decisionId: "benchmark-decision", decision: decisions, confidence } }).tests;
|
|
49
|
+
const cases = CASES.map((item) => ({ ...item, observed: observedTests.find((test) => test.testId === item.id)?.classification ?? "UNKNOWN" }));
|
|
50
|
+
const protectedFalsePositives = cases.filter((item) => item.protected && ["REDUNDANT_CANDIDATE", "OBSOLETE_CANDIDATE", "SAFE_TO_REMOVE"].includes(item.observed)).length;
|
|
51
|
+
const summary = {
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
status: "OFFLINE_DETERMINISTIC_FIXTURE",
|
|
54
|
+
caseCount: CASES.length,
|
|
55
|
+
metrics: {
|
|
56
|
+
redundantCandidatePrecision: precision(cases, "REDUNDANT_CANDIDATE"),
|
|
57
|
+
redundantCandidateRecall: recall(cases, "REDUNDANT_CANDIDATE"),
|
|
58
|
+
obsoleteCandidatePrecision: precision(cases, "OBSOLETE_CANDIDATE"),
|
|
59
|
+
obsoleteCandidateRecall: recall(cases, "OBSOLETE_CANDIDATE"),
|
|
60
|
+
protectedTestFalsePositiveRate: protectedFalsePositives / CASES.filter((item) => item.protected).length,
|
|
61
|
+
safeToRemoveFalsePositiveRate: precision(cases, "SAFE_TO_REMOVE") === null ? null : 1 - precision(cases, "SAFE_TO_REMOVE"),
|
|
62
|
+
rewriteRecommendationPrecision: null,
|
|
63
|
+
},
|
|
64
|
+
deletionAuthority: false,
|
|
65
|
+
cases,
|
|
66
|
+
};
|
|
67
|
+
return { ...summary, fingerprint: canonicalFingerprint(summary) };
|
|
68
|
+
}
|