@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.
Files changed (249) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/AGENTS.md +1 -1
  3. package/AGENT_COMPATIBILITY.md +8 -0
  4. package/CLAUDE.md +1 -1
  5. package/CONTRIBUTING.md +90 -0
  6. package/DOCS_INDEX.md +46 -12
  7. package/ENG/c-development-eng.md +112 -0
  8. package/ENG/cpp-development-eng.md +109 -0
  9. package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
  10. package/ENG/go-development-eng.md +103 -0
  11. package/ENG/java-development-eng.md +125 -0
  12. package/ENG/nodejs-backend-development-eng.md +605 -0
  13. package/ENG/php-development-eng.md +104 -0
  14. package/ENG/rust-development-eng.md +422 -0
  15. package/ENG/sec-code-eng.md +7 -7
  16. package/ENG/sql-development-eng.md +108 -0
  17. package/ENG/swift-development-eng.md +111 -0
  18. package/ENG/typescript-development-eng.md +108 -0
  19. package/EXECUTION_STATE.md +12 -0
  20. package/GUIDE_ROUTER.md +418 -9
  21. package/LOOP_ENGINEERING.md +28 -2
  22. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  23. package/PROTOCOL_INTEGRATION.md +55 -2
  24. package/QUALITY_SCORECARD.md +1 -0
  25. package/README.md +78 -52
  26. package/TERMINOLOGY.md +2 -0
  27. package/THIRD_PARTY_NOTICES.md +19 -7
  28. package/THREAT_MODEL.md +140 -1
  29. package/completions/_forgeloop +22 -4
  30. package/completions/forgeloop.bash +40 -4
  31. package/completions/forgeloop.fish +130 -1
  32. package/docs/ADVISORY_CONTEXT.md +25 -0
  33. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  34. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  35. package/docs/AGENT_PROTOCOL_SUMMARY.md +81 -3
  36. package/docs/AGENT_SKILL.md +66 -0
  37. package/docs/ARTIFACT_REFERENCE.md +123 -0
  38. package/docs/AUDIT_UX.md +46 -0
  39. package/docs/BROWSER_VERIFICATION.md +136 -0
  40. package/docs/CLI_REFERENCE.md +392 -10
  41. package/docs/CODE_ATTESTATION.md +2 -2
  42. package/docs/DOCUMENTATION_GUIDE.md +34 -12
  43. package/docs/GETTING_STARTED.md +59 -0
  44. package/docs/JEV_BENCHMARKS.md +31 -0
  45. package/docs/MODEL_ROUTING.md +37 -0
  46. package/docs/OPENSRC_ADAPTER.md +241 -0
  47. package/docs/PACKAGE_CONTENTS.md +60 -19
  48. package/docs/PROVIDERS.md +126 -0
  49. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  50. package/docs/RECIPES.md +32 -0
  51. package/docs/RELEASE_CHECKLIST.md +66 -5
  52. package/docs/SECURITY_REVIEW.md +71 -0
  53. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  54. package/docs/TEST_INTELLIGENCE.md +29 -0
  55. package/docs/TEST_PRUNING.md +14 -0
  56. package/docs/TROUBLESHOOTING.md +298 -3
  57. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  58. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  59. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  60. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  61. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  62. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  63. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  64. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  65. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  66. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  67. package/docs/diagrams/README.md +13 -9
  68. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  69. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  70. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  71. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  72. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  73. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  74. package/docs/documentation-manifest.json +1397 -0
  75. package/docs/protocol-requirements.json +101 -0
  76. package/package.json +46 -4
  77. package/schemas/config.schema.json +14 -0
  78. package/schemas/context-plan.schema.json +18 -0
  79. package/schemas/routing-input.schema.json +1 -1
  80. package/schemas/semantic-decision.schema.json +46 -0
  81. package/schemas/test-utility.schema.json +44 -0
  82. package/scripts/CI_VALIDATORS.md +84 -11
  83. package/scripts/benchmark-jev.mjs +5 -0
  84. package/scripts/benchmark-test-intelligence.mjs +4 -0
  85. package/scripts/generate-agent-protocol-summary.mjs +40 -1
  86. package/scripts/generate-forgeloop-skill.mjs +133 -0
  87. package/scripts/jev-smoke.mjs +19 -0
  88. package/skills/forgeloop/README.md +9 -0
  89. package/skills/forgeloop/SKILL.md +77 -0
  90. package/skills/forgeloop/references/lifecycle.md +9 -0
  91. package/skills/forgeloop/references/recovery.md +7 -0
  92. package/skills/forgeloop/references/verification.md +7 -0
  93. package/src/adapters/agent-browser/assertions.js +47 -0
  94. package/src/adapters/agent-browser/commands.js +54 -0
  95. package/src/adapters/agent-browser/index.js +3 -0
  96. package/src/adapters/agent-browser/locator.js +40 -0
  97. package/src/adapters/agent-browser/process.js +215 -0
  98. package/src/adapters/agent-browser/provider.js +313 -0
  99. package/src/adapters/emulated-services/constants.js +24 -0
  100. package/src/adapters/emulated-services/index.js +7 -0
  101. package/src/adapters/emulated-services/process.js +162 -0
  102. package/src/adapters/emulated-services/provider.js +282 -0
  103. package/src/adapters/opensrc/normalize.js +90 -0
  104. package/src/adapters/opensrc/process.js +248 -0
  105. package/src/adapters/opensrc/provider.js +338 -0
  106. package/src/adapters/opensrc/search.js +264 -0
  107. package/src/adapters/typesafe/client.js +28 -0
  108. package/src/adapters/typesafe/engine.js +63 -0
  109. package/src/adapters/typesafe/normalize.js +41 -0
  110. package/src/cli.js +108 -0
  111. package/src/commands/checkpoint-revalidate.js +176 -0
  112. package/src/commands/context-plan.js +38 -0
  113. package/src/commands/contract-create.js +264 -0
  114. package/src/commands/contract-revise.js +236 -0
  115. package/src/commands/decision-show.js +14 -0
  116. package/src/commands/decision-status.js +22 -0
  117. package/src/commands/discover.js +41 -0
  118. package/src/commands/doctor.js +15 -0
  119. package/src/commands/gate-record.js +205 -0
  120. package/src/commands/gate-revalidate.js +137 -0
  121. package/src/commands/model-route.js +32 -0
  122. package/src/commands/next.js +19 -7
  123. package/src/commands/route.js +146 -18
  124. package/src/commands/semantic-plan.js +17 -0
  125. package/src/commands/task-abandon.js +224 -0
  126. package/src/commands/task-create.js +84 -25
  127. package/src/commands/task-list.js +22 -2
  128. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  129. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  130. package/src/commands/test-inventory.js +5 -0
  131. package/src/commands/test-prune-plan.js +5 -0
  132. package/src/commands/test-prune-probe.js +5 -0
  133. package/src/commands/test-utility.js +5 -0
  134. package/src/commands/validate-protocol.js +10 -1
  135. package/src/config/guides.json +44 -0
  136. package/src/core/artifact-registry.js +24 -0
  137. package/src/core/audit-ux.js +514 -0
  138. package/src/core/browser-verification/constants.js +149 -0
  139. package/src/core/browser-verification/normalize.js +254 -0
  140. package/src/core/browser-verification/provider.js +519 -0
  141. package/src/core/browser-verification/service.js +115 -0
  142. package/src/core/build-script.js +151 -0
  143. package/src/core/c-cpp-project.js +143 -0
  144. package/src/core/checkpoint-revalidation.js +319 -0
  145. package/src/core/cli-command-definitions.js +249 -1
  146. package/src/core/command-executors.js +115 -3
  147. package/src/core/command-input.js +212 -102
  148. package/src/core/completion-artifacts.js +14 -5
  149. package/src/core/completion.js +4 -6
  150. package/src/core/config.js +3 -0
  151. package/src/core/context-compiler/budget.js +9 -0
  152. package/src/core/context-compiler/candidates.js +39 -0
  153. package/src/core/context-compiler/compiler.js +63 -0
  154. package/src/core/context-compiler/fingerprint.js +11 -0
  155. package/src/core/context-compiler/policy.js +13 -0
  156. package/src/core/context-compiler/result.js +23 -0
  157. package/src/core/contract-bootstrap-recovery.js +655 -0
  158. package/src/core/contract-presets.js +82 -0
  159. package/src/core/contract-revision.js +210 -0
  160. package/src/core/decision/artifact.js +69 -0
  161. package/src/core/decision/benchmarks.js +103 -0
  162. package/src/core/decision/cache.js +27 -0
  163. package/src/core/decision/constants.js +58 -0
  164. package/src/core/decision/cutover.js +34 -0
  165. package/src/core/decision/engine.js +22 -0
  166. package/src/core/decision/errors.js +68 -0
  167. package/src/core/decision/events.js +101 -0
  168. package/src/core/decision/freshness.js +19 -0
  169. package/src/core/decision/normalizers/index.js +115 -0
  170. package/src/core/decision/policy.js +18 -0
  171. package/src/core/decision/projection.js +16 -0
  172. package/src/core/decision/question-registry.js +201 -0
  173. package/src/core/decision/request.js +26 -0
  174. package/src/core/decision/resolver.js +130 -0
  175. package/src/core/decision/result.js +58 -0
  176. package/src/core/decision/service.js +156 -0
  177. package/src/core/decision/state-builder.js +65 -0
  178. package/src/core/decision/task-bindings.js +30 -0
  179. package/src/core/decision/test-provider.js +32 -0
  180. package/src/core/decision/thresholds.js +15 -0
  181. package/src/core/error-codes.js +281 -3
  182. package/src/core/events.js +226 -57
  183. package/src/core/evidence-readiness.js +9 -0
  184. package/src/core/execution-prerequisites.js +14 -0
  185. package/src/core/execution-profile.js +63 -38
  186. package/src/core/filesystem.js +1 -10
  187. package/src/core/gate-provenance.js +124 -0
  188. package/src/core/go-project.js +206 -0
  189. package/src/core/integration-invocation-policy.js +27 -4
  190. package/src/core/integration-resources.js +86 -61
  191. package/src/core/java-project.js +403 -0
  192. package/src/core/model-router/constants.js +10 -0
  193. package/src/core/model-router/policy.js +103 -0
  194. package/src/core/model-router/router.js +37 -0
  195. package/src/core/multi-language-project.js +117 -0
  196. package/src/core/next-action-model.js +58 -0
  197. package/src/core/next-action-phases.js +130 -42
  198. package/src/core/next-action-refresh.js +43 -9
  199. package/src/core/next-action-review-phase.js +7 -2
  200. package/src/core/next-action.js +35 -7
  201. package/src/core/next-explanation.js +63 -0
  202. package/src/core/phase.js +128 -10
  203. package/src/core/php-project.js +85 -0
  204. package/src/core/preflight-consistency.js +23 -9
  205. package/src/core/preflight-loaders.js +37 -5
  206. package/src/core/project-detection.js +1760 -52
  207. package/src/core/protocol-info.js +65 -0
  208. package/src/core/protocol.js +20 -0
  209. package/src/core/reconcile-closure.js +132 -53
  210. package/src/core/recovery-history.js +1 -0
  211. package/src/core/resumability.js +154 -44
  212. package/src/core/route-artifact.js +15 -1
  213. package/src/core/router.js +223 -4
  214. package/src/core/runtime-context.js +118 -61
  215. package/src/core/rust-project.js +400 -0
  216. package/src/core/schema-validation.js +3 -0
  217. package/src/core/security-review/constants.js +64 -0
  218. package/src/core/security-review/normalize.js +245 -0
  219. package/src/core/security-review/provider.js +204 -0
  220. package/src/core/security-review/service.js +134 -0
  221. package/src/core/semantic-planning/constants.js +19 -0
  222. package/src/core/semantic-planning/projection.js +94 -0
  223. package/src/core/semantic-planning/service.js +15 -0
  224. package/src/core/sources.js +37 -0
  225. package/src/core/sql-project.js +141 -0
  226. package/src/core/swift-project.js +200 -0
  227. package/src/core/task-claim-state.js +201 -1
  228. package/src/core/task-conflict-inspection.js +31 -5
  229. package/src/core/task-paths.js +13 -0
  230. package/src/core/task-recovery.js +1 -0
  231. package/src/core/templates.js +3 -0
  232. package/src/core/test-intelligence/benchmarks.js +68 -0
  233. package/src/core/test-intelligence/inventory.js +73 -0
  234. package/src/core/test-intelligence/prune.js +90 -0
  235. package/src/core/test-intelligence/semantic-state.js +15 -0
  236. package/src/core/test-intelligence/service.js +40 -0
  237. package/src/core/test-intelligence/utility.js +50 -0
  238. package/src/core/trace.js +11 -7
  239. package/src/core/transaction.js +1 -0
  240. package/src/core/typescript-project.js +349 -0
  241. package/src/core/xml-structure.js +123 -0
  242. package/src/integration.d.ts +492 -0
  243. package/src/integration.js +54 -0
  244. package/src/providers/README.md +47 -0
  245. package/src/providers/capabilities.js +46 -0
  246. package/src/providers/errors.js +15 -0
  247. package/src/providers/index.js +29 -0
  248. package/src/providers/json-snapshot.js +105 -0
  249. package/src/providers/registry.js +152 -0
@@ -0,0 +1,130 @@
1
+ import { readdir } from "node:fs/promises";
2
+
3
+ import { canonicalFingerprint, readJsonArtifact } from "../artifacts.js";
4
+ import { fileExists, ensureWithin } from "../filesystem.js";
5
+ import { readEvents, validateEventLedger } from "../events.js";
6
+ import { taskDecisionDirectory } from "../task-paths.js";
7
+ import { getQuestionSet } from "./question-registry.js";
8
+ import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
9
+ import { DECISION_ENGINE_ID, DECISION_POLICY_VERSION, PINNED_JEV_MODEL, SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT } from "./constants.js";
10
+ import { assertDecisionFresh } from "./freshness.js";
11
+
12
+ function invalid(message, code = DECISION_ERROR_CODES.BINDING_INVALID) {
13
+ return decisionError(code, message);
14
+ }
15
+
16
+ function assertArtifactIdentity(artifact, { expectedDecisionKind, expectedTaskId, expectedQuestionSetId } = {}) {
17
+ if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) throw invalid("Semantic decision artifact is not an object.");
18
+ if (artifact.taskId !== expectedTaskId) throw invalid("Semantic decision task binding is invalid.");
19
+ if (expectedDecisionKind && artifact.decisionKind !== expectedDecisionKind) throw invalid("Semantic decision kind binding is invalid.");
20
+ if (expectedQuestionSetId && artifact.questionSetId !== expectedQuestionSetId) throw invalid("Semantic decision question-set binding is invalid.");
21
+ if (artifact.engine !== DECISION_ENGINE_ID || artifact.model !== PINNED_JEV_MODEL) throw decisionError(DECISION_ERROR_CODES.MODEL_UNSUPPORTED, "Semantic decision is not bound to the pinned Jev engine and model.");
22
+ if (artifact.policyVersion !== DECISION_POLICY_VERSION) throw invalid("Semantic decision policy version is invalid.");
23
+ }
24
+
25
+ function assertArtifactShape(artifact) {
26
+ if (artifact.schemaVersion !== 1 || artifact.protocolVersion !== 1
27
+ || !artifact.answers || typeof artifact.answers !== "object" || Array.isArray(artifact.answers)
28
+ || !artifact.confidence || typeof artifact.confidence !== "object" || Array.isArray(artifact.confidence)
29
+ || !artifact.decision || typeof artifact.decision !== "object" || Array.isArray(artifact.decision)
30
+ || !artifact.usage || typeof artifact.usage !== "object" || Array.isArray(artifact.usage)) {
31
+ throw invalid("Semantic decision artifact schema is invalid.");
32
+ }
33
+ }
34
+
35
+ function assertArtifactAuthority(artifact) {
36
+ if (artifact.authority !== "SEMANTIC_DECISION" || artifact.evidenceAuthority !== "NONE"
37
+ || artifact.lifecycleAuthority !== false || artifact.completionAuthority !== false
38
+ || artifact.ownershipAuthority !== false || artifact.installationAuthority !== false) {
39
+ throw invalid("Semantic decision authority boundary is invalid.");
40
+ }
41
+ }
42
+
43
+ function assertArtifactQuestionSet(artifact) {
44
+ const questionSet = artifact.questionSet ?? getQuestionSet(artifact.questionSetId);
45
+ if (questionSet.id !== artifact.questionSetId || questionSet.decisionKind === undefined) throw invalid("Semantic decision question-set identity is invalid.");
46
+ if (artifact.questionSetVersion !== questionSet.version || artifact.questionSetFingerprint !== questionSet.fingerprint) {
47
+ throw invalid("Semantic decision question-set fingerprint is stale.");
48
+ }
49
+ if (typeof artifact.decisionId !== "string" || artifact.decisionId.length === 0) {
50
+ throw invalid("Semantic decision ID is invalid.");
51
+ }
52
+ }
53
+
54
+ function assertCanonicalArtifact(artifact, options = {}) {
55
+ assertArtifactIdentity(artifact, options);
56
+ assertArtifactShape(artifact);
57
+ assertArtifactAuthority(artifact);
58
+ assertArtifactQuestionSet(artifact);
59
+ return artifact;
60
+ }
61
+
62
+ function decisionRecordForArtifact(events, artifact) {
63
+ return events.find((event) => event.event === SEMANTIC_DECISION_RECORDED_EVENT
64
+ && event.taskId === artifact.taskId
65
+ && event.details?.decisionId === artifact.decisionId) ?? null;
66
+ }
67
+
68
+ function isSuperseded(events, artifact) {
69
+ return events.some((event) => event.event === SEMANTIC_DECISION_SUPERSEDED_EVENT
70
+ && event.taskId === artifact.taskId
71
+ && event.details?.decisionId === artifact.decisionId);
72
+ }
73
+
74
+ export function assertRequiredFreshDecision({ artifact, currentBindings = {}, expectedDecisionKind, expectedTaskId, expectedQuestionSetId, ledger = [] } = {}) {
75
+ assertCanonicalArtifact(artifact, { expectedDecisionKind, expectedTaskId, expectedQuestionSetId });
76
+ if (!Array.isArray(ledger)) throw invalid("Semantic decision ledger is unavailable.", DECISION_ERROR_CODES.LEDGER_INVALID);
77
+ const record = decisionRecordForArtifact(ledger, artifact);
78
+ if (!record || record.details.artifactFingerprint !== canonicalFingerprint(artifact)) {
79
+ throw invalid("Semantic decision has no matching canonical ledger record.", DECISION_ERROR_CODES.LEDGER_INVALID);
80
+ }
81
+ if (isSuperseded(ledger, artifact)) throw decisionError(DECISION_ERROR_CODES.STALE, "Semantic decision has been superseded.");
82
+ assertDecisionFresh(artifact, {
83
+ ...currentBindings,
84
+ model: PINNED_JEV_MODEL,
85
+ policyFingerprint: currentBindings.policyFingerprint ?? artifact.policyFingerprint,
86
+ });
87
+ return artifact;
88
+ }
89
+
90
+ async function decisionArtifacts(target, taskId, packageRoot) {
91
+ const directory = taskDecisionDirectory(taskId);
92
+ const absolute = ensureWithin(target, directory);
93
+ if (!(await fileExists(absolute))) return [];
94
+ const names = (await readdir(absolute, { withFileTypes: true }))
95
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
96
+ .map((entry) => entry.name.slice(0, -5))
97
+ .sort();
98
+ const artifacts = [];
99
+ for (const decisionId of names) {
100
+ try {
101
+ artifacts.push((await readJsonArtifact(target, `${directory}/${decisionId}.json`, "semantic-decision", packageRoot)).value);
102
+ } catch {
103
+ // The canonical resolver reports a missing/invalid candidate below.
104
+ }
105
+ }
106
+ return artifacts;
107
+ }
108
+
109
+ export async function resolveRequiredSemanticDecision({ target, packageRoot, taskId, decisionKind, decisionId = null, currentBindings = {} } = {}) {
110
+ if (typeof taskId !== "string" || !taskId) throw decisionError(DECISION_ERROR_CODES.REQUIRED, "A task-bound semantic decision is required.");
111
+ const ledgerResult = await validateEventLedger(target, packageRoot, { taskId });
112
+ if (!ledgerResult.valid) throw decisionError(DECISION_ERROR_CODES.LEDGER_INVALID, "The task ledger is invalid for semantic decision resolution.", ledgerResult.errors);
113
+ const events = await readEvents(target, packageRoot, { taskId });
114
+ const candidates = decisionId
115
+ ? [(await readJsonArtifact(target, `${taskDecisionDirectory(taskId)}/${decisionId}.json`, "semantic-decision", packageRoot)).value]
116
+ : await decisionArtifacts(target, taskId, packageRoot);
117
+ const records = events
118
+ .filter((event) => event.event === SEMANTIC_DECISION_RECORDED_EVENT && event.taskId === taskId && (!decisionKind || event.details?.decisionKind === decisionKind))
119
+ .sort((left, right) => right.seq - left.seq);
120
+ for (const record of records) {
121
+ const artifact = candidates.find((candidate) => candidate.decisionId === record.details?.decisionId);
122
+ if (!artifact || isSuperseded(events, artifact)) continue;
123
+ try {
124
+ return assertRequiredFreshDecision({ artifact, currentBindings, expectedDecisionKind: decisionKind, expectedTaskId: taskId, ledger: events });
125
+ } catch (error) {
126
+ if (decisionId) throw error;
127
+ }
128
+ }
129
+ throw decisionError(DECISION_ERROR_CODES.REQUIRED, `No fresh canonical Jev decision is available for ${decisionKind ?? "the requested operation"}.`);
130
+ }
@@ -0,0 +1,58 @@
1
+ import { PINNED_JEV_MODEL } from "./constants.js";
2
+ import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
3
+ import { normalizeCanonicalDecision } from "./normalizers/index.js";
4
+
5
+ function finite(value) { return typeof value === "number" && Number.isFinite(value); }
6
+
7
+ function normalizeAnswer(value) {
8
+ if (value === null || typeof value === "string" || typeof value === "boolean" || finite(value)) return value;
9
+ if (value && typeof value === "object" && !Array.isArray(value)) {
10
+ const result = {};
11
+ for (const [key, child] of Object.entries(value)) result[key] = normalizeAnswer(child);
12
+ return result;
13
+ }
14
+ if (Array.isArray(value)) return value.slice(0, 64).map(normalizeAnswer);
15
+ throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, "Semantic decision answer has an unsupported shape.");
16
+ }
17
+
18
+ function normalizeAnswers(rawAnswers, questionSet) {
19
+ return Object.fromEntries(Object.keys(questionSet.questions).map((key) => {
20
+ if (!Object.prototype.hasOwnProperty.call(rawAnswers, key)) {
21
+ throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, `Semantic decision result is missing answer ${key}.`);
22
+ }
23
+ return [key, normalizeAnswer(rawAnswers[key])];
24
+ }));
25
+ }
26
+
27
+ function normalizeConfidence(confidence, questionSet) {
28
+ if (!confidence || typeof confidence !== "object" || Array.isArray(confidence)) {
29
+ throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, "Semantic decision result confidence is invalid.");
30
+ }
31
+ return Object.fromEntries(Object.keys(questionSet.questions).map((key) => [key, confidence[key] ?? null]));
32
+ }
33
+
34
+ function normalizeUsage(raw) {
35
+ const inputTokens = raw?.input_tokens ?? raw?.inputTokens;
36
+ const outputTokens = raw?.output_tokens ?? raw?.outputTokens;
37
+ if (!Number.isInteger(inputTokens) || inputTokens < 0 || !Number.isInteger(outputTokens) || outputTokens < 0) {
38
+ throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, "Semantic decision result usage is invalid.");
39
+ }
40
+ return { inputTokens, outputTokens, reportedBy: "PROVIDER" };
41
+ }
42
+
43
+ export function normalizeDecisionResult(raw, { questionSet, input = {}, model = PINNED_JEV_MODEL } = {}) {
44
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, "Semantic decision result must be an object.");
45
+ if (raw.model !== model || raw.model !== PINNED_JEV_MODEL) throw decisionError(DECISION_ERROR_CODES.MODEL_UNSUPPORTED, "Semantic decision result used an unsupported model.");
46
+ if (!raw.answers || typeof raw.answers !== "object" || Array.isArray(raw.answers)) throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, "Semantic decision result is missing answers.");
47
+ const answers = normalizeAnswers(raw.answers, questionSet);
48
+ const confidence = normalizeConfidence(raw.confidence, questionSet);
49
+ const usage = normalizeUsage(raw.usage);
50
+ return {
51
+ model,
52
+ questionSetId: questionSet?.id ?? null,
53
+ answers,
54
+ confidence: Object.fromEntries(Object.keys(questionSet.questions).map((key) => [key, confidence[key] ?? null])),
55
+ decision: normalizeCanonicalDecision({ questionSet, answers, confidence, input }),
56
+ usage,
57
+ };
58
+ }
@@ -0,0 +1,156 @@
1
+ import { canonicalFingerprint } from "../artifacts.js";
2
+ import { readConfig } from "../config.js";
3
+ import { appendProtocolEvent, readEvents } from "../events.js";
4
+ import { withTaskMutation } from "../task-command.js";
5
+ import { DECISION_DEFAULT_POLICY, PINNED_JEV_MODEL, SEMANTIC_DECISION_RECORDED_EVENT } from "./constants.js";
6
+ import { buildDecisionArtifact, decisionArtifactFingerprint, readDecisionArtifact, writeDecisionArtifact } from "./artifact.js";
7
+ import { decisionEventDetails, decisionSupersededEventDetails } from "./events.js";
8
+ import { createTypesafeEngine } from "../../adapters/typesafe/engine.js";
9
+ import { normalizeDecisionPolicy, decisionPolicyFingerprint } from "./policy.js";
10
+ import { validateDecisionRequest } from "./request.js";
11
+ import { normalizeDecisionResult } from "./result.js";
12
+ import { randomUUID } from "node:crypto";
13
+ import { readCurrentDecisionBindings } from "./task-bindings.js";
14
+ import { resolveRequiredSemanticDecision } from "./resolver.js";
15
+ import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
16
+ import { getTaskTransaction } from "../transaction.js";
17
+ import { getTestSemanticProvider } from "./test-provider.js";
18
+
19
+ function resolveProvider(provider) {
20
+ return provider ?? getTestSemanticProvider();
21
+ }
22
+
23
+ async function loadPolicy(target, packageRoot) {
24
+ try {
25
+ const config = await readConfig(target, packageRoot);
26
+ return normalizeDecisionPolicy(config.decisionEngine ?? DECISION_DEFAULT_POLICY);
27
+ } catch (error) {
28
+ if (error.code === "ARTIFACT_MISSING") return normalizeDecisionPolicy(DECISION_DEFAULT_POLICY);
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ async function readCachedDecision({ target, packageRoot, taskId, decisionKind, currentBindings }) {
34
+ try {
35
+ return await resolveRequiredSemanticDecision({ target, packageRoot, taskId, decisionKind, currentBindings });
36
+ } catch (error) {
37
+ if (["E_DECISION_REQUIRED", "E_DECISION_STALE", "E_DECISION_BINDING_INVALID"].includes(error.code)) return null;
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ function buildArtifact({ taskId, decisionId, policy, validated, result, request, taskBindings }) {
43
+ return buildDecisionArtifact({
44
+ taskId,
45
+ decisionId,
46
+ decisionKind: validated.decisionKind,
47
+ model: result.model ?? PINNED_JEV_MODEL,
48
+ questionSetId: validated.questionSet.id,
49
+ questionSetVersion: validated.questionSet.version,
50
+ questionSetFingerprint: validated.questionSet.fingerprint,
51
+ questionSet: validated.questionSet,
52
+ stateFingerprint: canonicalFingerprint(validated.state),
53
+ taskStateFingerprint: canonicalFingerprint(validated.state.lifecycle),
54
+ semanticStateFingerprint: canonicalFingerprint(validated.state.semantic),
55
+ policyVersion: policy.policyVersion,
56
+ policyFingerprint: decisionPolicyFingerprint(policy),
57
+ repositoryFingerprint: request.repositoryFingerprint ?? taskBindings.repositoryFingerprint,
58
+ contractFingerprint: request.contractFingerprint ?? taskBindings.contractFingerprint,
59
+ routeFingerprint: request.routeFingerprint ?? taskBindings.routeFingerprint,
60
+ verificationCycle: request.verificationCycle ?? taskBindings.verificationCycle,
61
+ candidateSetFingerprint: request.candidateSetFingerprint,
62
+ answers: result.answers,
63
+ confidence: result.confidence ?? {},
64
+ decision: result.decision,
65
+ usage: result.usage,
66
+ latencyMs: result.latencyMs ?? null,
67
+ });
68
+ }
69
+
70
+ async function readCachedSemanticDecision({ policy, decisionId, target, taskId, packageRoot, validated, taskBindings, request }) {
71
+ if (!policy.cache || decisionId || !target || !taskId || await getTaskTransaction(target)) return null;
72
+ return readCachedDecision({
73
+ target,
74
+ packageRoot,
75
+ taskId,
76
+ decisionKind: validated.decisionKind,
77
+ currentBindings: {
78
+ ...taskBindings,
79
+ stateFingerprint: canonicalFingerprint(validated.state),
80
+ taskStateFingerprint: canonicalFingerprint(validated.state.lifecycle),
81
+ semanticStateFingerprint: canonicalFingerprint(validated.state.semantic),
82
+ questionSetFingerprint: validated.questionSet.fingerprint,
83
+ policyFingerprint: decisionPolicyFingerprint(policy),
84
+ model: PINNED_JEV_MODEL,
85
+ ...(request?.candidateSetFingerprint !== undefined ? { candidateSetFingerprint: request.candidateSetFingerprint } : {}),
86
+ ...(request?.policyFingerprint !== undefined ? { policyFingerprint: request.policyFingerprint } : {}),
87
+ },
88
+ });
89
+ }
90
+
91
+ async function persistDecision(target, packageRoot, resolvedDecisionId, artifact, ctx) {
92
+ await writeDecisionArtifact(target, ctx.taskId, resolvedDecisionId, artifact, packageRoot, { taskId: ctx.taskId, operation: "semantic-decision" });
93
+ const priorEvents = await readEvents(target, packageRoot, { taskId: ctx.taskId });
94
+ const event = await appendProtocolEvent(target, { taskId: ctx.taskId, event: SEMANTIC_DECISION_RECORDED_EVENT, details: decisionEventDetails(artifact) }, packageRoot, { taskId: ctx.taskId });
95
+ const previous = priorEvents.findLast((candidate) => candidate.event === SEMANTIC_DECISION_RECORDED_EVENT
96
+ && candidate.taskId === ctx.taskId
97
+ && candidate.details?.decisionKind === artifact.decisionKind
98
+ && !priorEvents.some((superseded) => superseded.event === "SEMANTIC_DECISION_SUPERSEDED"
99
+ && superseded.details?.decisionId === candidate.details?.decisionId));
100
+ if (!previous) return { artifact, artifactFingerprint: decisionArtifactFingerprint(artifact), event, supersededEvent: null };
101
+ const previousArtifact = await readDecisionArtifact(target, ctx.taskId, previous.details.decisionId, packageRoot);
102
+ const supersededEvent = await appendProtocolEvent(target, {
103
+ taskId: ctx.taskId,
104
+ event: "SEMANTIC_DECISION_SUPERSEDED",
105
+ details: decisionSupersededEventDetails(previousArtifact.value, resolvedDecisionId),
106
+ }, packageRoot, { taskId: ctx.taskId });
107
+ return { artifact, artifactFingerprint: decisionArtifactFingerprint(artifact), event, supersededEvent };
108
+ }
109
+
110
+ export async function recordSemanticDecision({ target, packageRoot, taskId, decisionId, request, provider = null } = {}) {
111
+ const resolvedDecisionId = decisionId ?? `${String(request?.decisionKind ?? "decision").toLowerCase()}-${Date.now()}-${randomUUID().slice(0, 8)}`;
112
+ const policy = await loadPolicy(target, packageRoot);
113
+ const taskBindings = target && taskId ? await readCurrentDecisionBindings(target, packageRoot, taskId) : {};
114
+ const validated = validateDecisionRequest({
115
+ ...request,
116
+ taskId,
117
+ state: { lifecycle: taskBindings.state ?? {}, semantic: request?.state ?? {} },
118
+ });
119
+ const cached = await readCachedSemanticDecision({ policy, decisionId, target, taskId, packageRoot, validated, taskBindings, request });
120
+ if (cached) return { artifact: cached, artifactFingerprint: decisionArtifactFingerprint(cached), event: null, supersededEvent: null, cached: true, policy };
121
+ const engine = resolveProvider(provider) ?? createTypesafeEngine({ policy });
122
+ if (provider && (provider.id !== "typesafe-jev" || provider.model !== PINNED_JEV_MODEL || typeof provider.evaluate !== "function")) {
123
+ throw decisionError(DECISION_ERROR_CODES.MODEL_UNSUPPORTED, "Semantic decision provider must be the pinned ForgeLoop Jev engine.");
124
+ }
125
+ const result = normalizeDecisionResult(await engine.evaluate(validated), { questionSet: validated.questionSet, input: validated });
126
+ const artifact = buildArtifact({ taskId, decisionId: resolvedDecisionId, policy, validated, result, request, taskBindings });
127
+ const persisted = await withTaskMutation(target, { taskId, packageRoot }, "semantic-decision", (ctx) => persistDecision(target, packageRoot, resolvedDecisionId, artifact, ctx), { explicitRequired: true });
128
+ return { ...persisted, policy };
129
+ }
130
+
131
+ export async function ensureSemanticDecision({ target, packageRoot, taskId, decisionId, request, provider = null, allowNetwork = true } = {}) {
132
+ const policy = await loadPolicy(target, packageRoot);
133
+ const taskBindings = await readCurrentDecisionBindings(target, packageRoot, taskId);
134
+ const validated = validateDecisionRequest({
135
+ ...request,
136
+ taskId,
137
+ state: { lifecycle: taskBindings.state ?? {}, semantic: request?.state ?? {} },
138
+ });
139
+ const currentBindings = {
140
+ ...taskBindings,
141
+ stateFingerprint: canonicalFingerprint(validated.state),
142
+ taskStateFingerprint: canonicalFingerprint(validated.state.lifecycle),
143
+ semanticStateFingerprint: canonicalFingerprint(validated.state.semantic),
144
+ questionSetFingerprint: validated.questionSet.fingerprint,
145
+ policyFingerprint: decisionPolicyFingerprint(policy),
146
+ model: PINNED_JEV_MODEL,
147
+ ...(request?.candidateSetFingerprint !== undefined ? { candidateSetFingerprint: request.candidateSetFingerprint } : {}),
148
+ };
149
+ const cached = await getTaskTransaction(target) ? null : await readCachedDecision({
150
+ target, packageRoot, taskId, decisionKind: validated.decisionKind,
151
+ currentBindings,
152
+ });
153
+ if (cached) return { artifact: cached, artifactFingerprint: decisionArtifactFingerprint(cached), cached: true, policy };
154
+ if (!allowNetwork && !provider) throw decisionError(DECISION_ERROR_CODES.REQUIRED, `A current ${validated.decisionKind} decision is required.`);
155
+ return recordSemanticDecision({ target, packageRoot, taskId, decisionId, request: { ...request, questionSet: validated.questionSet }, provider });
156
+ }
@@ -0,0 +1,65 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { DECISION_LIMITS } from "./constants.js";
3
+ import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
4
+
5
+ const SECRET_KEY = /(^|[_-])(api[_-]?key|password|passwd|secret|token|refresh|cookie|authorization|private[_-]?key|credential|database[_-]?url|connection[_-]?string|access[_-]?key|client[_-]?secret)([_-]|$)/i;
6
+ const SECRET_VALUE = /^(?:bearer\s+)?(?:sk-|pk-|ghp_|github_pat_|xox[baprs]-|AIza|AKIA)[A-Za-z0-9._-]{12,}$/i;
7
+
8
+ function isHomePath(value) {
9
+ return typeof value === "string" && (/^\/Users\/[^/]+\//.test(value) || /^\/home\/[^/]+\//.test(value));
10
+ }
11
+
12
+ function sanitize(value, depth, counters) {
13
+ if (depth > DECISION_LIMITS.maxDepth) throw decisionError(DECISION_ERROR_CODES.STATE_LIMIT, "Decision state nesting exceeds the supported limit.");
14
+ if (value === null || typeof value === "boolean" || typeof value === "number") return value;
15
+ if (typeof value === "string") {
16
+ if (/[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD]/u.test(value)) throw decisionError(DECISION_ERROR_CODES.STATE_UNSAFE, "Decision state contains unsafe control characters.");
17
+ if (SECRET_VALUE.test(value)) throw decisionError(DECISION_ERROR_CODES.STATE_UNSAFE, "Decision state contains a secret-like value.");
18
+ if (isHomePath(value)) return "<redacted-user-path>";
19
+ counters.bytes += Buffer.byteLength(value);
20
+ if (value.length > DECISION_LIMITS.maxStringChars) return `${value.slice(0, DECISION_LIMITS.maxStringChars)}…<truncated>`;
21
+ return value;
22
+ }
23
+ if (Array.isArray(value)) {
24
+ if (value.length > DECISION_LIMITS.maxItems) throw decisionError(DECISION_ERROR_CODES.STATE_LIMIT, "Decision state contains too many items.");
25
+ return value.map((item) => sanitize(item, depth + 1, counters));
26
+ }
27
+ if (typeof value === "object") {
28
+ const entries = Object.entries(value);
29
+ if (entries.length > DECISION_LIMITS.maxItems) throw decisionError(DECISION_ERROR_CODES.STATE_LIMIT, "Decision state contains too many fields.");
30
+ const result = {};
31
+ for (const [key, child] of entries) {
32
+ if (SECRET_KEY.test(key) || key.toLowerCase() === "env" || key.toLowerCase() === "environment") continue;
33
+ result[key] = sanitize(child, depth + 1, counters);
34
+ }
35
+ return result;
36
+ }
37
+ throw decisionError(DECISION_ERROR_CODES.STATE_UNSAFE, "Decision state contains an unsupported value.");
38
+ }
39
+
40
+ export function buildDecisionState(value) {
41
+ const counters = { bytes: 0 };
42
+ const sanitized = sanitize(value, 0, counters);
43
+ const bytes = Buffer.byteLength(JSON.stringify(sanitized));
44
+ if (bytes > DECISION_LIMITS.maxStateBytes) throw decisionError(DECISION_ERROR_CODES.STATE_LIMIT, "Decision state exceeds the supported byte limit.");
45
+ return sanitized;
46
+ }
47
+
48
+ export function buildLifecycleDecisionState(value = {}) {
49
+ return buildDecisionState(value ?? {});
50
+ }
51
+
52
+ export function buildSemanticDecisionState(value = {}) {
53
+ return buildDecisionState(value ?? {});
54
+ }
55
+
56
+ export function buildCombinedDecisionState({ lifecycle = {}, semantic = {} } = {}) {
57
+ return {
58
+ lifecycle: buildLifecycleDecisionState(lifecycle),
59
+ semantic: buildSemanticDecisionState(semantic),
60
+ };
61
+ }
62
+
63
+ export function decisionStateFingerprint(value, canonicalFingerprint) {
64
+ return canonicalFingerprint(buildDecisionState(value));
65
+ }
@@ -0,0 +1,30 @@
1
+ import { canonicalFingerprint } from "../artifacts.js";
2
+ import { readContract } from "../contract.js";
3
+ import { readPersistedRoute } from "../route-artifact.js";
4
+ import { readWorkState } from "../work-state.js";
5
+
6
+ async function readOptionalArtifact(reader) {
7
+ try {
8
+ return await reader();
9
+ } catch (error) {
10
+ if (error.code === "ARTIFACT_MISSING") return null;
11
+ throw error;
12
+ }
13
+ }
14
+
15
+ export async function readCurrentDecisionBindings(target, packageRoot, taskId, { candidateSetFingerprint, policyFingerprint } = {}) {
16
+ const state = await readWorkState(target, { packageRoot, taskId });
17
+ const contract = await readOptionalArtifact(() => readContract(target, packageRoot, { taskId }));
18
+ const route = await readOptionalArtifact(() => readPersistedRoute(target, packageRoot, { taskId }));
19
+ return {
20
+ state,
21
+ ...(state ? { stateFingerprint: canonicalFingerprint(state) } : {}),
22
+ ...(state ? { taskStateFingerprint: canonicalFingerprint(state) } : {}),
23
+ ...(state?.repositoryFingerprint ? { repositoryFingerprint: state.repositoryFingerprint } : {}),
24
+ ...((contract?.fingerprint ?? state?.contractFingerprint) ? { contractFingerprint: contract?.fingerprint ?? state.contractFingerprint } : {}),
25
+ ...((route?.fingerprint ?? state?.routeFingerprint) ? { routeFingerprint: route?.fingerprint ?? state.routeFingerprint } : {}),
26
+ ...(state?.verificationCycle !== undefined ? { verificationCycle: state.verificationCycle } : {}),
27
+ ...(candidateSetFingerprint !== undefined ? { candidateSetFingerprint } : {}),
28
+ ...(policyFingerprint !== undefined ? { policyFingerprint } : {}),
29
+ };
30
+ }
@@ -0,0 +1,32 @@
1
+ // This provider is deliberately reachable only through repository test injection.
2
+ // Production commands always use the pinned TypeSafe Jev engine or fail closed.
3
+ export const testSemanticProvider = Object.freeze({
4
+ id: "typesafe-jev",
5
+ model: "jev-1.13.0",
6
+ async evaluate(request) {
7
+ const answers = Object.fromEntries(Object.entries(request.questionSet.questions).map(([name, question]) => [
8
+ name,
9
+ question.type === "choice" ? Object.keys(question.criteria)[0] : { noul: true },
10
+ ]));
11
+ return {
12
+ model: "jev-1.13.0",
13
+ answers,
14
+ confidence: Object.fromEntries(Object.keys(answers).map((name) => [name, 0.95])),
15
+ usage: { inputTokens: 0, outputTokens: 0 },
16
+ };
17
+ },
18
+ });
19
+
20
+ let activeTestSemanticProvider = null;
21
+
22
+ export function installTestSemanticProvider(provider = testSemanticProvider) {
23
+ activeTestSemanticProvider = provider;
24
+ }
25
+
26
+ export function clearTestSemanticProvider() {
27
+ activeTestSemanticProvider = null;
28
+ }
29
+
30
+ export function getTestSemanticProvider() {
31
+ return activeTestSemanticProvider;
32
+ }
@@ -0,0 +1,15 @@
1
+ export const DECISION_RISK_TIERS = Object.freeze(["LOW_RISK", "NORMAL", "HIGH_RISK", "CRITICAL"]);
2
+ export const DECISION_THRESHOLDS = Object.freeze({
3
+ LOW_RISK: 0.65,
4
+ NORMAL: 0.75,
5
+ HIGH_RISK: 0.9,
6
+ CRITICAL: 0.97,
7
+ });
8
+
9
+ export function decisionConfidenceThreshold(risk = "NORMAL") {
10
+ return DECISION_THRESHOLDS[risk] ?? DECISION_THRESHOLDS.NORMAL;
11
+ }
12
+
13
+ export function requiresEscalation(confidence, risk = "NORMAL") {
14
+ return typeof confidence !== "number" || confidence < decisionConfidenceThreshold(risk);
15
+ }