@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,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));
|