@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.
Files changed (211) hide show
  1. package/AGENT_COMPATIBILITY.md +8 -0
  2. package/DOCS_INDEX.md +35 -3
  3. package/ENG/nodejs-backend-development-eng.md +2 -2
  4. package/ENG/sec-code-eng.md +7 -7
  5. package/EXECUTION_STATE.md +12 -0
  6. package/LOOP_ENGINEERING.md +28 -2
  7. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  8. package/PROTOCOL_INTEGRATION.md +55 -2
  9. package/README.md +40 -25
  10. package/TERMINOLOGY.md +2 -0
  11. package/THREAT_MODEL.md +140 -1
  12. package/completions/_forgeloop +19 -1
  13. package/completions/forgeloop.bash +37 -1
  14. package/completions/forgeloop.fish +123 -1
  15. package/docs/ADVISORY_CONTEXT.md +25 -0
  16. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  17. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  18. package/docs/AGENT_PROTOCOL_SUMMARY.md +27 -2
  19. package/docs/AGENT_SKILL.md +66 -0
  20. package/docs/ARTIFACT_REFERENCE.md +123 -0
  21. package/docs/AUDIT_UX.md +46 -0
  22. package/docs/BROWSER_VERIFICATION.md +136 -0
  23. package/docs/CLI_REFERENCE.md +366 -6
  24. package/docs/CODE_ATTESTATION.md +2 -2
  25. package/docs/DOCUMENTATION_GUIDE.md +32 -11
  26. package/docs/JEV_BENCHMARKS.md +31 -0
  27. package/docs/MODEL_ROUTING.md +37 -0
  28. package/docs/OPENSRC_ADAPTER.md +241 -0
  29. package/docs/PACKAGE_CONTENTS.md +35 -8
  30. package/docs/PROVIDERS.md +126 -0
  31. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  32. package/docs/RECIPES.md +9 -0
  33. package/docs/RELEASE_CHECKLIST.md +38 -5
  34. package/docs/SECURITY_REVIEW.md +71 -0
  35. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  36. package/docs/TEST_INTELLIGENCE.md +29 -0
  37. package/docs/TEST_PRUNING.md +14 -0
  38. package/docs/TROUBLESHOOTING.md +198 -1
  39. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  40. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  41. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  42. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  43. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  44. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  45. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  46. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  47. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  48. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  49. package/docs/diagrams/README.md +13 -9
  50. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  51. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  52. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  53. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  54. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  55. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  56. package/docs/documentation-manifest.json +750 -5
  57. package/docs/protocol-requirements.json +24 -0
  58. package/package.json +30 -3
  59. package/schemas/config.schema.json +14 -0
  60. package/schemas/context-plan.schema.json +18 -0
  61. package/schemas/semantic-decision.schema.json +46 -0
  62. package/schemas/test-utility.schema.json +44 -0
  63. package/scripts/CI_VALIDATORS.md +6 -6
  64. package/scripts/benchmark-jev.mjs +5 -0
  65. package/scripts/benchmark-test-intelligence.mjs +4 -0
  66. package/scripts/generate-agent-protocol-summary.mjs +4 -1
  67. package/scripts/generate-forgeloop-skill.mjs +133 -0
  68. package/scripts/jev-smoke.mjs +19 -0
  69. package/skills/forgeloop/README.md +9 -0
  70. package/skills/forgeloop/SKILL.md +77 -0
  71. package/skills/forgeloop/references/lifecycle.md +9 -0
  72. package/skills/forgeloop/references/recovery.md +7 -0
  73. package/skills/forgeloop/references/verification.md +7 -0
  74. package/src/adapters/agent-browser/assertions.js +47 -0
  75. package/src/adapters/agent-browser/commands.js +54 -0
  76. package/src/adapters/agent-browser/index.js +3 -0
  77. package/src/adapters/agent-browser/locator.js +40 -0
  78. package/src/adapters/agent-browser/process.js +215 -0
  79. package/src/adapters/agent-browser/provider.js +313 -0
  80. package/src/adapters/emulated-services/constants.js +24 -0
  81. package/src/adapters/emulated-services/index.js +7 -0
  82. package/src/adapters/emulated-services/process.js +162 -0
  83. package/src/adapters/emulated-services/provider.js +282 -0
  84. package/src/adapters/opensrc/normalize.js +90 -0
  85. package/src/adapters/opensrc/process.js +248 -0
  86. package/src/adapters/opensrc/provider.js +338 -0
  87. package/src/adapters/opensrc/search.js +264 -0
  88. package/src/adapters/typesafe/client.js +28 -0
  89. package/src/adapters/typesafe/engine.js +63 -0
  90. package/src/adapters/typesafe/normalize.js +41 -0
  91. package/src/cli.js +108 -0
  92. package/src/commands/checkpoint-revalidate.js +176 -0
  93. package/src/commands/context-plan.js +38 -0
  94. package/src/commands/contract-create.js +264 -0
  95. package/src/commands/contract-revise.js +236 -0
  96. package/src/commands/decision-show.js +14 -0
  97. package/src/commands/decision-status.js +22 -0
  98. package/src/commands/discover.js +41 -0
  99. package/src/commands/doctor.js +15 -0
  100. package/src/commands/gate-record.js +205 -0
  101. package/src/commands/gate-revalidate.js +137 -0
  102. package/src/commands/model-route.js +32 -0
  103. package/src/commands/route.js +146 -18
  104. package/src/commands/semantic-plan.js +17 -0
  105. package/src/commands/task-abandon.js +224 -0
  106. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  107. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  108. package/src/commands/test-inventory.js +5 -0
  109. package/src/commands/test-prune-plan.js +5 -0
  110. package/src/commands/test-prune-probe.js +5 -0
  111. package/src/commands/test-utility.js +5 -0
  112. package/src/commands/validate-protocol.js +10 -1
  113. package/src/core/artifact-registry.js +24 -0
  114. package/src/core/audit-ux.js +514 -0
  115. package/src/core/browser-verification/constants.js +149 -0
  116. package/src/core/browser-verification/normalize.js +254 -0
  117. package/src/core/browser-verification/provider.js +519 -0
  118. package/src/core/browser-verification/service.js +115 -0
  119. package/src/core/checkpoint-revalidation.js +319 -0
  120. package/src/core/cli-command-definitions.js +241 -0
  121. package/src/core/command-executors.js +110 -0
  122. package/src/core/command-input.js +115 -43
  123. package/src/core/completion-artifacts.js +14 -5
  124. package/src/core/completion.js +4 -6
  125. package/src/core/config.js +3 -0
  126. package/src/core/context-compiler/budget.js +9 -0
  127. package/src/core/context-compiler/candidates.js +39 -0
  128. package/src/core/context-compiler/compiler.js +63 -0
  129. package/src/core/context-compiler/fingerprint.js +11 -0
  130. package/src/core/context-compiler/policy.js +13 -0
  131. package/src/core/context-compiler/result.js +23 -0
  132. package/src/core/contract-bootstrap-recovery.js +655 -0
  133. package/src/core/contract-revision.js +210 -0
  134. package/src/core/decision/artifact.js +69 -0
  135. package/src/core/decision/benchmarks.js +103 -0
  136. package/src/core/decision/cache.js +27 -0
  137. package/src/core/decision/constants.js +58 -0
  138. package/src/core/decision/cutover.js +34 -0
  139. package/src/core/decision/engine.js +22 -0
  140. package/src/core/decision/errors.js +68 -0
  141. package/src/core/decision/events.js +101 -0
  142. package/src/core/decision/freshness.js +19 -0
  143. package/src/core/decision/normalizers/index.js +115 -0
  144. package/src/core/decision/policy.js +18 -0
  145. package/src/core/decision/projection.js +16 -0
  146. package/src/core/decision/question-registry.js +201 -0
  147. package/src/core/decision/request.js +26 -0
  148. package/src/core/decision/resolver.js +130 -0
  149. package/src/core/decision/result.js +58 -0
  150. package/src/core/decision/service.js +156 -0
  151. package/src/core/decision/state-builder.js +65 -0
  152. package/src/core/decision/task-bindings.js +30 -0
  153. package/src/core/decision/test-provider.js +32 -0
  154. package/src/core/decision/thresholds.js +15 -0
  155. package/src/core/error-codes.js +278 -0
  156. package/src/core/events.js +226 -57
  157. package/src/core/evidence-readiness.js +9 -0
  158. package/src/core/execution-prerequisites.js +14 -0
  159. package/src/core/execution-profile.js +63 -38
  160. package/src/core/gate-provenance.js +124 -0
  161. package/src/core/integration-invocation-policy.js +27 -4
  162. package/src/core/integration-resources.js +86 -61
  163. package/src/core/model-router/constants.js +10 -0
  164. package/src/core/model-router/policy.js +103 -0
  165. package/src/core/model-router/router.js +37 -0
  166. package/src/core/next-action-model.js +58 -0
  167. package/src/core/next-action-phases.js +130 -42
  168. package/src/core/next-action-refresh.js +43 -9
  169. package/src/core/next-action-review-phase.js +7 -2
  170. package/src/core/next-action.js +35 -7
  171. package/src/core/phase.js +128 -10
  172. package/src/core/preflight-consistency.js +23 -9
  173. package/src/core/preflight-loaders.js +37 -5
  174. package/src/core/protocol-info.js +65 -0
  175. package/src/core/protocol.js +20 -0
  176. package/src/core/reconcile-closure.js +128 -52
  177. package/src/core/recovery-history.js +1 -0
  178. package/src/core/resumability.js +154 -44
  179. package/src/core/route-artifact.js +15 -1
  180. package/src/core/router.js +67 -1
  181. package/src/core/runtime-context.js +118 -61
  182. package/src/core/schema-validation.js +3 -0
  183. package/src/core/security-review/constants.js +64 -0
  184. package/src/core/security-review/normalize.js +245 -0
  185. package/src/core/security-review/provider.js +204 -0
  186. package/src/core/security-review/service.js +134 -0
  187. package/src/core/semantic-planning/constants.js +19 -0
  188. package/src/core/semantic-planning/projection.js +94 -0
  189. package/src/core/semantic-planning/service.js +15 -0
  190. package/src/core/sources.js +37 -0
  191. package/src/core/task-claim-state.js +201 -1
  192. package/src/core/task-conflict-inspection.js +31 -5
  193. package/src/core/task-paths.js +13 -0
  194. package/src/core/task-recovery.js +1 -0
  195. package/src/core/templates.js +3 -0
  196. package/src/core/test-intelligence/benchmarks.js +68 -0
  197. package/src/core/test-intelligence/inventory.js +73 -0
  198. package/src/core/test-intelligence/prune.js +90 -0
  199. package/src/core/test-intelligence/semantic-state.js +15 -0
  200. package/src/core/test-intelligence/service.js +40 -0
  201. package/src/core/test-intelligence/utility.js +50 -0
  202. package/src/core/trace.js +11 -7
  203. package/src/core/transaction.js +1 -0
  204. package/src/integration.d.ts +492 -0
  205. package/src/integration.js +54 -0
  206. package/src/providers/README.md +47 -0
  207. package/src/providers/capabilities.js +46 -0
  208. package/src/providers/errors.js +15 -0
  209. package/src/providers/index.js +29 -0
  210. package/src/providers/json-snapshot.js +105 -0
  211. package/src/providers/registry.js +152 -0
@@ -0,0 +1,73 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ const TEST_DIRECTORIES = Object.freeze(["test", "tests", "spec", "specs"]);
6
+ const TEST_FILE_PATTERN = /\.(?:test|spec)\.(?:c|cpp|go|java|js|jsx|mjs|py|rb|rs|ts|tsx)$/i;
7
+
8
+ async function filesUnder(root, relative = "") {
9
+ const current = path.join(root, relative);
10
+ let entries = [];
11
+ try { entries = await readdir(current, { withFileTypes: true }); } catch { return []; }
12
+ const result = [];
13
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
14
+ const child = path.join(relative, entry.name);
15
+ if (entry.isDirectory()) result.push(...await filesUnder(root, child));
16
+ else if (entry.isFile() && TEST_FILE_PATTERN.test(entry.name)) result.push(child);
17
+ }
18
+ return result;
19
+ }
20
+
21
+ function stableId(framework, file, suite, name) {
22
+ return `test-${createHash("sha256").update(JSON.stringify({ framework, file, suite, name })).digest("hex").slice(0, 24)}`;
23
+ }
24
+
25
+ function frameworkFor(source) {
26
+ if (/node:test/.test(source)) return "node:test";
27
+ if (/vitest/.test(source)) return "vitest";
28
+ if (/jest/.test(source)) return "jest";
29
+ if (/pytest|unittest/.test(source)) return "python";
30
+ return "unknown";
31
+ }
32
+
33
+ function collectUnits(source) {
34
+ const units = [];
35
+ const matcher = /\b(test|it)\s*\(\s*(["'`])([^"'`]+)\2/g;
36
+ let match;
37
+ while ((match = matcher.exec(source))) units.push({ name: match[3].trim(), line: source.slice(0, match.index).split("\n").length });
38
+ return units;
39
+ }
40
+
41
+ function signals(file, name) {
42
+ const text = `${file} ${name}`.toLowerCase();
43
+ return {
44
+ requiredBehavior: /contract|acceptance|required|lifecycle|protocol/.test(text),
45
+ security: /security|auth|secret|permission|credential|csp|injection/.test(text),
46
+ publicApi: /api|compat|public|cli|protocol/.test(text),
47
+ criticalPath: /critical|transaction|lock|concurr|race|recovery/.test(text),
48
+ integration: /integration|e2e|browser|provider|package|release/.test(text),
49
+ migration: /migration|schema|upgrade|downgrade/.test(text),
50
+ release: /package|release|publish|smoke/.test(text),
51
+ platformSpecific: /windows|win32|macos|darwin|linux|platform|native/.test(text),
52
+ };
53
+ }
54
+
55
+ export async function inventoryTests(projectRoot = process.cwd()) {
56
+ const files = [];
57
+ for (const directory of TEST_DIRECTORIES) files.push(...await filesUnder(projectRoot, directory));
58
+ const tests = [];
59
+ for (const relativeFile of [...new Set(files)].sort()) {
60
+ const source = await readFile(path.join(projectRoot, relativeFile), "utf8");
61
+ const framework = frameworkFor(source);
62
+ for (const unit of collectUnits(source)) {
63
+ const file = relativeFile.replaceAll(path.sep, "/");
64
+ tests.push({
65
+ testId: stableId(framework, file, "", unit.name), file, framework, suite: "", name: unit.name,
66
+ line: unit.line, sourceSummary: `${file}:${unit.line} ${unit.name}`.slice(0, 500),
67
+ targets: [file], runtimeMs: null, uniqueBranches: null, signals: signals(file, unit.name),
68
+ });
69
+ }
70
+ }
71
+ tests.sort((a, b) => a.testId.localeCompare(b.testId));
72
+ return { schemaVersion: 1, source: "DETERMINISTIC", tests };
73
+ }
@@ -0,0 +1,90 @@
1
+ import { readTestUtility } from "./service.js";
2
+ import { cp, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+ const PROTECTED_CLASSES = new Set([
10
+ "KEEP_REQUIRED", "KEEP_RISK_GUARD", "KEEP_UNIQUE", "KEEP_AUTHORITY_BOUNDARY",
11
+ "KEEP_RECOVERY_INVARIANT", "KEEP_RELEASE_SMOKE", "KEEP_MIGRATION_COMPATIBILITY",
12
+ "KEEP_PLATFORM_BEHAVIOR",
13
+ ]);
14
+
15
+ function planFor(test) {
16
+ if (test.protected || PROTECTED_CLASSES.has(test.classification)) return { action: "KEEP", reason: "PROTECTED_TEST" };
17
+ if (test.classification === "REDUNDANT_CANDIDATE" && test.recommendation === "PROBE_REMOVAL") return { action: "PROBE_REMOVAL", reason: "SEMANTIC_AND_DETERMINISTIC_EVIDENCE_REQUIRED" };
18
+ if (test.classification === "KEEP_DOCUMENTATION_VALUE") return { action: "KEEP", reason: "DOCUMENTATION_VALUE" };
19
+ return { action: "BLOCKED", reason: "INSUFFICIENT_PROVENANCE" };
20
+ }
21
+
22
+ export async function buildPrunePlan({ target, packageRoot, taskId } = {}) {
23
+ const utility = await readTestUtility({ target, packageRoot, taskId });
24
+ const items = utility.value.tests.map((test) => ({ testId: test.testId, file: test.file, name: test.name, ...planFor(test) }));
25
+ return { schemaVersion: 1, taskId, semanticStatus: utility.value.semanticStatus, items, deletionAuthority: false };
26
+ }
27
+
28
+ export async function runPruneProbe({ target, packageRoot, taskId, testId } = {}) {
29
+ const plan = await buildPrunePlan({ target, packageRoot, taskId });
30
+ const item = plan.items.find((candidate) => candidate.testId === testId);
31
+ if (!item) {
32
+ const error = new Error(`Unknown test ID: ${testId}`); error.code = "E_TEST_ID_UNKNOWN"; throw error;
33
+ }
34
+ if (item.action !== "PROBE_REMOVAL") {
35
+ const error = new Error(`Test prune probe blocked for ${testId}: ${item.reason}`);
36
+ error.code = "E_TEST_PRUNE_BLOCKED";
37
+ throw error;
38
+ }
39
+ const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), "forgeloop-prune-probe-"));
40
+ try {
41
+ await cp(target, temporaryRoot, {
42
+ recursive: true,
43
+ dereference: false,
44
+ filter(source) {
45
+ const relative = path.relative(target, source);
46
+ return relative !== ".forgeloop" && relative !== ".git" && !relative.startsWith(`${path.sep}.forgeloop${path.sep}`) && !relative.startsWith(`${path.sep}.git${path.sep}`) && relative !== "node_modules";
47
+ },
48
+ });
49
+ const dependencies = path.join(target, "node_modules");
50
+ try { await symlink(dependencies, path.join(temporaryRoot, "node_modules"), "junction"); } catch { /* dependencies are optional for static probes */ }
51
+ if (item.file.endsWith(".mjs") || item.file.endsWith(".js")) {
52
+ const candidatePath = path.join(temporaryRoot, item.file);
53
+ const source = await readFile(candidatePath, "utf8");
54
+ const escapedName = item.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
+ const pattern = new RegExp(`\\b(test|it)\\s*\\(\\s*([\\"'])${escapedName}\\2`);
56
+ if (!pattern.test(source)) {
57
+ return { status: "INCONCLUSIVE", reason: "TEST_UNIT_NOT_LOCATED", testId, liveWorktreeModified: false, temporaryWorkspaceCleaned: true };
58
+ }
59
+ const disabled = source.replace(pattern, (_, keyword, quote) => `${keyword}.skip(${quote}${item.name}${quote}`);
60
+ await writeFile(candidatePath, disabled, "utf8");
61
+ const result = await execFileAsync(process.execPath, ["--test", item.file], {
62
+ cwd: temporaryRoot,
63
+ timeout: 120_000,
64
+ maxBuffer: 2_000_000,
65
+ windowsHide: true,
66
+ });
67
+ return {
68
+ status: "INCONCLUSIVE",
69
+ reason: "COVERAGE_OR_MUTATION_EVIDENCE_UNAVAILABLE",
70
+ testId,
71
+ affectedTestExitCode: 0,
72
+ affectedTestOutput: `${result.stdout ?? ""}${result.stderr ?? ""}`.slice(-8_000),
73
+ liveWorktreeModified: false,
74
+ temporaryWorkspaceCleaned: true,
75
+ };
76
+ }
77
+ return { status: "INCONCLUSIVE", reason: "FRAMEWORK_PROBE_UNSUPPORTED", testId, liveWorktreeModified: false, temporaryWorkspaceCleaned: true };
78
+ } catch (error) {
79
+ return {
80
+ status: "BLOCKED",
81
+ reason: error.code === "ETIMEDOUT" ? "AFFECTED_TEST_TIMEOUT" : "AFFECTED_TEST_FAILED",
82
+ testId,
83
+ error: error.message,
84
+ liveWorktreeModified: false,
85
+ temporaryWorkspaceCleaned: true,
86
+ };
87
+ } finally {
88
+ await rm(temporaryRoot, { recursive: true, force: true });
89
+ }
90
+ }
@@ -0,0 +1,15 @@
1
+ export const TEST_UTILITY_BATCH_SIZE = 8;
2
+
3
+ export function batchTests(tests = [], size = TEST_UTILITY_BATCH_SIZE) {
4
+ const batches = [];
5
+ for (let index = 0; index < tests.length; index += size) batches.push(tests.slice(index, index + size));
6
+ return batches;
7
+ }
8
+
9
+ export function buildTestSemanticState(tests = []) {
10
+ return {
11
+ tests: tests.map(({ testId, file, framework, suite, name, line, sourceSummary, targets, runtimeMs, uniqueBranches, signals }) => ({
12
+ testId, file, framework, suite, name, line, sourceSummary, targets, runtimeMs, uniqueBranches, signals,
13
+ })),
14
+ };
15
+ }
@@ -0,0 +1,40 @@
1
+ import { taskArtifactPath } from "../task-paths.js";
2
+ import { writeJsonArtifact, readJsonArtifact } from "../artifacts.js";
3
+ import { withTaskMutation } from "../task-command.js";
4
+ import { inventoryTests } from "./inventory.js";
5
+ import { buildTestUtilityArtifact } from "./utility.js";
6
+ import { canonicalFingerprint } from "../artifacts.js";
7
+ import { readWorkState } from "../work-state.js";
8
+ import { buildTestUtilityQuestionSet } from "../decision/question-registry.js";
9
+ import { ensureSemanticDecision } from "../decision/service.js";
10
+ import { batchTests, buildTestSemanticState } from "./semantic-state.js";
11
+
12
+ export async function runTestInventory({ projectRoot = process.cwd() } = {}) { return inventoryTests(projectRoot); }
13
+
14
+ export async function runTestUtility({ target, packageRoot, taskId, provider = null } = {}) {
15
+ const inventory = await inventoryTests(target);
16
+ const semanticDecisions = [];
17
+ const state = await readWorkState(target, { packageRoot, taskId });
18
+ for (const [index, batch] of batchTests(inventory.tests).entries()) {
19
+ const questionSet = buildTestUtilityQuestionSet(batch);
20
+ semanticDecisions.push(await ensureSemanticDecision({
21
+ target, packageRoot, taskId, provider,
22
+ decisionId: `test-utility-${index}-${canonicalFingerprint(batch.map((test) => test.testId)).slice(0, 16)}`,
23
+ request: {
24
+ decisionKind: "TEST_UTILITY", questionSetId: questionSet.id, questionSet,
25
+ state: { lifecycle: state, ...buildTestSemanticState(batch) },
26
+ candidateIds: batch.map((test) => test.testId),
27
+ candidateSetFingerprint: canonicalFingerprint(batch.map((test) => test.testId)),
28
+ },
29
+ }));
30
+ }
31
+ return withTaskMutation(target, { taskId, packageRoot }, "test-utility", async (ctx) => {
32
+ const artifact = buildTestUtilityArtifact({ taskId: ctx.taskId, inventory, semanticDecisions: semanticDecisions.map((decision) => decision.artifact) });
33
+ const written = await writeJsonArtifact(target, taskArtifactPath(ctx.taskId, "testUtility"), artifact, "test-utility", packageRoot, { taskId: ctx.taskId, operation: "test-utility" });
34
+ return { path: written.path, artifact, inventory };
35
+ });
36
+ }
37
+
38
+ export async function readTestUtility({ target, packageRoot, taskId } = {}) {
39
+ return readJsonArtifact(target, taskArtifactPath(taskId, "testUtility"), "test-utility", packageRoot);
40
+ }
@@ -0,0 +1,50 @@
1
+ import { canonicalFingerprint } from "../artifacts.js";
2
+
3
+ function classify(test, judgment = null, confidence = 1) {
4
+ const signal = test.signals ?? {};
5
+ if (signal.requiredBehavior) return { classification: "KEEP_REQUIRED", recommendation: "KEEP", protected: true };
6
+ if (signal.security) return { classification: "KEEP_RISK_GUARD", recommendation: "KEEP", protected: true };
7
+ if (signal.criticalPath) return { classification: "KEEP_RECOVERY_INVARIANT", recommendation: "KEEP", protected: true };
8
+ if (signal.migration) return { classification: "KEEP_MIGRATION_COMPATIBILITY", recommendation: "KEEP", protected: true };
9
+ if (signal.publicApi) return { classification: "KEEP_UNIQUE", recommendation: "KEEP", protected: true };
10
+ if (signal.release) return { classification: "KEEP_RELEASE_SMOKE", recommendation: "KEEP", protected: true };
11
+ if (signal.platformSpecific) return { classification: "KEEP_PLATFORM_BEHAVIOR", recommendation: "KEEP", protected: true };
12
+ if (signal.integration) return { classification: "KEEP_INTEGRATION_GUARD", recommendation: "KEEP", protected: true };
13
+ if (!judgment || confidence < 0.75) return { classification: "UNKNOWN", recommendation: "BLOCKED", protected: false };
14
+ if (judgment.protocol_invariant) return { classification: "KEEP_RECOVERY_INVARIANT", recommendation: "KEEP", protected: true };
15
+ if (judgment.semantic_duplicate) {
16
+ return {
17
+ classification: "REDUNDANT_CANDIDATE",
18
+ recommendation: judgment.requires_system_two_review ? "BLOCKED" : "PROBE_REMOVAL",
19
+ protected: false,
20
+ };
21
+ }
22
+ if (judgment.likely_obsolete) return { classification: "OBSOLETE_CANDIDATE", recommendation: "BLOCKED", protected: false };
23
+ if (judgment.likely_flaky_low_signal) return { classification: "FLAKY_LOW_SIGNAL", recommendation: "BLOCKED", protected: false };
24
+ if (judgment.expensive_relative_to_signal) return { classification: "EXPENSIVE_LOW_SIGNAL", recommendation: "BLOCKED", protected: false };
25
+ if (judgment.documentation_value) return { classification: "KEEP_DOCUMENTATION_VALUE", recommendation: "KEEP", protected: false };
26
+ if (judgment.integration_value) return { classification: "KEEP_INTEGRATION_GUARD", recommendation: "KEEP", protected: true };
27
+ if (judgment.has_unique_semantic_intent) return { classification: "KEEP_UNIQUE", recommendation: "KEEP", protected: false };
28
+ return { classification: "UNKNOWN", recommendation: "BLOCKED", protected: false };
29
+ }
30
+
31
+ export function buildTestUtilityArtifact({ taskId, inventory, semanticStatus = "NOT_REQUESTED", semanticDecision = null, semanticDecisions = [] } = {}) {
32
+ const decisions = semanticDecisions.length > 0 ? semanticDecisions : (semanticDecision ? [semanticDecision] : []);
33
+ const byTest = new Map(decisions.flatMap((decision) => Object.entries(decision.decision?.tests ?? {}).map(([testId, judgment]) => [testId, {
34
+ judgment, confidence: Math.min(...Object.keys(judgment).map((key) => decision.confidence?.[`test_${(decision.decision.candidateIds ?? []).indexOf(testId)}_${key}`] ?? 0)),
35
+ }])));
36
+ return {
37
+ schemaVersion: 1,
38
+ protocolVersion: 1,
39
+ taskId,
40
+ generatedAt: new Date().toISOString(),
41
+ inventoryFingerprint: canonicalFingerprint(inventory),
42
+ semanticStatus: decisions.length > 0 ? "PROVIDER_REPORTED" : semanticStatus,
43
+ ...(decisions.length > 0 ? {
44
+ ...(decisions.length === 1 ? { decisionId: decisions[0].decisionId, semanticDecisionFingerprint: canonicalFingerprint(decisions[0]) } : {}),
45
+ decisionIds: decisions.map((decision) => decision.decisionId),
46
+ semanticDecisionFingerprints: decisions.map((decision) => canonicalFingerprint(decision)),
47
+ } : {}),
48
+ tests: (inventory?.tests ?? []).map((test) => ({ ...test, ...classify(test, byTest.get(test.testId)?.judgment, byTest.get(test.testId)?.confidence ?? 0) })),
49
+ };
50
+ }
package/src/core/trace.js CHANGED
@@ -51,6 +51,7 @@ const EVENT_CATEGORY_MAP = Object.freeze({
51
51
  TASK_RECOVERY_RECORDED: "recovery",
52
52
  TASK_RECOVERY_RESUMED: "recovery",
53
53
  OPERATOR_RECOVERY_RECORDED: "recovery",
54
+ TASK_ABANDONED: "recovery",
54
55
  LEGACY_RECOVERY_MIGRATION_RECORDED: "recovery",
55
56
  GATE_SATISFIED: "policy",
56
57
  COMPLETION_VALIDATED: "completion",
@@ -82,6 +83,13 @@ const LIFECYCLE_TRANSITIONS = Object.freeze([
82
83
  "COMPLETION_REJECTED",
83
84
  ]);
84
85
 
86
+ const STATIC_EVENT_SUMMARIES = Object.freeze({
87
+ TASK_RECOVERY_RESUMED: "Task resumed after recovery",
88
+ OPERATOR_RECOVERY_RECORDED: "Operator recovery recorded",
89
+ TASK_ABANDONED: "Task explicitly abandoned",
90
+ LEGACY_RECOVERY_MIGRATION_RECORDED: "Legacy recovery migration recorded",
91
+ });
92
+
85
93
  export function eventCategory(eventType) {
86
94
  return EVENT_CATEGORY_MAP[eventType] ?? "integrity";
87
95
  }
@@ -93,6 +101,8 @@ export function timestampQuality(event) {
93
101
 
94
102
  function eventSummary(event) {
95
103
  const d = event.details ?? {};
104
+ const staticSummary = STATIC_EVENT_SUMMARIES[event.event];
105
+ if (staticSummary) return staticSummary;
96
106
  switch (event.event) {
97
107
  case "TASK_RECEIVED":
98
108
  return `Task received${d.taskId ? ` (${d.taskId})` : ""}`;
@@ -132,12 +142,6 @@ function eventSummary(event) {
132
142
  return "Continuity checkpoint recorded";
133
143
  case "TASK_RECOVERY_RECORDED":
134
144
  return `Task recovery recorded (${d.classification ?? "unknown"})`;
135
- case "TASK_RECOVERY_RESUMED":
136
- return "Task resumed after recovery";
137
- case "OPERATOR_RECOVERY_RECORDED":
138
- return "Operator recovery recorded";
139
- case "LEGACY_RECOVERY_MIGRATION_RECORDED":
140
- return "Legacy recovery migration recorded";
141
145
  case "COMPLETION_VALIDATED":
142
146
  return "Completion validated";
143
147
  case "COMPLETION_REJECTED":
@@ -451,7 +455,7 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
451
455
  const evidence = [...evidenceSources.values()].sort((a, b) => a.ref.localeCompare(b.ref));
452
456
 
453
457
  const recovery = taskEvents
454
- .filter((event) => ["TASK_RECOVERY_RECORDED", "TASK_RECOVERY_RESUMED", "OPERATOR_RECOVERY_RECORDED", "LEGACY_RECOVERY_MIGRATION_RECORDED"].includes(event.event))
458
+ .filter((event) => ["TASK_RECOVERY_RECORDED", "TASK_RECOVERY_RESUMED", "OPERATOR_RECOVERY_RECORDED", "TASK_ABANDONED", "LEGACY_RECOVERY_MIGRATION_RECORDED"].includes(event.event))
455
459
  .map((event) => ({ sequence: event.seq, at: event.at, type: event.event, details: event.details ?? {} }));
456
460
 
457
461
  const completionEvents = taskEvents.filter((event) => ["COMPLETION_VALIDATED", "COMPLETION_REJECTED"].includes(event.event));
@@ -142,6 +142,7 @@ export async function withTaskTransaction({
142
142
  target,
143
143
  transactionId,
144
144
  taskId,
145
+ operation,
145
146
  lock,
146
147
  async readText(relativePath) {
147
148
  await assertSafePath(target, relativePath);