@cassiomc1/forgeloop 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/AGENT_COMPATIBILITY.md +8 -0
  2. package/DOCS_INDEX.md +35 -3
  3. package/ENG/nodejs-backend-development-eng.md +2 -2
  4. package/ENG/sec-code-eng.md +7 -7
  5. package/EXECUTION_STATE.md +12 -0
  6. package/LOOP_ENGINEERING.md +28 -2
  7. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  8. package/PROTOCOL_INTEGRATION.md +55 -2
  9. package/README.md +40 -25
  10. package/TERMINOLOGY.md +2 -0
  11. package/THREAT_MODEL.md +140 -1
  12. package/completions/_forgeloop +19 -1
  13. package/completions/forgeloop.bash +37 -1
  14. package/completions/forgeloop.fish +123 -1
  15. package/docs/ADVISORY_CONTEXT.md +25 -0
  16. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  17. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  18. package/docs/AGENT_PROTOCOL_SUMMARY.md +27 -2
  19. package/docs/AGENT_SKILL.md +66 -0
  20. package/docs/ARTIFACT_REFERENCE.md +123 -0
  21. package/docs/AUDIT_UX.md +46 -0
  22. package/docs/BROWSER_VERIFICATION.md +136 -0
  23. package/docs/CLI_REFERENCE.md +366 -6
  24. package/docs/CODE_ATTESTATION.md +2 -2
  25. package/docs/DOCUMENTATION_GUIDE.md +32 -11
  26. package/docs/JEV_BENCHMARKS.md +31 -0
  27. package/docs/MODEL_ROUTING.md +37 -0
  28. package/docs/OPENSRC_ADAPTER.md +241 -0
  29. package/docs/PACKAGE_CONTENTS.md +35 -8
  30. package/docs/PROVIDERS.md +126 -0
  31. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  32. package/docs/RECIPES.md +9 -0
  33. package/docs/RELEASE_CHECKLIST.md +38 -5
  34. package/docs/SECURITY_REVIEW.md +71 -0
  35. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  36. package/docs/TEST_INTELLIGENCE.md +29 -0
  37. package/docs/TEST_PRUNING.md +14 -0
  38. package/docs/TROUBLESHOOTING.md +198 -1
  39. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  40. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  41. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  42. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  43. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  44. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  45. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  46. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  47. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  48. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  49. package/docs/diagrams/README.md +13 -9
  50. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  51. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  52. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  53. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  54. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  55. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  56. package/docs/documentation-manifest.json +750 -5
  57. package/docs/protocol-requirements.json +24 -0
  58. package/package.json +30 -3
  59. package/schemas/config.schema.json +14 -0
  60. package/schemas/context-plan.schema.json +18 -0
  61. package/schemas/semantic-decision.schema.json +46 -0
  62. package/schemas/test-utility.schema.json +44 -0
  63. package/scripts/CI_VALIDATORS.md +6 -6
  64. package/scripts/benchmark-jev.mjs +5 -0
  65. package/scripts/benchmark-test-intelligence.mjs +4 -0
  66. package/scripts/generate-agent-protocol-summary.mjs +4 -1
  67. package/scripts/generate-forgeloop-skill.mjs +133 -0
  68. package/scripts/jev-smoke.mjs +19 -0
  69. package/skills/forgeloop/README.md +9 -0
  70. package/skills/forgeloop/SKILL.md +77 -0
  71. package/skills/forgeloop/references/lifecycle.md +9 -0
  72. package/skills/forgeloop/references/recovery.md +7 -0
  73. package/skills/forgeloop/references/verification.md +7 -0
  74. package/src/adapters/agent-browser/assertions.js +47 -0
  75. package/src/adapters/agent-browser/commands.js +54 -0
  76. package/src/adapters/agent-browser/index.js +3 -0
  77. package/src/adapters/agent-browser/locator.js +40 -0
  78. package/src/adapters/agent-browser/process.js +215 -0
  79. package/src/adapters/agent-browser/provider.js +313 -0
  80. package/src/adapters/emulated-services/constants.js +24 -0
  81. package/src/adapters/emulated-services/index.js +7 -0
  82. package/src/adapters/emulated-services/process.js +162 -0
  83. package/src/adapters/emulated-services/provider.js +282 -0
  84. package/src/adapters/opensrc/normalize.js +90 -0
  85. package/src/adapters/opensrc/process.js +248 -0
  86. package/src/adapters/opensrc/provider.js +338 -0
  87. package/src/adapters/opensrc/search.js +264 -0
  88. package/src/adapters/typesafe/client.js +28 -0
  89. package/src/adapters/typesafe/engine.js +63 -0
  90. package/src/adapters/typesafe/normalize.js +41 -0
  91. package/src/cli.js +108 -0
  92. package/src/commands/checkpoint-revalidate.js +176 -0
  93. package/src/commands/context-plan.js +38 -0
  94. package/src/commands/contract-create.js +264 -0
  95. package/src/commands/contract-revise.js +236 -0
  96. package/src/commands/decision-show.js +14 -0
  97. package/src/commands/decision-status.js +22 -0
  98. package/src/commands/discover.js +41 -0
  99. package/src/commands/doctor.js +15 -0
  100. package/src/commands/gate-record.js +205 -0
  101. package/src/commands/gate-revalidate.js +137 -0
  102. package/src/commands/model-route.js +32 -0
  103. package/src/commands/route.js +146 -18
  104. package/src/commands/semantic-plan.js +17 -0
  105. package/src/commands/task-abandon.js +224 -0
  106. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  107. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  108. package/src/commands/test-inventory.js +5 -0
  109. package/src/commands/test-prune-plan.js +5 -0
  110. package/src/commands/test-prune-probe.js +5 -0
  111. package/src/commands/test-utility.js +5 -0
  112. package/src/commands/validate-protocol.js +10 -1
  113. package/src/core/artifact-registry.js +24 -0
  114. package/src/core/audit-ux.js +514 -0
  115. package/src/core/browser-verification/constants.js +149 -0
  116. package/src/core/browser-verification/normalize.js +254 -0
  117. package/src/core/browser-verification/provider.js +519 -0
  118. package/src/core/browser-verification/service.js +115 -0
  119. package/src/core/checkpoint-revalidation.js +319 -0
  120. package/src/core/cli-command-definitions.js +241 -0
  121. package/src/core/command-executors.js +110 -0
  122. package/src/core/command-input.js +115 -43
  123. package/src/core/completion-artifacts.js +14 -5
  124. package/src/core/completion.js +4 -6
  125. package/src/core/config.js +3 -0
  126. package/src/core/context-compiler/budget.js +9 -0
  127. package/src/core/context-compiler/candidates.js +39 -0
  128. package/src/core/context-compiler/compiler.js +63 -0
  129. package/src/core/context-compiler/fingerprint.js +11 -0
  130. package/src/core/context-compiler/policy.js +13 -0
  131. package/src/core/context-compiler/result.js +23 -0
  132. package/src/core/contract-bootstrap-recovery.js +655 -0
  133. package/src/core/contract-revision.js +210 -0
  134. package/src/core/decision/artifact.js +69 -0
  135. package/src/core/decision/benchmarks.js +103 -0
  136. package/src/core/decision/cache.js +27 -0
  137. package/src/core/decision/constants.js +58 -0
  138. package/src/core/decision/cutover.js +34 -0
  139. package/src/core/decision/engine.js +22 -0
  140. package/src/core/decision/errors.js +68 -0
  141. package/src/core/decision/events.js +101 -0
  142. package/src/core/decision/freshness.js +19 -0
  143. package/src/core/decision/normalizers/index.js +115 -0
  144. package/src/core/decision/policy.js +18 -0
  145. package/src/core/decision/projection.js +16 -0
  146. package/src/core/decision/question-registry.js +201 -0
  147. package/src/core/decision/request.js +26 -0
  148. package/src/core/decision/resolver.js +130 -0
  149. package/src/core/decision/result.js +58 -0
  150. package/src/core/decision/service.js +156 -0
  151. package/src/core/decision/state-builder.js +65 -0
  152. package/src/core/decision/task-bindings.js +30 -0
  153. package/src/core/decision/test-provider.js +32 -0
  154. package/src/core/decision/thresholds.js +15 -0
  155. package/src/core/error-codes.js +278 -0
  156. package/src/core/events.js +226 -57
  157. package/src/core/evidence-readiness.js +9 -0
  158. package/src/core/execution-prerequisites.js +14 -0
  159. package/src/core/execution-profile.js +63 -38
  160. package/src/core/gate-provenance.js +124 -0
  161. package/src/core/integration-invocation-policy.js +27 -4
  162. package/src/core/integration-resources.js +86 -61
  163. package/src/core/model-router/constants.js +10 -0
  164. package/src/core/model-router/policy.js +103 -0
  165. package/src/core/model-router/router.js +37 -0
  166. package/src/core/next-action-model.js +58 -0
  167. package/src/core/next-action-phases.js +130 -42
  168. package/src/core/next-action-refresh.js +43 -9
  169. package/src/core/next-action-review-phase.js +7 -2
  170. package/src/core/next-action.js +35 -7
  171. package/src/core/phase.js +128 -10
  172. package/src/core/preflight-consistency.js +23 -9
  173. package/src/core/preflight-loaders.js +37 -5
  174. package/src/core/protocol-info.js +65 -0
  175. package/src/core/protocol.js +20 -0
  176. package/src/core/reconcile-closure.js +128 -52
  177. package/src/core/recovery-history.js +1 -0
  178. package/src/core/resumability.js +154 -44
  179. package/src/core/route-artifact.js +15 -1
  180. package/src/core/router.js +67 -1
  181. package/src/core/runtime-context.js +118 -61
  182. package/src/core/schema-validation.js +3 -0
  183. package/src/core/security-review/constants.js +64 -0
  184. package/src/core/security-review/normalize.js +245 -0
  185. package/src/core/security-review/provider.js +204 -0
  186. package/src/core/security-review/service.js +134 -0
  187. package/src/core/semantic-planning/constants.js +19 -0
  188. package/src/core/semantic-planning/projection.js +94 -0
  189. package/src/core/semantic-planning/service.js +15 -0
  190. package/src/core/sources.js +37 -0
  191. package/src/core/task-claim-state.js +201 -1
  192. package/src/core/task-conflict-inspection.js +31 -5
  193. package/src/core/task-paths.js +13 -0
  194. package/src/core/task-recovery.js +1 -0
  195. package/src/core/templates.js +3 -0
  196. package/src/core/test-intelligence/benchmarks.js +68 -0
  197. package/src/core/test-intelligence/inventory.js +73 -0
  198. package/src/core/test-intelligence/prune.js +90 -0
  199. package/src/core/test-intelligence/semantic-state.js +15 -0
  200. package/src/core/test-intelligence/service.js +40 -0
  201. package/src/core/test-intelligence/utility.js +50 -0
  202. package/src/core/trace.js +11 -7
  203. package/src/core/transaction.js +1 -0
  204. package/src/integration.d.ts +492 -0
  205. package/src/integration.js +54 -0
  206. package/src/providers/README.md +47 -0
  207. package/src/providers/capabilities.js +46 -0
  208. package/src/providers/errors.js +15 -0
  209. package/src/providers/index.js +29 -0
  210. package/src/providers/json-snapshot.js +105 -0
  211. package/src/providers/registry.js +152 -0
@@ -0,0 +1,24 @@
1
+ export const EMULATED_SERVICES_ID = "emulated-services";
2
+ export const EMULATED_SERVICES_SUPPORTED_VERSION = "0.11.2";
3
+
4
+ export const EMULATED_SERVICES_PROCESS_LIMITS = Object.freeze({
5
+ maxStdoutBytes: 64 * 1024,
6
+ maxStderrBytes: 64 * 1024,
7
+ maxServices: 32,
8
+ maxTimeoutMs: 120_000,
9
+ terminationGraceMs: 250,
10
+ readinessPollMs: 50,
11
+ });
12
+
13
+ export const EMULATED_SERVICES_ERROR_CODES = Object.freeze({
14
+ CONFIG_INVALID: "E_EMULATED_SERVICES_CONFIG_INVALID",
15
+ UNAVAILABLE: "E_EMULATED_SERVICES_UNAVAILABLE",
16
+ VERSION_UNSUPPORTED: "E_EMULATED_SERVICES_VERSION_UNSUPPORTED",
17
+ OUTPUT_INVALID: "E_EMULATED_SERVICES_OUTPUT_INVALID",
18
+ OUTPUT_LIMIT: "E_EMULATED_SERVICES_OUTPUT_LIMIT",
19
+ TIMEOUT: "E_EMULATED_SERVICES_TIMEOUT",
20
+ CANCELLED: "E_EMULATED_SERVICES_CANCELLED",
21
+ EXECUTION_FAILED: "E_EMULATED_SERVICES_EXECUTION_FAILED",
22
+ ENDPOINT_INVALID: "E_EMULATED_SERVICES_ENDPOINT_INVALID",
23
+ CLEANUP_FAILED: "E_EMULATED_SERVICES_CLEANUP_FAILED",
24
+ });
@@ -0,0 +1,7 @@
1
+ export { createEmulatedServicesProvider, normalizeEndpoint, parseVersion } from "./provider.js";
2
+ export {
3
+ EMULATED_SERVICES_ERROR_CODES,
4
+ EMULATED_SERVICES_ID,
5
+ EMULATED_SERVICES_PROCESS_LIMITS,
6
+ EMULATED_SERVICES_SUPPORTED_VERSION,
7
+ } from "./constants.js";
@@ -0,0 +1,162 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+
3
+ import {
4
+ EMULATED_SERVICES_ERROR_CODES,
5
+ EMULATED_SERVICES_PROCESS_LIMITS,
6
+ } from "./constants.js";
7
+
8
+ function processError(code, message, cause) {
9
+ const error = new Error(message, cause ? { cause } : undefined);
10
+ error.name = "EmulatedServicesProcessError";
11
+ error.code = code;
12
+ return error;
13
+ }
14
+
15
+ function byteLength(value) {
16
+ return Buffer.isBuffer(value) ? value.byteLength : Buffer.byteLength(String(value));
17
+ }
18
+
19
+ function terminateChild(child) {
20
+ if (!child || typeof child.kill !== "function") return;
21
+ try {
22
+ child.kill("SIGTERM");
23
+ } catch {
24
+ // The process may have exited between a boundary failure and termination.
25
+ }
26
+ const timer = setTimeout(() => {
27
+ try {
28
+ child.kill("SIGKILL");
29
+ } catch {
30
+ // Preserve the original boundary error.
31
+ }
32
+ }, EMULATED_SERVICES_PROCESS_LIMITS.terminationGraceMs);
33
+ timer.unref?.();
34
+ }
35
+
36
+ function waitForClose(child, alreadyClosed = () => false) {
37
+ if (alreadyClosed()) return Promise.resolve();
38
+ return new Promise((resolve) => {
39
+ let settled = false;
40
+ const finish = () => {
41
+ if (settled) return;
42
+ settled = true;
43
+ resolve();
44
+ };
45
+ child.once?.("close", finish);
46
+ const timer = setTimeout(finish, EMULATED_SERVICES_PROCESS_LIMITS.terminationGraceMs + 1000);
47
+ timer.unref?.();
48
+ });
49
+ }
50
+
51
+ function assertArgv(args) {
52
+ if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string" || /\p{Cc}/u.test(arg))) {
53
+ throw processError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services arguments must be portable strings");
54
+ }
55
+ }
56
+
57
+ export function spawnManagedEmulatedServicesProcess(executablePath, args, options = {}) {
58
+ assertArgv(args);
59
+ const spawnImpl = options.spawnImpl ?? nodeSpawn;
60
+ if (typeof spawnImpl !== "function") {
61
+ throw processError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services spawn implementation must be a function");
62
+ }
63
+
64
+ let child;
65
+ try {
66
+ child = spawnImpl(executablePath, [...args], {
67
+ cwd: options.cwd,
68
+ shell: false,
69
+ stdio: ["ignore", "pipe", "pipe"],
70
+ env: options.env,
71
+ });
72
+ } catch (error) {
73
+ throw processError(EMULATED_SERVICES_ERROR_CODES.UNAVAILABLE, "Unable to start the Emulated Services executable", error);
74
+ }
75
+
76
+ if (!child || !child.stdout || !child.stderr || typeof child.on !== "function") {
77
+ terminateChild(child);
78
+ throw processError(EMULATED_SERVICES_ERROR_CODES.UNAVAILABLE, "Emulated Services process did not expose piped output");
79
+ }
80
+
81
+ const maxStdoutBytes = options.maxStdoutBytes ?? EMULATED_SERVICES_PROCESS_LIMITS.maxStdoutBytes;
82
+ const maxStderrBytes = options.maxStderrBytes ?? EMULATED_SERVICES_PROCESS_LIMITS.maxStderrBytes;
83
+ let stdoutBytes = 0;
84
+ let stderrBytes = 0;
85
+ let overflowError = null;
86
+ const stdoutChunks = [];
87
+
88
+ child.stdout.on("data", (chunk) => {
89
+ stdoutBytes += byteLength(chunk);
90
+ if (stdoutBytes > maxStdoutBytes && !overflowError) {
91
+ overflowError = processError(EMULATED_SERVICES_ERROR_CODES.OUTPUT_LIMIT, "Emulated Services stdout exceeded its safety limit");
92
+ terminateChild(child);
93
+ return;
94
+ }
95
+ if (!overflowError) stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
96
+ });
97
+ child.stderr.on("data", (chunk) => {
98
+ stderrBytes += byteLength(chunk);
99
+ if (stderrBytes > maxStderrBytes && !overflowError) {
100
+ overflowError = processError(EMULATED_SERVICES_ERROR_CODES.OUTPUT_LIMIT, "Emulated Services stderr exceeded its safety limit");
101
+ terminateChild(child);
102
+ }
103
+ });
104
+
105
+ let processErrorValue = null;
106
+ child.on("error", (error) => {
107
+ processErrorValue = processError(
108
+ error?.code === "ENOENT" ? EMULATED_SERVICES_ERROR_CODES.UNAVAILABLE : EMULATED_SERVICES_ERROR_CODES.EXECUTION_FAILED,
109
+ "Emulated Services process failed",
110
+ error,
111
+ );
112
+ });
113
+
114
+ let hasClosed = false;
115
+ const closed = new Promise((resolve) => {
116
+ child.on("close", (code, signal) => {
117
+ hasClosed = true;
118
+ resolve({ code, signal });
119
+ });
120
+ });
121
+
122
+ return Object.freeze({
123
+ child,
124
+ closed,
125
+ output: () => ({
126
+ stdout: Buffer.concat(stdoutChunks).toString("utf8"),
127
+ stdoutBytes,
128
+ stderrBytes,
129
+ }),
130
+ failure: () => overflowError ?? processErrorValue,
131
+ stop: () => terminateChild(child),
132
+ waitForClose: () => waitForClose(child, () => hasClosed),
133
+ });
134
+ }
135
+
136
+ export async function runBoundedEmulatedServicesCommand(executablePath, args, options = {}) {
137
+ const managed = spawnManagedEmulatedServicesProcess(executablePath, args, options);
138
+ const timeoutMs = options.timeoutMs ?? 10_000;
139
+ const timeout = new Promise((_, reject) => {
140
+ const timer = setTimeout(() => {
141
+ managed.stop();
142
+ reject(processError(EMULATED_SERVICES_ERROR_CODES.TIMEOUT, "Emulated Services command exceeded its timeout"));
143
+ }, timeoutMs);
144
+ timer.unref?.();
145
+ });
146
+ try {
147
+ const result = await Promise.race([managed.closed, timeout]);
148
+ const failure = managed.failure();
149
+ if (failure) throw failure;
150
+ if (result.code !== 0) {
151
+ throw processError(EMULATED_SERVICES_ERROR_CODES.EXECUTION_FAILED, "Emulated Services command exited unsuccessfully");
152
+ }
153
+ return managed.output();
154
+ } finally {
155
+ managed.stop();
156
+ await managed.waitForClose();
157
+ }
158
+ }
159
+
160
+ export function emulatedServicesProcessError(code, message, cause) {
161
+ return processError(code, message, cause);
162
+ }
@@ -0,0 +1,282 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ EMULATED_SERVICES_ERROR_CODES,
7
+ EMULATED_SERVICES_ID,
8
+ EMULATED_SERVICES_PROCESS_LIMITS,
9
+ EMULATED_SERVICES_SUPPORTED_VERSION,
10
+ } from "./constants.js";
11
+ import {
12
+ emulatedServicesProcessError,
13
+ runBoundedEmulatedServicesCommand,
14
+ spawnManagedEmulatedServicesProcess,
15
+ } from "./process.js";
16
+
17
+ const FACTORY_KEYS = new Set([
18
+ "executablePath",
19
+ "expectedVersion",
20
+ "tempRoot",
21
+ "targetRoot",
22
+ "spawnImpl",
23
+ "fsImpl",
24
+ "mkdtempImpl",
25
+ "rmImpl",
26
+ "fetchImpl",
27
+ "sleepImpl",
28
+ "nowImpl",
29
+ ]);
30
+
31
+ function providerError(code, message, cause) {
32
+ return emulatedServicesProcessError(code, message, cause);
33
+ }
34
+
35
+ function assertPortableString(value, label) {
36
+ if (typeof value !== "string" || value.length === 0 || /\p{Cc}/u.test(value)) {
37
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `${label} must be a non-empty portable string`);
38
+ }
39
+ return value;
40
+ }
41
+
42
+ function assertAbsolutePath(value, label) {
43
+ assertPortableString(value, label);
44
+ if (!path.isAbsolute(value)) {
45
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `${label} must be absolute`);
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function assertRegularFile(filePath, label, fsImpl) {
51
+ assertAbsolutePath(filePath, label);
52
+ let stat;
53
+ try {
54
+ stat = fsImpl.lstatSync(filePath);
55
+ } catch (error) {
56
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `${label} is unavailable`, error);
57
+ }
58
+ if (!stat.isFile() || stat.isSymbolicLink?.()) {
59
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `${label} must be a regular non-symlink file`);
60
+ }
61
+ return filePath;
62
+ }
63
+
64
+ function isWithin(parent, candidate) {
65
+ const relative = path.relative(path.resolve(parent), path.resolve(candidate));
66
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
67
+ }
68
+
69
+ function assertTempRootSafe(tempRoot, targetRoot) {
70
+ if (targetRoot && (isWithin(targetRoot, tempRoot) || isWithin(tempRoot, targetRoot))) {
71
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services temporary state must be outside the target root");
72
+ }
73
+ }
74
+
75
+ function filteredEnvironment() {
76
+ const allowed = new Set(["PATH", "HOME", "TMPDIR", "TEMP", "TMP", "SystemRoot", "ComSpec", "LANG", "LC_ALL", "NODE_PATH"]);
77
+ return Object.fromEntries(Object.entries(process.env).filter(([key]) => allowed.has(key)));
78
+ }
79
+
80
+ function parseVersion(stdout) {
81
+ if (typeof stdout !== "string") {
82
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.OUTPUT_INVALID, "Emulated Services version output was not text");
83
+ }
84
+ const match = /^\s*(?:emulate(?:\s+|@))?v?(\d+\.\d+\.\d+)\s*$/iu.exec(stdout);
85
+ if (!match) {
86
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.OUTPUT_INVALID, "Emulated Services version output was malformed");
87
+ }
88
+ return match[1];
89
+ }
90
+
91
+ function normalizeServices(services) {
92
+ if (!Array.isArray(services) || services.length === 0 || services.length > EMULATED_SERVICES_PROCESS_LIMITS.maxServices) {
93
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "services must be a non-empty bounded array");
94
+ }
95
+ const normalized = services.map((service) => {
96
+ assertPortableString(service, "service");
97
+ if (!/^[a-z][a-z0-9-]*$/u.test(service)) {
98
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "service identifiers must be portable lowercase names");
99
+ }
100
+ return service;
101
+ });
102
+ if (new Set(normalized).size !== normalized.length) {
103
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "services must not contain duplicates");
104
+ }
105
+ return normalized;
106
+ }
107
+
108
+ function normalizeRequest(request, { fsImpl, targetRoot }) {
109
+ if (!request || typeof request !== "object" || Array.isArray(request)) {
110
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services request must be an object");
111
+ }
112
+ const services = normalizeServices(request.services);
113
+ const basePort = request.basePort ?? 4000;
114
+ if (!Number.isSafeInteger(basePort) || basePort < 1 || basePort + services.length - 1 > 65535) {
115
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "basePort must provide a valid loopback port range");
116
+ }
117
+ const timeoutMs = request.timeoutMs ?? 30_000;
118
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > EMULATED_SERVICES_PROCESS_LIMITS.maxTimeoutMs) {
119
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "timeoutMs is outside the supported bounded range");
120
+ }
121
+ let seedPath = null;
122
+ if (request.seedPath !== undefined && request.seedPath !== null) {
123
+ seedPath = assertRegularFile(request.seedPath, "seedPath", fsImpl);
124
+ if (targetRoot && isWithin(targetRoot, seedPath)) {
125
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "seedPath must not be inside the target root");
126
+ }
127
+ }
128
+ return { services, basePort, timeoutMs, seedPath, signal: request.signal };
129
+ }
130
+
131
+ function validateSignal(signal) {
132
+ if (signal !== undefined && (!signal || typeof signal.aborted !== "boolean" || typeof signal.addEventListener !== "function")) {
133
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "signal must be an AbortSignal");
134
+ }
135
+ }
136
+
137
+ function assertNotCancelled(signal) {
138
+ if (signal?.aborted) {
139
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CANCELLED, "Emulated Services operation was cancelled");
140
+ }
141
+ }
142
+
143
+ function normalizeEndpoint(port) {
144
+ const endpoint = `http://127.0.0.1:${port}`;
145
+ const url = new URL(endpoint);
146
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1") {
147
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.ENDPOINT_INVALID, "Emulated Services endpoint was not loopback HTTP");
148
+ }
149
+ return endpoint;
150
+ }
151
+
152
+ async function waitForReady(endpoint, { fetchImpl, signal, deadline, sleepImpl, nowImpl }) {
153
+ while (nowImpl() < deadline) {
154
+ assertNotCancelled(signal);
155
+ try {
156
+ const response = await fetchImpl(endpoint, { method: "GET", redirect: "manual", signal });
157
+ const status = Number(response?.status);
158
+ if (Number.isInteger(status) && status >= 200 && status < 500) return;
159
+ } catch (error) {
160
+ if (signal?.aborted) throw providerError(EMULATED_SERVICES_ERROR_CODES.CANCELLED, "Emulated Services operation was cancelled", error);
161
+ }
162
+ const remaining = deadline - nowImpl();
163
+ if (remaining <= 0) break;
164
+ await sleepImpl(Math.min(EMULATED_SERVICES_PROCESS_LIMITS.readinessPollMs, remaining), signal);
165
+ }
166
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.TIMEOUT, "Emulated Services did not become ready before the timeout");
167
+ }
168
+
169
+ function validateFactoryOptions(options) {
170
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
171
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services provider options must be an object");
172
+ }
173
+ for (const key of Object.keys(options)) {
174
+ if (!FACTORY_KEYS.has(key)) throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `Unsupported Emulated Services option: ${key}`);
175
+ }
176
+ }
177
+
178
+ export function createEmulatedServicesProvider(options = {}) {
179
+ validateFactoryOptions(options);
180
+ const fsImpl = options.fsImpl ?? fs;
181
+ const executablePath = assertRegularFile(options.executablePath, "executablePath", fsImpl);
182
+ const expectedVersion = options.expectedVersion ?? EMULATED_SERVICES_SUPPORTED_VERSION;
183
+ if (expectedVersion !== EMULATED_SERVICES_SUPPORTED_VERSION) {
184
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, `Only Emulated Services ${EMULATED_SERVICES_SUPPORTED_VERSION} is supported`);
185
+ }
186
+ const tempRoot = assertAbsolutePath(options.tempRoot ?? os.tmpdir(), "tempRoot");
187
+ const targetRoot = options.targetRoot === undefined ? null : assertAbsolutePath(options.targetRoot, "targetRoot");
188
+ assertTempRootSafe(tempRoot, targetRoot);
189
+ const mkdtempImpl = options.mkdtempImpl ?? fs.promises.mkdtemp;
190
+ const rmImpl = options.rmImpl ?? fs.promises.rm;
191
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
192
+ const sleepImpl = options.sleepImpl ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
193
+ const nowImpl = options.nowImpl ?? (() => Date.now());
194
+ if (typeof fetchImpl !== "function" || typeof mkdtempImpl !== "function" || typeof rmImpl !== "function") {
195
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.CONFIG_INVALID, "Emulated Services host capabilities are incomplete");
196
+ }
197
+
198
+ return Object.freeze({
199
+ id: EMULATED_SERVICES_ID,
200
+ version: expectedVersion,
201
+ async start(request = {}) {
202
+ const normalized = normalizeRequest(request, { fsImpl, targetRoot });
203
+ validateSignal(normalized.signal);
204
+ assertNotCancelled(normalized.signal);
205
+ const tempPrefix = path.join(tempRoot, "forgeloop-emulated-");
206
+ let stateRoot = null;
207
+ let managed = null;
208
+ let result = null;
209
+ let operationError = null;
210
+ try {
211
+ stateRoot = await mkdtempImpl(tempPrefix);
212
+ assertAbsolutePath(stateRoot, "Emulated Services temporary state");
213
+ assertTempRootSafe(stateRoot, targetRoot);
214
+ const versionResult = await runBoundedEmulatedServicesCommand(executablePath, ["--version"], {
215
+ cwd: stateRoot,
216
+ env: filteredEnvironment(),
217
+ spawnImpl: options.spawnImpl,
218
+ timeoutMs: Math.min(normalized.timeoutMs, 10_000),
219
+ });
220
+ const actualVersion = parseVersion(versionResult.stdout);
221
+ if (actualVersion !== expectedVersion) {
222
+ throw providerError(EMULATED_SERVICES_ERROR_CODES.VERSION_UNSUPPORTED, `Unsupported Emulated Services version: ${actualVersion}`);
223
+ }
224
+
225
+ const args = ["--service", normalized.services.join(","), "--port", String(normalized.basePort)];
226
+ if (normalized.seedPath) args.push("--seed", normalized.seedPath);
227
+ managed = spawnManagedEmulatedServicesProcess(executablePath, args, {
228
+ cwd: stateRoot,
229
+ env: filteredEnvironment(),
230
+ spawnImpl: options.spawnImpl,
231
+ });
232
+ const deadline = nowImpl() + normalized.timeoutMs;
233
+ const endpoints = normalized.services.map((service, index) => ({
234
+ id: service,
235
+ endpoint: normalizeEndpoint(normalized.basePort + index),
236
+ }));
237
+ for (const service of endpoints) {
238
+ await waitForReady(service.endpoint, {
239
+ fetchImpl,
240
+ signal: normalized.signal,
241
+ deadline,
242
+ sleepImpl,
243
+ nowImpl,
244
+ });
245
+ }
246
+ result = {
247
+ provider: { id: EMULATED_SERVICES_ID, version: expectedVersion },
248
+ services: endpoints,
249
+ status: "READY",
250
+ diagnostics: ["Host-provided emulated services were observed on loopback"],
251
+ authority: "OBSERVATION",
252
+ evidenceAuthority: "NONE",
253
+ lifecycleAuthority: false,
254
+ completionAuthority: false,
255
+ persisted: false,
256
+ cleanup: { status: "PENDING" },
257
+ };
258
+ } catch (error) {
259
+ operationError = error;
260
+ } finally {
261
+ if (managed) {
262
+ managed.stop();
263
+ await managed.waitForClose();
264
+ }
265
+ if (stateRoot) {
266
+ try {
267
+ await rmImpl(stateRoot, { recursive: true, force: true });
268
+ if (result) result.cleanup = { status: "COMPLETED" };
269
+ } catch (error) {
270
+ const cleanupError = providerError(EMULATED_SERVICES_ERROR_CODES.CLEANUP_FAILED, "Emulated Services temporary state cleanup failed", error);
271
+ if (!operationError) operationError = cleanupError;
272
+ if (result) result.cleanup = { status: "FAILED" };
273
+ }
274
+ }
275
+ }
276
+ if (operationError) throw operationError;
277
+ return Object.freeze(result);
278
+ },
279
+ });
280
+ }
281
+
282
+ export { normalizeEndpoint, parseVersion };
@@ -0,0 +1,90 @@
1
+ import {
2
+ ADVISORY_CONTEXT_LIMITS,
3
+ normalizeAdvisoryRecallOptions,
4
+ } from "../../core/advisory-context/constants.js";
5
+ import {
6
+ E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
7
+ E_ADVISORY_CONTEXT_RESULT_INVALID,
8
+ } from "../../core/error-codes.js";
9
+
10
+ function adapterError(code, message) {
11
+ const error = new Error(message);
12
+ error.name = "OpenSrcAdapterError";
13
+ error.code = code;
14
+ return error;
15
+ }
16
+
17
+ function fitText(value, maxLength) {
18
+ if (value.length <= maxLength) return value;
19
+ return `${value.slice(0, Math.max(1, maxLength - 1))}…`;
20
+ }
21
+
22
+ function lineRangeLabel(startLine, endLine) {
23
+ return startLine === endLine ? `L${startLine}` : `L${startLine}-L${endLine}`;
24
+ }
25
+
26
+ function normalizeMatch(match) {
27
+ if (!match || typeof match !== "object" || Array.isArray(match)) return null;
28
+ const { sourceSpec, relativePath, snippet, snippetStartLine, snippetEndLine } = match;
29
+ if (typeof sourceSpec !== "string" || sourceSpec === "") return null;
30
+ if (typeof relativePath !== "string" || relativePath === "" || relativePath.startsWith("/")) return null;
31
+ if (typeof snippet !== "string") return null;
32
+ if (!Number.isSafeInteger(snippetStartLine) || snippetStartLine < 1) return null;
33
+ if (!Number.isSafeInteger(snippetEndLine) || snippetEndLine < snippetStartLine) return null;
34
+ const range = lineRangeLabel(snippetStartLine, snippetEndLine);
35
+ const flatSnippet = snippet.replace(/\s+/gu, " ").trim();
36
+ return {
37
+ title: `${sourceSpec} — ${relativePath}`,
38
+ summary: flatSnippet === "" ? range : `${range} | ${flatSnippet}`,
39
+ sourceRef: `opensrc:${sourceSpec}:${relativePath}#${range}`,
40
+ identity: [sourceSpec, relativePath, range, snippet].join(" "),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Convert bounded deterministic search matches into ForgeLoop advisory items.
46
+ *
47
+ * Titles, summaries, and source references carry only portable relative
48
+ * paths; absolute cache locations never leave this boundary.
49
+ */
50
+ export function normalizeOpenSrcResult(matches, {
51
+ limit,
52
+ maxItemChars,
53
+ maxTotalChars,
54
+ timeoutMs,
55
+ } = {}) {
56
+ if (!Array.isArray(matches)) {
57
+ throw adapterError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc search matches must be an array");
58
+ }
59
+ if (matches.length > ADVISORY_CONTEXT_LIMITS.maxProviderReturnedItems) {
60
+ throw adapterError(
61
+ E_ADVISORY_CONTEXT_OUTPUT_LIMIT,
62
+ `OpenSrc returned ${matches.length} matches, exceeding the raw item ceiling of ${ADVISORY_CONTEXT_LIMITS.maxProviderReturnedItems}`,
63
+ );
64
+ }
65
+ const options = normalizeAdvisoryRecallOptions({ limit, maxItemChars, maxTotalChars, timeoutMs });
66
+ const prepared = [];
67
+ const seen = new Set();
68
+ for (const match of matches) {
69
+ const normalized = normalizeMatch(match);
70
+ if (!normalized) continue;
71
+ if (seen.has(normalized.identity)) continue;
72
+ seen.add(normalized.identity);
73
+ const summary = fitText(normalized.summary, options.maxItemChars);
74
+ prepared.push({ title: normalized.title, summary, sourceRef: normalized.sourceRef });
75
+ }
76
+
77
+ const items = [];
78
+ let totalChars = 0;
79
+ for (const candidate of prepared) {
80
+ if (items.length >= options.limit) break;
81
+ const chars = candidate.title.length + candidate.summary.length + candidate.sourceRef.length;
82
+ if (totalChars + chars > options.maxTotalChars) break;
83
+ totalChars += chars;
84
+ items.push(candidate);
85
+ }
86
+ if (items.length === 0 && prepared.length > 0) {
87
+ throw adapterError(E_ADVISORY_CONTEXT_OUTPUT_LIMIT, "OpenSrc matches cannot fit the requested output budget");
88
+ }
89
+ return { items };
90
+ }