@cassiomc1/forgeloop 1.12.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/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/AGENT_COMPATIBILITY.md +8 -0
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +46 -12
- package/ENG/c-development-eng.md +112 -0
- package/ENG/cpp-development-eng.md +109 -0
- package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
- package/ENG/go-development-eng.md +103 -0
- package/ENG/java-development-eng.md +125 -0
- package/ENG/nodejs-backend-development-eng.md +605 -0
- package/ENG/php-development-eng.md +104 -0
- package/ENG/rust-development-eng.md +422 -0
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +418 -9
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +78 -52
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +22 -4
- package/completions/forgeloop.bash +40 -4
- package/completions/forgeloop.fish +130 -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 +81 -3
- 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 +392 -10
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -12
- package/docs/GETTING_STARTED.md +59 -0
- 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 +60 -19
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +32 -0
- package/docs/RELEASE_CHECKLIST.md +66 -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 +298 -3
- 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 +1397 -0
- package/docs/protocol-requirements.json +101 -0
- package/package.json +46 -4
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/routing-input.schema.json +1 -1
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +40 -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/next.js +19 -7
- 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-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- 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/config/guides.json +44 -0
- 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/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +249 -1
- package/src/core/command-executors.js +115 -3
- package/src/core/command-input.js +212 -102
- 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-presets.js +82 -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 +281 -3
- 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/filesystem.js +1 -10
- package/src/core/gate-provenance.js +124 -0
- package/src/core/go-project.js +206 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/java-project.js +403 -0
- 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/multi-language-project.js +117 -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/next-explanation.js +63 -0
- package/src/core/phase.js +128 -10
- package/src/core/php-project.js +85 -0
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/project-detection.js +1760 -52
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +132 -53
- 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 +223 -4
- package/src/core/runtime-context.js +118 -61
- package/src/core/rust-project.js +400 -0
- 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/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -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/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -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,68 @@
|
|
|
1
|
+
export const DECISION_ERROR_CODES = Object.freeze({
|
|
2
|
+
ENGINE_UNAVAILABLE: "E_DECISION_ENGINE_UNAVAILABLE",
|
|
3
|
+
AUTH_REQUIRED: "E_DECISION_ENGINE_AUTH_REQUIRED",
|
|
4
|
+
AUTH_INVALID: "E_DECISION_ENGINE_AUTH_INVALID",
|
|
5
|
+
MODEL_UNSUPPORTED: "E_DECISION_MODEL_UNSUPPORTED",
|
|
6
|
+
REQUEST_INVALID: "E_DECISION_REQUEST_INVALID",
|
|
7
|
+
RESULT_INVALID: "E_DECISION_RESULT_INVALID",
|
|
8
|
+
TIMEOUT: "E_DECISION_TIMEOUT",
|
|
9
|
+
RATE_LIMITED: "E_DECISION_RATE_LIMITED",
|
|
10
|
+
STATE_UNSAFE: "E_DECISION_STATE_UNSAFE",
|
|
11
|
+
STATE_LIMIT: "E_DECISION_STATE_LIMIT",
|
|
12
|
+
LOW_CONFIDENCE: "E_DECISION_LOW_CONFIDENCE",
|
|
13
|
+
STALE: "E_DECISION_STALE",
|
|
14
|
+
POLICY_INVALID: "E_DECISION_POLICY_INVALID",
|
|
15
|
+
CACHE_INVALID: "E_DECISION_CACHE_INVALID",
|
|
16
|
+
QUESTION_SET_UNKNOWN: "E_DECISION_QUESTION_SET_UNKNOWN",
|
|
17
|
+
QUESTION_SET_STALE: "E_DECISION_QUESTION_SET_STALE",
|
|
18
|
+
REQUIRED: "E_DECISION_REQUIRED",
|
|
19
|
+
IMMUTABLE: "E_DECISION_IMMUTABLE",
|
|
20
|
+
LEDGER_INVALID: "E_DECISION_LEDGER_INVALID",
|
|
21
|
+
BINDING_INVALID: "E_DECISION_BINDING_INVALID",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export class DecisionError extends Error {
|
|
25
|
+
constructor(code, message, artifacts = []) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "DecisionError";
|
|
28
|
+
this.code = code;
|
|
29
|
+
this.artifacts = artifacts;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function decisionError(code, message, artifacts = []) {
|
|
34
|
+
return new DecisionError(code, message, artifacts);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const NETWORK_ERROR_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "EPIPE", "EHOSTUNREACH", "ENETUNREACH"]);
|
|
38
|
+
|
|
39
|
+
export function classifyProviderFailure(error) {
|
|
40
|
+
const status = Number(error?.status);
|
|
41
|
+
const detail = error?.body?.detail;
|
|
42
|
+
const providerErrorType = typeof detail?.error_type === "string" ? detail.error_type : null;
|
|
43
|
+
const requestId = typeof error?.requestId === "string" && /^[A-Za-z0-9_-]{1,64}$/u.test(error.requestId) ? error.requestId : null;
|
|
44
|
+
let networkClass = null;
|
|
45
|
+
if (error?.name === "APITimeoutError" || error?.code === "ETIMEDOUT") networkClass = "timeout";
|
|
46
|
+
else if (error?.name === "APIConnectionError" || NETWORK_ERROR_CODES.has(error?.code)) networkClass = "connection";
|
|
47
|
+
return {
|
|
48
|
+
httpStatus: Number.isFinite(status) ? status : null,
|
|
49
|
+
providerErrorType,
|
|
50
|
+
requestId,
|
|
51
|
+
networkClass,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function safeDecisionError(error, fallbackCode = DECISION_ERROR_CODES.ENGINE_UNAVAILABLE) {
|
|
56
|
+
if (error instanceof DecisionError) return error;
|
|
57
|
+
const diagnostics = classifyProviderFailure(error);
|
|
58
|
+
const build = (code, message) => {
|
|
59
|
+
const normalized = decisionError(code, message);
|
|
60
|
+
normalized.diagnostics = diagnostics;
|
|
61
|
+
return normalized;
|
|
62
|
+
};
|
|
63
|
+
if (diagnostics.httpStatus === 401 || diagnostics.httpStatus === 403) return build(DECISION_ERROR_CODES.AUTH_INVALID, "The semantic decision service rejected authentication.");
|
|
64
|
+
if (diagnostics.httpStatus === 429) return build(DECISION_ERROR_CODES.RATE_LIMITED, "The semantic decision service rate limit was reached.");
|
|
65
|
+
if (diagnostics.networkClass === "timeout") return build(DECISION_ERROR_CODES.TIMEOUT, "The semantic decision request timed out.");
|
|
66
|
+
if (error?.name === "AuthenticationError") return build(DECISION_ERROR_CODES.AUTH_INVALID, "The semantic decision service rejected authentication.");
|
|
67
|
+
return build(fallbackCode, "The semantic decision service is unavailable.");
|
|
68
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "../artifacts.js";
|
|
2
|
+
import { readDecisionArtifact } from "./artifact.js";
|
|
3
|
+
import { DECISION_KINDS, DECISION_STATUS, SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT } from "./constants.js";
|
|
4
|
+
import { DECISION_ERROR_CODES } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
const HEX = /^[a-f0-9]{64}$/;
|
|
7
|
+
|
|
8
|
+
export function assertSemanticDecisionDetails(details, eventName = SEMANTIC_DECISION_RECORDED_EVENT) {
|
|
9
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) throw Object.assign(new Error(`${eventName} requires structured details`), { code: DECISION_ERROR_CODES.RESULT_INVALID });
|
|
10
|
+
for (const key of ["decisionId", "decisionKind", "artifactFingerprint", "questionSetFingerprint", "stateFingerprint", "model"]) {
|
|
11
|
+
if (typeof details[key] !== "string" || !details[key]) throw Object.assign(new Error(`${eventName} details.${key} is required`), { code: DECISION_ERROR_CODES.RESULT_INVALID });
|
|
12
|
+
}
|
|
13
|
+
if (!DECISION_KINDS.includes(details.decisionKind) || details.model !== "jev-1.13.0" || !HEX.test(details.artifactFingerprint) || !HEX.test(details.questionSetFingerprint) || !HEX.test(details.stateFingerprint)) {
|
|
14
|
+
throw Object.assign(new Error(`${eventName} details are invalid`), { code: DECISION_ERROR_CODES.RESULT_INVALID });
|
|
15
|
+
}
|
|
16
|
+
if (eventName === SEMANTIC_DECISION_RECORDED_EVENT && details.status !== undefined && !DECISION_STATUS.includes(details.status)) {
|
|
17
|
+
throw Object.assign(new Error(`${eventName} details.status is invalid`), { code: DECISION_ERROR_CODES.RESULT_INVALID });
|
|
18
|
+
}
|
|
19
|
+
if (eventName === SEMANTIC_DECISION_SUPERSEDED_EVENT && (typeof details.supersededBy !== "string" || !details.supersededBy)) {
|
|
20
|
+
throw Object.assign(new Error(`${eventName} details.supersededBy is required`), { code: DECISION_ERROR_CODES.RESULT_INVALID });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function decisionEventDetails(artifact) {
|
|
25
|
+
return {
|
|
26
|
+
decisionId: artifact.decisionId,
|
|
27
|
+
decisionKind: artifact.decisionKind,
|
|
28
|
+
artifactFingerprint: canonicalFingerprint(artifact),
|
|
29
|
+
questionSetFingerprint: artifact.questionSetFingerprint,
|
|
30
|
+
stateFingerprint: artifact.stateFingerprint,
|
|
31
|
+
model: artifact.model,
|
|
32
|
+
status: "RECORDED",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function decisionSupersededEventDetails(previousArtifact, supersededBy) {
|
|
37
|
+
return {
|
|
38
|
+
...decisionEventDetails(previousArtifact),
|
|
39
|
+
supersededBy,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateSemanticDecisionEventBindings(events = []) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
const recorded = new Map();
|
|
46
|
+
for (const event of events) {
|
|
47
|
+
if (![SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT].includes(event.event)) continue;
|
|
48
|
+
try { assertSemanticDecisionDetails(event.details, event.event); } catch (error) {
|
|
49
|
+
errors.push({ code: error.code ?? DECISION_ERROR_CODES.RESULT_INVALID, message: `event ${event.seq} (${event.event}): ${error.message}` });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (event.event === SEMANTIC_DECISION_RECORDED_EVENT) {
|
|
53
|
+
const key = `${event.taskId}:${event.details.decisionId}`;
|
|
54
|
+
if (recorded.has(key)) {
|
|
55
|
+
errors.push({ code: DECISION_ERROR_CODES.RESULT_INVALID, message: `event ${event.seq} records duplicate decision ID ${event.details.decisionId}` });
|
|
56
|
+
}
|
|
57
|
+
recorded.set(key, event);
|
|
58
|
+
}
|
|
59
|
+
if (event.event === SEMANTIC_DECISION_SUPERSEDED_EVENT) {
|
|
60
|
+
const key = `${event.taskId}:${event.details.decisionId}`;
|
|
61
|
+
if (!recorded.has(key)) errors.push({ code: DECISION_ERROR_CODES.RESULT_INVALID, message: `event ${event.seq} supersedes a decision that was not recorded` });
|
|
62
|
+
if (!recorded.has(`${event.taskId}:${event.details.supersededBy}`)) {
|
|
63
|
+
errors.push({ code: DECISION_ERROR_CODES.RESULT_INVALID, message: `event ${event.seq} points to a superseding decision that was not recorded` });
|
|
64
|
+
}
|
|
65
|
+
const next = events.find((candidate) => candidate.seq === event.seq + 1);
|
|
66
|
+
if (next?.event !== "TRANSACTION_COMMITTED" || next.taskId !== event.taskId || next.details?.operation !== "semantic-decision") {
|
|
67
|
+
errors.push({ code: DECISION_ERROR_CODES.LEDGER_INVALID, message: `event ${event.seq} supersession is not closed by a semantic-decision transaction` });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (event.event === SEMANTIC_DECISION_RECORDED_EVENT) {
|
|
71
|
+
const next = events.find((candidate) => candidate.seq === event.seq + 1);
|
|
72
|
+
const closesDirectly = next?.event === "TRANSACTION_COMMITTED" && next.taskId === event.taskId && next.details?.operation === "semantic-decision";
|
|
73
|
+
const continuesWithSupersession = next?.event === SEMANTIC_DECISION_SUPERSEDED_EVENT && next.taskId === event.taskId;
|
|
74
|
+
if (!closesDirectly && !continuesWithSupersession) {
|
|
75
|
+
errors.push({ code: DECISION_ERROR_CODES.LEDGER_INVALID, message: `event ${event.seq} is not closed by the semantic-decision transaction boundary` });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return errors;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function validateSemanticDecisionArtifactBindings(target, packageRoot, events = []) {
|
|
83
|
+
const errors = [];
|
|
84
|
+
for (const event of events) {
|
|
85
|
+
if (event.event !== SEMANTIC_DECISION_RECORDED_EVENT) continue;
|
|
86
|
+
try {
|
|
87
|
+
const artifact = (await readDecisionArtifact(target, event.taskId, event.details.decisionId, packageRoot)).value;
|
|
88
|
+
if (artifact.taskId !== event.taskId
|
|
89
|
+
|| artifact.decisionId !== event.details.decisionId
|
|
90
|
+
|| canonicalFingerprint(artifact) !== event.details.artifactFingerprint
|
|
91
|
+
|| artifact.questionSetFingerprint !== event.details.questionSetFingerprint
|
|
92
|
+
|| artifact.stateFingerprint !== event.details.stateFingerprint
|
|
93
|
+
|| artifact.model !== event.details.model) {
|
|
94
|
+
errors.push({ code: DECISION_ERROR_CODES.LEDGER_INVALID, message: `event ${event.seq} does not match its immutable semantic decision artifact` });
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
errors.push({ code: DECISION_ERROR_CODES.LEDGER_INVALID, message: `event ${event.seq} semantic decision artifact is unavailable or invalid` });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return errors;
|
|
101
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export function decisionFreshnessErrors(artifact, current = {}) {
|
|
4
|
+
if (!artifact || typeof artifact !== "object") return [{ code: DECISION_ERROR_CODES.CACHE_INVALID, message: "Semantic decision artifact is missing." }];
|
|
5
|
+
const bindings = [
|
|
6
|
+
"stateFingerprint", "taskStateFingerprint", "semanticStateFingerprint",
|
|
7
|
+
"repositoryFingerprint", "contractFingerprint", "routeFingerprint",
|
|
8
|
+
"verificationCycle", "questionSetFingerprint", "policyFingerprint", "model",
|
|
9
|
+
"candidateSetFingerprint",
|
|
10
|
+
];
|
|
11
|
+
return bindings.flatMap((key) => artifact[key] !== undefined && current[key] !== undefined && JSON.stringify(artifact[key]) !== JSON.stringify(current[key])
|
|
12
|
+
? [{ code: DECISION_ERROR_CODES.STALE, message: `Semantic decision binding is stale: ${key}` }] : []);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function assertDecisionFresh(artifact, current) {
|
|
16
|
+
const errors = decisionFreshnessErrors(artifact, current);
|
|
17
|
+
if (errors.length) throw decisionError(DECISION_ERROR_CODES.STALE, errors[0].message);
|
|
18
|
+
return artifact;
|
|
19
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { DECISION_ERROR_CODES, decisionError } from "../errors.js";
|
|
2
|
+
|
|
3
|
+
function answerValue(answer) {
|
|
4
|
+
if (answer && typeof answer === "object" && !Array.isArray(answer)) {
|
|
5
|
+
for (const key of ["choice", "noul", "value", "answer"]) {
|
|
6
|
+
if (Object.prototype.hasOwnProperty.call(answer, key)) return answer[key];
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return answer;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function boolAnswer(value) {
|
|
13
|
+
if (value === true || value === "yes" || value === "YES") return true;
|
|
14
|
+
if (value === false || value === "no" || value === "NO") return false;
|
|
15
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1) return value >= 0.5;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function unique(values) {
|
|
20
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeQuestionSetAnswers(questionSet, answers) {
|
|
24
|
+
const normalized = {};
|
|
25
|
+
for (const [name, question] of Object.entries(questionSet.questions)) {
|
|
26
|
+
const value = answerValue(answers[name]);
|
|
27
|
+
if (question.type === "choice") {
|
|
28
|
+
if (typeof value !== "string" || !Object.prototype.hasOwnProperty.call(question.criteria, value)) {
|
|
29
|
+
throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, `Semantic answer ${name} is outside the canonical vocabulary.`);
|
|
30
|
+
}
|
|
31
|
+
normalized[name] = value;
|
|
32
|
+
} else {
|
|
33
|
+
const bool = boolAnswer(value);
|
|
34
|
+
if (bool === null) throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, `Semantic answer ${name} must be yes or no.`);
|
|
35
|
+
normalized[name] = bool;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeConfidence(questionSet, confidence) {
|
|
42
|
+
return Object.fromEntries(Object.keys(questionSet.questions).map((name) => {
|
|
43
|
+
const value = confidence?.[name];
|
|
44
|
+
if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1)) {
|
|
45
|
+
throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, `Semantic confidence ${name} must be finite and between 0 and 1.`);
|
|
46
|
+
}
|
|
47
|
+
return [name, value ?? null];
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeModelRouteDecision(answers, confidence) {
|
|
52
|
+
const reasoning = answers.reasoning_depth;
|
|
53
|
+
return {
|
|
54
|
+
tier: reasoning,
|
|
55
|
+
generationRequired: answers.generation_required,
|
|
56
|
+
confidence: confidence.reasoning_depth,
|
|
57
|
+
requiresEscalation: reasoning === "PRIMARY" || confidence.reasoning_depth === null || confidence.reasoning_depth < 0.75,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeExecutionProfileDecision(answers, confidence) {
|
|
62
|
+
return {
|
|
63
|
+
recommendedProfile: answers.recommended_depth,
|
|
64
|
+
confidence: confidence.recommended_depth,
|
|
65
|
+
needsBroadContext: answers.needs_broad_context,
|
|
66
|
+
needsDeepReview: answers.needs_deep_review,
|
|
67
|
+
highAmbiguity: answers.high_ambiguity,
|
|
68
|
+
crossComponentReasoning: answers.cross_component_reasoning,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function candidateProjection(questionSet, answers, confidence, ids, prefix) {
|
|
73
|
+
const rankedIds = [];
|
|
74
|
+
const excludedIds = [];
|
|
75
|
+
const confidenceById = {};
|
|
76
|
+
for (const [index, id] of ids.entries()) {
|
|
77
|
+
const key = `${prefix}${index}_relevant`;
|
|
78
|
+
confidenceById[id] = confidence[key];
|
|
79
|
+
if (answers[key] === true) rankedIds.push(id);
|
|
80
|
+
else if (answers[key] === false) excludedIds.push(id);
|
|
81
|
+
}
|
|
82
|
+
rankedIds.sort((a, b) => (confidence[`${prefix}${ids.indexOf(a)}_relevant`] ?? 0) - (confidence[`${prefix}${ids.indexOf(b)}_relevant`] ?? 0));
|
|
83
|
+
return { rankedIds: rankedIds.reverse(), excludedIds, confidenceById };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function projectDecision(questionSet, answers, confidence, input) {
|
|
87
|
+
const projections = {
|
|
88
|
+
INTAKE: () => ({ dimensions: answers }),
|
|
89
|
+
CONTRACT_APPLICABILITY: () => ({ applicable: answers.applicable }),
|
|
90
|
+
ROUTE: () => (questionSet.metadata?.candidateQuestionPrefix
|
|
91
|
+
? candidateProjection(questionSet, answers, confidence, questionSet.candidateIds ?? [], questionSet.metadata.candidateQuestionPrefix)
|
|
92
|
+
: { relevantGuides: unique([answers.relevant_guides]) }),
|
|
93
|
+
EXECUTION_PROFILE: () => normalizeExecutionProfileDecision(answers, confidence),
|
|
94
|
+
CONTEXT_PLAN: () => (questionSet.metadata?.candidateQuestionPrefix
|
|
95
|
+
? { needs: answers, ...candidateProjection(questionSet, answers, confidence, questionSet.candidateIds ?? [], questionSet.metadata.candidateQuestionPrefix) }
|
|
96
|
+
: { needs: answers }),
|
|
97
|
+
MODEL_ROUTE: () => normalizeModelRouteDecision(answers, confidence),
|
|
98
|
+
FAILURE_TRIAGE: () => ({ failureClass: answers.failure_class }),
|
|
99
|
+
DIAGNOSIS_PRIORITY: () => ({ priority: answers.priority }),
|
|
100
|
+
REVIEW_PLAN: () => ({ reviewFocus: answers }),
|
|
101
|
+
TASK_OVERLAP: () => ({ relationship: answers.relationship }),
|
|
102
|
+
TEST_UTILITY: () => (questionSet.metadata?.testCount
|
|
103
|
+
? { judgments: answers, candidateIds: unique(questionSet.candidateIds ?? input.candidateIds ?? []), tests: Object.fromEntries((questionSet.candidateIds ?? []).map((id, index) => [id, Object.fromEntries((questionSet.metadata.dimensions ?? []).map((dimension) => [dimension, answers[`test_${index}_${dimension}`]]))])) }
|
|
104
|
+
: { judgments: answers, candidateIds: unique(input.candidateIds ?? []) }),
|
|
105
|
+
TEST_PRUNE: () => ({ redundancy: answers.redundancy }),
|
|
106
|
+
};
|
|
107
|
+
const project = projections[questionSet.decisionKind];
|
|
108
|
+
if (!project) throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, `No canonical normalizer exists for ${questionSet.decisionKind}.`);
|
|
109
|
+
return project();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function normalizeCanonicalDecision({ questionSet, answers = {}, input = {}, confidence = {} } = {}) {
|
|
113
|
+
const normalizedAnswers = normalizeQuestionSetAnswers(questionSet, answers);
|
|
114
|
+
return projectDecision(questionSet, normalizedAnswers, normalizeConfidence(questionSet, confidence), input);
|
|
115
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "../artifacts.js";
|
|
2
|
+
import { DECISION_DEFAULT_POLICY, DECISION_ENGINE_ID, PINNED_JEV_MODEL } from "./constants.js";
|
|
3
|
+
import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
export function normalizeDecisionPolicy(input = {}) {
|
|
6
|
+
const policy = { ...DECISION_DEFAULT_POLICY, ...input };
|
|
7
|
+
if (policy.required !== true || policy.provider !== DECISION_ENGINE_ID || policy.model !== PINNED_JEV_MODEL) {
|
|
8
|
+
throw decisionError(DECISION_ERROR_CODES.POLICY_INVALID, "ForgeLoop Jev policy must remain mandatory and pinned.");
|
|
9
|
+
}
|
|
10
|
+
if (!Number.isInteger(policy.policyVersion) || policy.policyVersion < 1 || !Number.isInteger(policy.requestTimeoutMs) || policy.requestTimeoutMs < 500 || policy.requestTimeoutMs > 60_000 || !Number.isInteger(policy.maxRetries) || policy.maxRetries < 0 || policy.maxRetries > 3 || typeof policy.cache !== "boolean") {
|
|
11
|
+
throw decisionError(DECISION_ERROR_CODES.POLICY_INVALID, "ForgeLoop Jev policy limits are invalid.");
|
|
12
|
+
}
|
|
13
|
+
return Object.freeze(policy);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function decisionPolicyFingerprint(policy) {
|
|
17
|
+
return canonicalFingerprint(normalizeDecisionPolicy(policy));
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function projectSemanticDecisionStatus({ artifact = null, fresh = false, errorCodes = [] } = {}) {
|
|
2
|
+
return {
|
|
3
|
+
required: true,
|
|
4
|
+
engine: "typesafe-jev",
|
|
5
|
+
model: "jev-1.13.0",
|
|
6
|
+
authority: "SEMANTIC_DECISION",
|
|
7
|
+
evidenceAuthority: "NONE",
|
|
8
|
+
lifecycleAuthority: false,
|
|
9
|
+
completionAuthority: false,
|
|
10
|
+
ownershipAuthority: false,
|
|
11
|
+
installationAuthority: false,
|
|
12
|
+
status: artifact ? (fresh ? "FRESH" : "STALE") : "ABSENT",
|
|
13
|
+
decisionId: artifact?.decisionId ?? null,
|
|
14
|
+
errorCodes: [...errorCodes],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "../artifacts.js";
|
|
2
|
+
import { DECISION_QUESTION_SETS } from "./constants.js";
|
|
3
|
+
import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
function freezeQuestionSet(definition) {
|
|
6
|
+
return Object.freeze({
|
|
7
|
+
...definition,
|
|
8
|
+
questions: Object.freeze({ ...definition.questions }),
|
|
9
|
+
fingerprint: canonicalFingerprint(definition.questions),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const QUESTION_SET_DEFINITIONS = Object.freeze({
|
|
14
|
+
"contract-v1": Object.freeze({
|
|
15
|
+
id: "contract-v1", version: 1, decisionKind: "CONTRACT_APPLICABILITY",
|
|
16
|
+
questions: Object.freeze({ applicable: { type: "noul", criteria: { yes: "Contract applies to the requested work", no: "Contract scope is not applicable" } } }),
|
|
17
|
+
}),
|
|
18
|
+
"route-v1": Object.freeze({
|
|
19
|
+
id: "route-v1", version: 1, decisionKind: "ROUTE",
|
|
20
|
+
questions: Object.freeze({ relevant_guides: { type: "choice", criteria: { clean: "General engineering hygiene", test: "Testing context", security: "Security context", unknown: "Insufficient information" } } }),
|
|
21
|
+
}),
|
|
22
|
+
"execution-profile-v1": Object.freeze({
|
|
23
|
+
id: "execution-profile-v1", version: 1, decisionKind: "EXECUTION_PROFILE",
|
|
24
|
+
questions: Object.freeze({
|
|
25
|
+
recommended_depth: { type: "choice", criteria: { light: "Light bounded context", balanced: "Normal engineering context", full: "Deep broad context" } },
|
|
26
|
+
needs_broad_context: { type: "noul", criteria: { yes: "Broad context is needed", no: "Broad context is not needed" } },
|
|
27
|
+
needs_deep_review: { type: "noul", criteria: { yes: "Deep review is needed", no: "Deep review is not needed" } },
|
|
28
|
+
high_ambiguity: { type: "noul", criteria: { yes: "The task is highly ambiguous", no: "The task is not highly ambiguous" } },
|
|
29
|
+
cross_component_reasoning: { type: "noul", criteria: { yes: "Cross-component reasoning is needed", no: "Cross-component reasoning is not needed" } },
|
|
30
|
+
}),
|
|
31
|
+
}),
|
|
32
|
+
"intake-v1": Object.freeze({
|
|
33
|
+
id: "intake-v1", version: 1, decisionKind: "INTAKE",
|
|
34
|
+
questions: Object.freeze({
|
|
35
|
+
work_type: { type: "choice", criteria: { documentation: "Documentation or prose change", code: "Executable code change", security: "Security-sensitive change", unknown: "Insufficient information" } },
|
|
36
|
+
backend: { type: "noul", criteria: { yes: "Backend is affected", no: "Backend is not affected" } },
|
|
37
|
+
frontend: { type: "noul", criteria: { yes: "Frontend is affected", no: "Frontend is not affected" } },
|
|
38
|
+
authentication: { type: "noul", criteria: { yes: "Authentication is affected", no: "Authentication is not affected" } },
|
|
39
|
+
authorization: { type: "noul", criteria: { yes: "Authorization is affected", no: "Authorization is not affected" } },
|
|
40
|
+
security: { type: "noul", criteria: { yes: "Security is materially relevant", no: "Security is not materially relevant" } },
|
|
41
|
+
database: { type: "noul", criteria: { yes: "Database behavior is affected", no: "Database behavior is not affected" } },
|
|
42
|
+
network: { type: "noul", criteria: { yes: "Network behavior is affected", no: "Network behavior is not affected" } },
|
|
43
|
+
public_api: { type: "noul", criteria: { yes: "A public API is affected", no: "No public API is affected" } },
|
|
44
|
+
behavior_change: { type: "noul", criteria: { yes: "Behavior changes", no: "Behavior does not change" } },
|
|
45
|
+
executable_change: { type: "noul", criteria: { yes: "Executable content changes", no: "Executable content does not change" } },
|
|
46
|
+
dependency_change: { type: "noul", criteria: { yes: "Dependencies change", no: "Dependencies do not change" } },
|
|
47
|
+
migration: { type: "noul", criteria: { yes: "A migration is involved", no: "No migration is involved" } },
|
|
48
|
+
publication: { type: "noul", criteria: { yes: "Publication is involved", no: "Publication is not involved" } },
|
|
49
|
+
documentation: { type: "noul", criteria: { yes: "Documentation is materially relevant", no: "Documentation is not materially relevant" } },
|
|
50
|
+
package: { type: "noul", criteria: { yes: "Package behavior is affected", no: "Package behavior is not affected" } },
|
|
51
|
+
ci: { type: "noul", criteria: { yes: "CI behavior is affected", no: "CI behavior is not affected" } },
|
|
52
|
+
performance: { type: "noul", criteria: { yes: "Performance is materially relevant", no: "Performance is not materially relevant" } },
|
|
53
|
+
accessibility: { type: "noul", criteria: { yes: "Accessibility is materially relevant", no: "Accessibility is not materially relevant" } },
|
|
54
|
+
compatibility: { type: "noul", criteria: { yes: "Compatibility is materially relevant", no: "Compatibility is not materially relevant" } },
|
|
55
|
+
data_sensitivity: { type: "noul", criteria: { yes: "Sensitive data is involved", no: "Sensitive data is not involved" } },
|
|
56
|
+
operational_risk: { type: "noul", criteria: { yes: "Operational risk is elevated", no: "Operational risk is not elevated" } },
|
|
57
|
+
}),
|
|
58
|
+
}),
|
|
59
|
+
"context-v1": Object.freeze({
|
|
60
|
+
id: "context-v1", version: 1, decisionKind: "CONTEXT_PLAN",
|
|
61
|
+
questions: Object.freeze({
|
|
62
|
+
need_task_history: { type: "noul", criteria: { yes: "Task history is needed", no: "Task history is not needed" } },
|
|
63
|
+
need_relevant_artifacts: { type: "noul", criteria: { yes: "Relevant artifacts are needed", no: "Relevant artifacts are not needed" } },
|
|
64
|
+
need_dependency_context: { type: "noul", criteria: { yes: "Dependency context is needed", no: "Dependency context is not needed" } },
|
|
65
|
+
need_security_context: { type: "noul", criteria: { yes: "Security context is needed", no: "Security context is not needed" } },
|
|
66
|
+
need_review_context: { type: "noul", criteria: { yes: "Review context is needed", no: "Review context is not needed" } },
|
|
67
|
+
need_verification_context: { type: "noul", criteria: { yes: "Verification context is needed", no: "Verification context is not needed" } },
|
|
68
|
+
need_advisory_context: { type: "noul", criteria: { yes: "Advisory context is needed", no: "Advisory context is not needed" } },
|
|
69
|
+
need_repository_snippets: { type: "noul", criteria: { yes: "Repository snippets are needed", no: "Repository snippets are not needed" } },
|
|
70
|
+
need_external_docs: { type: "noul", criteria: { yes: "External documentation is needed", no: "External documentation is not needed" } },
|
|
71
|
+
}),
|
|
72
|
+
}),
|
|
73
|
+
"model-route-v1": Object.freeze({
|
|
74
|
+
id: "model-route-v1", version: 1, decisionKind: "MODEL_ROUTE",
|
|
75
|
+
questions: Object.freeze({ generation_required: { type: "noul", criteria: { yes: "Generation is required", no: "A deterministic or semantic-only decision is sufficient" } }, reasoning_depth: { type: "choice", criteria: { NONE: "No generation or deep reasoning", FAST: "Small bounded reasoning", STANDARD: "Cross-file reasoning", PRIMARY: "Architecture or high ambiguity reasoning" } } }),
|
|
76
|
+
}),
|
|
77
|
+
"failure-v1": Object.freeze({
|
|
78
|
+
id: "failure-v1", version: 1, decisionKind: "FAILURE_TRIAGE",
|
|
79
|
+
questions: Object.freeze({ failure_class: { type: "choice", criteria: { implementation: "Incorrect implementation", expectation: "Stale expectation", state: "State divergence", dependency: "Dependency incompatibility", environment: "Network or environment issue", unknown: "Insufficient information" } } }),
|
|
80
|
+
}),
|
|
81
|
+
"diagnosis-v1": Object.freeze({
|
|
82
|
+
id: "diagnosis-v1", version: 1, decisionKind: "DIAGNOSIS_PRIORITY",
|
|
83
|
+
questions: Object.freeze({ priority: { type: "choice", criteria: { inspect_state: "Inspect state transition", inspect_caller: "Inspect caller", inspect_dependency: "Inspect dependency behavior", narrow_experiment: "Run a narrower experiment", unknown: "Insufficient information" } } }),
|
|
84
|
+
}),
|
|
85
|
+
"review-v1": Object.freeze({
|
|
86
|
+
id: "review-v1", version: 1, decisionKind: "REVIEW_PLAN",
|
|
87
|
+
questions: Object.freeze({
|
|
88
|
+
needs_security_review: { type: "noul", criteria: { yes: "Security review is needed", no: "Security review is not needed" } },
|
|
89
|
+
needs_api_compatibility_review: { type: "noul", criteria: { yes: "API compatibility review is needed", no: "API compatibility review is not needed" } },
|
|
90
|
+
needs_concurrency_review: { type: "noul", criteria: { yes: "Concurrency review is needed", no: "Concurrency review is not needed" } },
|
|
91
|
+
needs_package_review: { type: "noul", criteria: { yes: "Package review is needed", no: "Package review is not needed" } },
|
|
92
|
+
needs_docs_review: { type: "noul", criteria: { yes: "Documentation review is needed", no: "Documentation review is not needed" } },
|
|
93
|
+
needs_migration_review: { type: "noul", criteria: { yes: "Migration review is needed", no: "Migration review is not needed" } },
|
|
94
|
+
needs_performance_review: { type: "noul", criteria: { yes: "Performance review is needed", no: "Performance review is not needed" } },
|
|
95
|
+
needs_dependency_review: { type: "noul", criteria: { yes: "Dependency review is needed", no: "Dependency review is not needed" } },
|
|
96
|
+
needs_full_diff_context: { type: "noul", criteria: { yes: "Full diff context is needed", no: "Full diff context is not needed" } },
|
|
97
|
+
needs_primary_model: { type: "noul", criteria: { yes: "Primary model reasoning is needed", no: "Primary model reasoning is not needed" } },
|
|
98
|
+
}),
|
|
99
|
+
}),
|
|
100
|
+
"test-utility-v1": Object.freeze({
|
|
101
|
+
id: "test-utility-v1", version: 1, decisionKind: "TEST_UTILITY",
|
|
102
|
+
questions: Object.freeze({
|
|
103
|
+
validates_required_behavior: { type: "noul", criteria: { yes: "Test validates required behavior", no: "Test does not validate required behavior" } },
|
|
104
|
+
guards_public_contract: { type: "noul", criteria: { yes: "Test guards a public contract", no: "Test does not guard a public contract" } },
|
|
105
|
+
guards_security_boundary: { type: "noul", criteria: { yes: "Test guards a security boundary", no: "Test does not guard a security boundary" } },
|
|
106
|
+
guards_failure_mode: { type: "noul", criteria: { yes: "Test guards a failure mode", no: "Test does not guard a failure mode" } },
|
|
107
|
+
guards_regression_prone_behavior: { type: "noul", criteria: { yes: "Test guards regression-prone behavior", no: "Test does not guard regression-prone behavior" } },
|
|
108
|
+
has_unique_semantic_intent: { type: "noul", criteria: { yes: "Test has unique semantic intent", no: "Test has overlapping semantic intent" } },
|
|
109
|
+
semantic_duplicate: { type: "noul", criteria: { yes: "Test is a semantic duplicate", no: "Test is not a semantic duplicate" } },
|
|
110
|
+
likely_obsolete: { type: "noul", criteria: { yes: "Test is likely obsolete", no: "Test is not likely obsolete" } },
|
|
111
|
+
likely_flaky_low_signal: { type: "noul", criteria: { yes: "Test is likely flaky and low signal", no: "Test is not likely flaky and low signal" } },
|
|
112
|
+
expensive_relative_to_signal: { type: "noul", criteria: { yes: "Test is expensive relative to signal", no: "Test is not expensive relative to signal" } },
|
|
113
|
+
documentation_value: { type: "noul", criteria: { yes: "Test has documentation value", no: "Test lacks documentation value" } },
|
|
114
|
+
integration_value: { type: "noul", criteria: { yes: "Test has integration value", no: "Test lacks integration value" } },
|
|
115
|
+
platform_specific_value: { type: "noul", criteria: { yes: "Test has platform-specific value", no: "Test lacks platform-specific value" } },
|
|
116
|
+
protocol_invariant: { type: "noul", criteria: { yes: "Test protects a protocol invariant", no: "Test does not protect a protocol invariant" } },
|
|
117
|
+
requires_system_two_review: { type: "noul", criteria: { yes: "Test requires System Two review", no: "Test does not require System Two review" } },
|
|
118
|
+
}),
|
|
119
|
+
}),
|
|
120
|
+
"task-overlap-v1": Object.freeze({
|
|
121
|
+
id: "task-overlap-v1", version: 1, decisionKind: "TASK_OVERLAP",
|
|
122
|
+
questions: Object.freeze({ relationship: { type: "choice", criteria: { SAME_PROBLEM: "Same problem", RELATED: "Related problem", INDEPENDENT: "Independent problem", UNKNOWN: "Insufficient information" } } }),
|
|
123
|
+
}),
|
|
124
|
+
"test-prune-v1": Object.freeze({
|
|
125
|
+
id: "test-prune-v1", version: 1, decisionKind: "TEST_PRUNE",
|
|
126
|
+
questions: Object.freeze({ redundancy: { type: "choice", criteria: { DISTINCT_BEHAVIOR: "Distinct behavior", PARTIALLY_OVERLAPPING: "Partially overlapping", SEMANTIC_DUPLICATE: "Semantic duplicate", UNKNOWN: "Insufficient information" } } }),
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export function getQuestionSet(id) {
|
|
131
|
+
const definition = QUESTION_SET_DEFINITIONS[id];
|
|
132
|
+
if (!definition || !DECISION_QUESTION_SETS[id]) throw decisionError(DECISION_ERROR_CODES.QUESTION_SET_UNKNOWN, `Unknown semantic decision question set: ${id}`);
|
|
133
|
+
return freezeQuestionSet(definition);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildDynamicQuestionSet({ id, version = 1, decisionKind, questions, candidateIds = [], metadata = {} } = {}) {
|
|
137
|
+
if (typeof id !== "string" || !id || !Number.isInteger(version) || version < 1
|
|
138
|
+
|| typeof decisionKind !== "string" || !questions || typeof questions !== "object"
|
|
139
|
+
|| Array.isArray(questions)) {
|
|
140
|
+
throw decisionError(DECISION_ERROR_CODES.REQUEST_INVALID, "Dynamic semantic question set is invalid.");
|
|
141
|
+
}
|
|
142
|
+
const names = Object.keys(questions);
|
|
143
|
+
if (names.length === 0 || names.length > 128 || names.some((name) => !/^[a-z][a-z0-9_]{0,95}$/.test(name))) {
|
|
144
|
+
throw decisionError(DECISION_ERROR_CODES.REQUEST_INVALID, "Dynamic semantic question set has invalid bounded questions.");
|
|
145
|
+
}
|
|
146
|
+
return freezeQuestionSet({ id, version, decisionKind, questions, candidateIds: [...candidateIds].slice(0, 64), metadata });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildContextQuestionSet(candidates = []) {
|
|
150
|
+
const bounded = candidates.slice(0, 64);
|
|
151
|
+
const questions = Object.fromEntries(bounded.map((candidate, index) => [
|
|
152
|
+
`candidate_${index}_relevant`, {
|
|
153
|
+
type: "noul",
|
|
154
|
+
criteria: { yes: `Candidate ${candidate.id} is relevant to the bounded task context`, no: `Candidate ${candidate.id} is not relevant to the bounded task context` },
|
|
155
|
+
},
|
|
156
|
+
]));
|
|
157
|
+
return buildDynamicQuestionSet({
|
|
158
|
+
id: "context-candidates-v1", decisionKind: "CONTEXT_PLAN", questions,
|
|
159
|
+
candidateIds: bounded.map((candidate) => candidate.id),
|
|
160
|
+
metadata: { candidateQuestionPrefix: "candidate_", relevanceSuffix: "_relevant" },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function buildRouteQuestionSet(guides = []) {
|
|
165
|
+
const bounded = guides.slice(0, 64);
|
|
166
|
+
const questions = Object.fromEntries(bounded.map((guide, index) => [
|
|
167
|
+
`guide_${index}_relevant`, {
|
|
168
|
+
type: "noul",
|
|
169
|
+
criteria: { yes: `Guide ${guide} is relevant to the bounded route`, no: `Guide ${guide} is not relevant to the bounded route` },
|
|
170
|
+
},
|
|
171
|
+
]));
|
|
172
|
+
return buildDynamicQuestionSet({
|
|
173
|
+
id: "route-candidates-v1", decisionKind: "ROUTE", questions,
|
|
174
|
+
candidateIds: bounded,
|
|
175
|
+
metadata: { candidateQuestionPrefix: "guide_", relevanceSuffix: "_relevant" },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function buildExecutionProfileQuestionSet() {
|
|
180
|
+
return getQuestionSet("execution-profile-v1");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function buildTestUtilityQuestionSet(tests = []) {
|
|
184
|
+
const dimensions = ["unique_semantic_intent", "semantic_duplicate", "likely_obsolete", "likely_flaky_low_signal", "expensive_relative_to_signal", "documentation_value", "integration_value", "guards_regression_prone_behavior", "requires_system_two_review"];
|
|
185
|
+
const bounded = tests.slice(0, 16);
|
|
186
|
+
const questions = {};
|
|
187
|
+
for (let index = 0; index < bounded.length; index += 1) {
|
|
188
|
+
for (const dimension of dimensions) questions[`test_${index}_${dimension}`] = {
|
|
189
|
+
type: "noul", criteria: { yes: `${dimension} applies to test ${bounded[index].testId}`, no: `${dimension} does not apply to test ${bounded[index].testId}` },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return buildDynamicQuestionSet({
|
|
193
|
+
id: "test-utility-candidates-v1", decisionKind: "TEST_UTILITY", questions,
|
|
194
|
+
candidateIds: bounded.map((test) => test.testId),
|
|
195
|
+
metadata: { dimensions, candidateQuestionPrefix: "test_", testCount: bounded.length },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function listQuestionSets() {
|
|
200
|
+
return Object.values(DECISION_QUESTION_SETS).map((entry) => getQuestionSet(entry.id));
|
|
201
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { buildCombinedDecisionState } from "./state-builder.js";
|
|
2
|
+
import { DECISION_KINDS, PINNED_JEV_MODEL } from "./constants.js";
|
|
3
|
+
import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
|
|
4
|
+
import { getQuestionSet } from "./question-registry.js";
|
|
5
|
+
|
|
6
|
+
export function validateDecisionRequest(input = {}) {
|
|
7
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw decisionError(DECISION_ERROR_CODES.REQUEST_INVALID, "Decision request must be an object.");
|
|
8
|
+
if (typeof input.taskId !== "string" || !input.taskId) throw decisionError(DECISION_ERROR_CODES.REQUEST_INVALID, "Decision request requires a taskId.");
|
|
9
|
+
if (!DECISION_KINDS.includes(input.decisionKind)) throw decisionError(DECISION_ERROR_CODES.REQUEST_INVALID, "Decision request has an unsupported decision kind.");
|
|
10
|
+
if (typeof input.questionSetId !== "string" && !input.questionSet) throw decisionError(DECISION_ERROR_CODES.REQUEST_INVALID, "Decision request requires a question set.");
|
|
11
|
+
const questionSet = input.questionSet ?? getQuestionSet(input.questionSetId);
|
|
12
|
+
if (typeof questionSet.id !== "string" || typeof questionSet.fingerprint !== "string") throw decisionError(DECISION_ERROR_CODES.REQUEST_INVALID, "Decision request question set is not canonical.");
|
|
13
|
+
if (questionSet.decisionKind !== input.decisionKind) throw decisionError(DECISION_ERROR_CODES.REQUEST_INVALID, "Question set does not match decision kind.");
|
|
14
|
+
if (input.model !== undefined && input.model !== PINNED_JEV_MODEL) throw decisionError(DECISION_ERROR_CODES.MODEL_UNSUPPORTED, "Only the pinned ForgeLoop Jev model is supported.");
|
|
15
|
+
const state = input.state && typeof input.state === "object"
|
|
16
|
+
&& (Object.prototype.hasOwnProperty.call(input.state, "lifecycle")
|
|
17
|
+
|| Object.prototype.hasOwnProperty.call(input.state, "semantic"))
|
|
18
|
+
? buildCombinedDecisionState(input.state)
|
|
19
|
+
: buildCombinedDecisionState({ semantic: input.state ?? {} });
|
|
20
|
+
return {
|
|
21
|
+
...input,
|
|
22
|
+
model: PINNED_JEV_MODEL,
|
|
23
|
+
questionSet,
|
|
24
|
+
state,
|
|
25
|
+
};
|
|
26
|
+
}
|