@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,338 @@
|
|
|
1
|
+
import { realpath, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { assertSafePath, isPathWithin } from "../../core/filesystem.js";
|
|
5
|
+
import {
|
|
6
|
+
normalizeAdvisoryRecallOptions,
|
|
7
|
+
} from "../../core/advisory-context/constants.js";
|
|
8
|
+
import {
|
|
9
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
10
|
+
E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE,
|
|
11
|
+
E_ADVISORY_CONTEXT_RESULT_INVALID,
|
|
12
|
+
E_ADVISORY_CONTEXT_TIMEOUT,
|
|
13
|
+
} from "../../core/error-codes.js";
|
|
14
|
+
import {
|
|
15
|
+
OPENSRC_PROCESS_LIMITS,
|
|
16
|
+
parseOpenSrcVersion,
|
|
17
|
+
runOpenSrcCommand,
|
|
18
|
+
} from "./process.js";
|
|
19
|
+
import { OPENSRC_SEARCH_LIMITS, searchSourceRoot } from "./search.js";
|
|
20
|
+
import { normalizeOpenSrcResult } from "./normalize.js";
|
|
21
|
+
|
|
22
|
+
const MAX_SOURCES = OPENSRC_SEARCH_LIMITS.maxSources;
|
|
23
|
+
const MAX_SOURCE_SPEC_CHARS = 256;
|
|
24
|
+
|
|
25
|
+
function providerError(code, message) {
|
|
26
|
+
const error = new Error(message);
|
|
27
|
+
error.name = "OpenSrcAdvisoryProviderError";
|
|
28
|
+
error.code = code;
|
|
29
|
+
return error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertAbsolutePath(value, label) {
|
|
33
|
+
if (typeof value !== "string" || value.trim() === "" || !path.isAbsolute(value) || /\p{Cc}/u.test(value)) {
|
|
34
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, `${label} must be an absolute portable path`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertExpectedVersion(value) {
|
|
40
|
+
if (
|
|
41
|
+
typeof value !== "string"
|
|
42
|
+
|| value.trim() === ""
|
|
43
|
+
|| value.length > 64
|
|
44
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/u.test(value)
|
|
45
|
+
) {
|
|
46
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc expectedVersion must be a qualified version token under 64 characters");
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertSourceSpec(value) {
|
|
52
|
+
if (typeof value !== "string" || value.trim() === "" || value.length > MAX_SOURCE_SPEC_CHARS) {
|
|
53
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc source specs must be non-empty strings under 256 characters");
|
|
54
|
+
}
|
|
55
|
+
if (/[\0\p{Cc}\s]/u.test(value)) {
|
|
56
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc source specs must not contain whitespace or control characters");
|
|
57
|
+
}
|
|
58
|
+
if (value.startsWith("-")) {
|
|
59
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc source specs must not begin with '-'");
|
|
60
|
+
}
|
|
61
|
+
if (/[;|&$`<>(){}[\]!*?~#"']/u.test(value)) {
|
|
62
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc source specs must not contain shell metacharacters");
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertSources(value) {
|
|
68
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
69
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc sources must be a non-empty array");
|
|
70
|
+
}
|
|
71
|
+
if (value.length > MAX_SOURCES) {
|
|
72
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, `OpenSrc sources must contain at most ${MAX_SOURCES} entries`);
|
|
73
|
+
}
|
|
74
|
+
return value.map(assertSourceSpec);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function remainingTimeout(deadline) {
|
|
78
|
+
const remaining = deadline - Date.now();
|
|
79
|
+
if (remaining < 1) {
|
|
80
|
+
throw providerError(E_ADVISORY_CONTEXT_TIMEOUT, "OpenSrc advisory recall deadline expired");
|
|
81
|
+
}
|
|
82
|
+
return remaining;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function reportTransport(callback, payload) {
|
|
86
|
+
if (typeof callback !== "function") return;
|
|
87
|
+
try {
|
|
88
|
+
callback(Object.freeze({ ...payload }));
|
|
89
|
+
} catch {
|
|
90
|
+
// Observability hooks cannot change advisory correctness or failure mapping.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function canonicalizeExisting(candidate) {
|
|
95
|
+
try {
|
|
96
|
+
return await realpath(candidate);
|
|
97
|
+
} catch {
|
|
98
|
+
return path.resolve(candidate);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function assertCacheOutsideProject(cacheRoot, projectRoot) {
|
|
103
|
+
// Compare both the lexical and the canonicalized spellings: either side may
|
|
104
|
+
// traverse symlinked prefixes (for example /var versus /private/var), so a
|
|
105
|
+
// single spelling can miss a genuine containment relationship. The roots
|
|
106
|
+
// must be disjoint in both directions: neither may contain the other.
|
|
107
|
+
const spellings = [
|
|
108
|
+
[path.resolve(cacheRoot), path.resolve(projectRoot)],
|
|
109
|
+
[await canonicalizeExisting(cacheRoot), await canonicalizeExisting(projectRoot)],
|
|
110
|
+
];
|
|
111
|
+
for (const [candidateCache, candidateProject] of spellings) {
|
|
112
|
+
if (isPathWithin(candidateProject, candidateCache) || isPathWithin(candidateCache, candidateProject)) {
|
|
113
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc cacheRoot and project root must be disjoint");
|
|
114
|
+
}
|
|
115
|
+
if (isPathWithin(path.join(candidateProject, ".forgeloop"), candidateCache)) {
|
|
116
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc cacheRoot must be outside .forgeloop");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return path.resolve(cacheRoot);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function runAndReport(executablePath, args, {
|
|
123
|
+
cwd,
|
|
124
|
+
timeoutMs,
|
|
125
|
+
spawnImpl,
|
|
126
|
+
env,
|
|
127
|
+
maxStdoutBytes,
|
|
128
|
+
maxStderrBytes,
|
|
129
|
+
onTransport,
|
|
130
|
+
kind,
|
|
131
|
+
sourceIndex,
|
|
132
|
+
} = {}) {
|
|
133
|
+
const result = await runOpenSrcCommand(executablePath, args, {
|
|
134
|
+
cwd,
|
|
135
|
+
timeoutMs,
|
|
136
|
+
spawnImpl,
|
|
137
|
+
env,
|
|
138
|
+
maxStdoutBytes,
|
|
139
|
+
maxStderrBytes,
|
|
140
|
+
});
|
|
141
|
+
reportTransport(onTransport, {
|
|
142
|
+
kind,
|
|
143
|
+
...(sourceIndex === undefined ? {} : { sourceIndex }),
|
|
144
|
+
stdoutBytes: result.stdoutBytes,
|
|
145
|
+
stderrBytes: result.stderrBytes,
|
|
146
|
+
durationMs: result.durationMs,
|
|
147
|
+
});
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function resolveSourceRoot({
|
|
152
|
+
executablePath,
|
|
153
|
+
source,
|
|
154
|
+
sourceIndex,
|
|
155
|
+
projectRoot,
|
|
156
|
+
cacheRoot,
|
|
157
|
+
timeoutMs,
|
|
158
|
+
spawnImpl,
|
|
159
|
+
env,
|
|
160
|
+
onTransport,
|
|
161
|
+
}) {
|
|
162
|
+
let result;
|
|
163
|
+
try {
|
|
164
|
+
result = await runAndReport(executablePath, ["path", source, "--cwd", projectRoot], {
|
|
165
|
+
cwd: projectRoot,
|
|
166
|
+
timeoutMs,
|
|
167
|
+
spawnImpl,
|
|
168
|
+
env,
|
|
169
|
+
maxStdoutBytes: OPENSRC_PROCESS_LIMITS.maxStdoutBytes,
|
|
170
|
+
maxStderrBytes: OPENSRC_PROCESS_LIMITS.maxStderrBytes,
|
|
171
|
+
onTransport,
|
|
172
|
+
kind: "path",
|
|
173
|
+
sourceIndex,
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw error.code ? error : providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "OpenSrc source resolution failed");
|
|
177
|
+
}
|
|
178
|
+
const lines = result.stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
179
|
+
if (lines.length !== 1) {
|
|
180
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc path must print exactly one path");
|
|
181
|
+
}
|
|
182
|
+
const [printed] = lines;
|
|
183
|
+
if (/\p{Cc}/u.test(printed)) {
|
|
184
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc path output must be portable text");
|
|
185
|
+
}
|
|
186
|
+
if (!path.isAbsolute(printed)) {
|
|
187
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc path output must be absolute");
|
|
188
|
+
}
|
|
189
|
+
let resolved;
|
|
190
|
+
try {
|
|
191
|
+
resolved = await realpath(printed);
|
|
192
|
+
} catch {
|
|
193
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc source path could not be canonicalized");
|
|
194
|
+
}
|
|
195
|
+
let canonicalCache;
|
|
196
|
+
try {
|
|
197
|
+
canonicalCache = await realpath(cacheRoot);
|
|
198
|
+
} catch {
|
|
199
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc cache root could not be canonicalized");
|
|
200
|
+
}
|
|
201
|
+
if (resolved !== canonicalCache && !isPathWithin(canonicalCache, resolved)) {
|
|
202
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc source path escaped the configured cache");
|
|
203
|
+
}
|
|
204
|
+
let rootStat;
|
|
205
|
+
try {
|
|
206
|
+
rootStat = await stat(resolved);
|
|
207
|
+
} catch {
|
|
208
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc source path is not an accessible directory");
|
|
209
|
+
}
|
|
210
|
+
if (!rootStat.isDirectory()) {
|
|
211
|
+
throw providerError(E_ADVISORY_CONTEXT_RESULT_INVALID, "OpenSrc source path is not a directory");
|
|
212
|
+
}
|
|
213
|
+
return resolved;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Create an optional host-injected OpenSrc adapter.
|
|
218
|
+
*
|
|
219
|
+
* Construction is inert: it validates the explicit executable, version,
|
|
220
|
+
* cache, and source allowlist but does not discover binaries, spawn
|
|
221
|
+
* processes, access the network, or write lifecycle state. Version
|
|
222
|
+
* qualification repeats lazily before every recall so a changed executable
|
|
223
|
+
* cannot silently change the provider identity.
|
|
224
|
+
*/
|
|
225
|
+
export function createOpenSrcAdvisoryContextProvider({
|
|
226
|
+
executablePath,
|
|
227
|
+
expectedVersion,
|
|
228
|
+
cacheRoot,
|
|
229
|
+
sources,
|
|
230
|
+
spawnImpl,
|
|
231
|
+
env,
|
|
232
|
+
onTransport,
|
|
233
|
+
} = {}) {
|
|
234
|
+
const qualifiedExecutablePath = assertAbsolutePath(executablePath, "OpenSrc executablePath");
|
|
235
|
+
const qualifiedExpectedVersion = assertExpectedVersion(expectedVersion);
|
|
236
|
+
const qualifiedCacheRoot = assertAbsolutePath(cacheRoot, "OpenSrc cacheRoot");
|
|
237
|
+
const qualifiedSources = assertSources(sources);
|
|
238
|
+
if (spawnImpl !== undefined && typeof spawnImpl !== "function") {
|
|
239
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc spawn implementation must be a function");
|
|
240
|
+
}
|
|
241
|
+
if (env !== undefined && (!env || typeof env !== "object" || Array.isArray(env))) {
|
|
242
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc process environment must be an object");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const provider = {
|
|
246
|
+
id: "opensrc",
|
|
247
|
+
version: qualifiedExpectedVersion,
|
|
248
|
+
async recall({
|
|
249
|
+
projectPath,
|
|
250
|
+
taskId,
|
|
251
|
+
query,
|
|
252
|
+
limit,
|
|
253
|
+
maxItemChars,
|
|
254
|
+
maxTotalChars,
|
|
255
|
+
timeoutMs,
|
|
256
|
+
} = {}) {
|
|
257
|
+
if (typeof projectPath !== "string" || !path.isAbsolute(projectPath) || /\p{Cc}/u.test(projectPath)) {
|
|
258
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc projectPath must be an absolute path");
|
|
259
|
+
}
|
|
260
|
+
if (typeof taskId !== "string" || taskId.trim() === "") {
|
|
261
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc taskId must be a non-empty string");
|
|
262
|
+
}
|
|
263
|
+
if (typeof query !== "string" || query.trim() === "" || /\p{Cc}/u.test(query)) {
|
|
264
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc query must be a non-empty portable string");
|
|
265
|
+
}
|
|
266
|
+
const options = normalizeAdvisoryRecallOptions({ limit, maxItemChars, maxTotalChars, timeoutMs });
|
|
267
|
+
const projectRoot = path.resolve(projectPath);
|
|
268
|
+
try {
|
|
269
|
+
await assertSafePath(projectRoot, ".");
|
|
270
|
+
} catch {
|
|
271
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "OpenSrc target directory is unavailable or unsafe");
|
|
272
|
+
}
|
|
273
|
+
await assertCacheOutsideProject(qualifiedCacheRoot, projectRoot);
|
|
274
|
+
|
|
275
|
+
const childEnv = { ...(env ?? {}), OPENSRC_HOME: qualifiedCacheRoot };
|
|
276
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
277
|
+
let versionResult;
|
|
278
|
+
try {
|
|
279
|
+
versionResult = await runAndReport(qualifiedExecutablePath, ["--version"], {
|
|
280
|
+
cwd: projectRoot,
|
|
281
|
+
timeoutMs: remainingTimeout(deadline),
|
|
282
|
+
spawnImpl,
|
|
283
|
+
env: childEnv,
|
|
284
|
+
maxStdoutBytes: OPENSRC_PROCESS_LIMITS.maxStdoutBytes,
|
|
285
|
+
maxStderrBytes: OPENSRC_PROCESS_LIMITS.maxStderrBytes,
|
|
286
|
+
onTransport,
|
|
287
|
+
kind: "version",
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
throw error.code ? error : providerError(E_ADVISORY_CONTEXT_PROVIDER_UNAVAILABLE, "OpenSrc version probe failed");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let actualVersion;
|
|
294
|
+
try {
|
|
295
|
+
actualVersion = parseOpenSrcVersion(versionResult.stdout);
|
|
296
|
+
} catch {
|
|
297
|
+
throw providerError(E_ADVISORY_CONTEXT_PROVIDER_INVALID, "OpenSrc version probe was not qualified");
|
|
298
|
+
}
|
|
299
|
+
if (actualVersion !== qualifiedExpectedVersion) {
|
|
300
|
+
throw providerError(
|
|
301
|
+
E_ADVISORY_CONTEXT_PROVIDER_INVALID,
|
|
302
|
+
"OpenSrc executable version does not match the expected qualified version",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const matches = [];
|
|
307
|
+
const sharedBudget = { remainingReadBytes: OPENSRC_SEARCH_LIMITS.maxTotalReadBytes };
|
|
308
|
+
const boundedSources = qualifiedSources.slice(0, MAX_SOURCES);
|
|
309
|
+
for (let sourceIndex = 0; sourceIndex < boundedSources.length; sourceIndex += 1) {
|
|
310
|
+
const source = boundedSources[sourceIndex];
|
|
311
|
+
const sourceRoot = await resolveSourceRoot({
|
|
312
|
+
executablePath: qualifiedExecutablePath,
|
|
313
|
+
source,
|
|
314
|
+
sourceIndex,
|
|
315
|
+
projectRoot,
|
|
316
|
+
cacheRoot: qualifiedCacheRoot,
|
|
317
|
+
timeoutMs: remainingTimeout(deadline),
|
|
318
|
+
spawnImpl,
|
|
319
|
+
env: childEnv,
|
|
320
|
+
onTransport,
|
|
321
|
+
});
|
|
322
|
+
const sourceMatches = await searchSourceRoot({
|
|
323
|
+
sourceRoot,
|
|
324
|
+
sourceSpec: source,
|
|
325
|
+
sourceIndex,
|
|
326
|
+
query,
|
|
327
|
+
budget: sharedBudget,
|
|
328
|
+
deadline,
|
|
329
|
+
});
|
|
330
|
+
matches.push(...sourceMatches);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return normalizeOpenSrcResult(matches, options);
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
return Object.freeze(provider);
|
|
338
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
E_ADVISORY_CONTEXT_TIMEOUT,
|
|
6
|
+
} from "../../core/error-codes.js";
|
|
7
|
+
|
|
8
|
+
export const OPENSRC_SEARCH_LIMITS = Object.freeze({
|
|
9
|
+
maxSources: 8,
|
|
10
|
+
maxFilesPerSource: 2000,
|
|
11
|
+
maxEntriesPerSource: 5000,
|
|
12
|
+
maxFileBytes: 256 * 1024,
|
|
13
|
+
maxTotalReadBytes: 8 * 1024 * 1024,
|
|
14
|
+
maxMatchesPerSource: 32,
|
|
15
|
+
maxSnippetLines: 7,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function searchTimeoutError() {
|
|
19
|
+
const error = new Error("OpenSrc search exceeded the recall deadline");
|
|
20
|
+
error.code = E_ADVISORY_CONTEXT_TIMEOUT;
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertDeadline(deadline, clockImpl) {
|
|
25
|
+
if (deadline === null || deadline === undefined) return;
|
|
26
|
+
if (clockImpl() >= deadline) throw searchTimeoutError();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SKIPPED_DIRECTORIES = new Set([
|
|
30
|
+
".git",
|
|
31
|
+
"node_modules",
|
|
32
|
+
"target",
|
|
33
|
+
"dist",
|
|
34
|
+
"build",
|
|
35
|
+
"coverage",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const BINARY_EXTENSIONS = new Set([
|
|
39
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".bmp", ".avif",
|
|
40
|
+
".woff", ".woff2", ".ttf", ".otf", ".eot",
|
|
41
|
+
".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar",
|
|
42
|
+
".pdf", ".exe", ".dll", ".so", ".dylib", ".a", ".o",
|
|
43
|
+
".mp3", ".mp4", ".wav", ".ogg", ".webm", ".mov",
|
|
44
|
+
".pyc", ".pyo", ".class", ".wasm",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
function defaultHooks() {
|
|
48
|
+
return { lstat, readdir, readFile, realpath };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function tokenizeQuery(query) {
|
|
52
|
+
return query
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.split(/[^a-z0-9_]+/u)
|
|
55
|
+
.filter((token) => token.length > 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isProbablyBinary(buffer) {
|
|
59
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, 8000));
|
|
60
|
+
for (let index = 0; index < sample.length; index += 1) {
|
|
61
|
+
if (sample[index] === 0) return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function scoreLine(lowerLine, lowerQuery, queryTokens) {
|
|
67
|
+
if (lowerLine.includes(lowerQuery)) return { fullMatch: true, tokenOverlap: queryTokens.length };
|
|
68
|
+
let tokenOverlap = 0;
|
|
69
|
+
for (const token of queryTokens) {
|
|
70
|
+
if (token.length > 1 && lowerLine.includes(token)) tokenOverlap += 1;
|
|
71
|
+
}
|
|
72
|
+
return { fullMatch: false, tokenOverlap };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildSnippet(lines, matchIndex, maxSnippetLines) {
|
|
76
|
+
const context = Math.max(0, Math.floor((maxSnippetLines - 1) / 2));
|
|
77
|
+
const start = Math.max(0, matchIndex - context);
|
|
78
|
+
const end = Math.min(lines.length, matchIndex + context + 1);
|
|
79
|
+
return {
|
|
80
|
+
startLine: start + 1,
|
|
81
|
+
endLine: end,
|
|
82
|
+
text: lines.slice(start, end).join("\n"),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function resolveEntryTarget(resolvedRoot, absolutePath, stat, { lstatImpl, realpathImpl }) {
|
|
87
|
+
if (stat.isSymbolicLink()) {
|
|
88
|
+
let resolved;
|
|
89
|
+
try {
|
|
90
|
+
resolved = await realpathImpl(absolutePath);
|
|
91
|
+
} catch {
|
|
92
|
+
return { kind: "skip", stat };
|
|
93
|
+
}
|
|
94
|
+
const rootWithSep = resolvedRoot.endsWith(path.sep) ? resolvedRoot : `${resolvedRoot}${path.sep}`;
|
|
95
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(rootWithSep)) return { kind: "skip", stat };
|
|
96
|
+
try {
|
|
97
|
+
stat = await lstatImpl(resolved);
|
|
98
|
+
} catch {
|
|
99
|
+
return { kind: "skip", stat };
|
|
100
|
+
}
|
|
101
|
+
return { kind: stat.isFile() ? "file" : "skip", stat };
|
|
102
|
+
}
|
|
103
|
+
if (stat.isDirectory()) return { kind: "directory", stat };
|
|
104
|
+
if (stat.isFile()) return { kind: "file", stat };
|
|
105
|
+
return { kind: "skip", stat };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function processDirectoryEntry(resolvedRoot, relativePath, name, context) {
|
|
109
|
+
const { sourceSpec, sourceIndex, lowerQuery, queryTokens, limits, matches, state, budget,
|
|
110
|
+
lstatImpl, realpathImpl, readFileImpl, deadline, clockImpl } = context;
|
|
111
|
+
assertDeadline(deadline, clockImpl);
|
|
112
|
+
const absolutePath = path.join(resolvedRoot, relativePath);
|
|
113
|
+
let stat;
|
|
114
|
+
try {
|
|
115
|
+
stat = await lstatImpl(absolutePath);
|
|
116
|
+
} catch {
|
|
117
|
+
return "skip";
|
|
118
|
+
}
|
|
119
|
+
const entry = await resolveEntryTarget(resolvedRoot, absolutePath, stat, { lstatImpl, realpathImpl });
|
|
120
|
+
if (entry.kind === "directory") return "directory";
|
|
121
|
+
if (entry.kind !== "file") return "skip";
|
|
122
|
+
if (BINARY_EXTENSIONS.has(path.extname(name).toLowerCase())) return "skip";
|
|
123
|
+
if (entry.stat.size > limits.maxFileBytes) return "skip";
|
|
124
|
+
if (state.filesSeen >= limits.maxFilesPerSource) return "skip";
|
|
125
|
+
if (entry.stat.size > budget.remainingReadBytes) return "skip";
|
|
126
|
+
assertDeadline(deadline, clockImpl);
|
|
127
|
+
let buffer;
|
|
128
|
+
try {
|
|
129
|
+
buffer = await readFileImpl(absolutePath);
|
|
130
|
+
} catch {
|
|
131
|
+
return "skip";
|
|
132
|
+
}
|
|
133
|
+
assertDeadline(deadline, clockImpl);
|
|
134
|
+
state.filesSeen += 1;
|
|
135
|
+
budget.remainingReadBytes -= buffer.byteLength;
|
|
136
|
+
if (isProbablyBinary(buffer)) return "skip";
|
|
137
|
+
const text = buffer.toString("utf8");
|
|
138
|
+
if (/\u0000/u.test(text)) return "skip";
|
|
139
|
+
collectLineMatches(text.split("\n"), {
|
|
140
|
+
sourceSpec,
|
|
141
|
+
sourceIndex,
|
|
142
|
+
relativePath: relativePath.split(path.sep).join("/"),
|
|
143
|
+
lowerQuery,
|
|
144
|
+
queryTokens,
|
|
145
|
+
limits,
|
|
146
|
+
matches,
|
|
147
|
+
});
|
|
148
|
+
return "file";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function collectLineMatches(lines, { sourceSpec, sourceIndex, relativePath, lowerQuery, queryTokens, limits, matches }) {
|
|
152
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
153
|
+
if (matches.length >= limits.maxMatchesPerSource) break;
|
|
154
|
+
const line = lines[lineIndex];
|
|
155
|
+
const { fullMatch, tokenOverlap } = scoreLine(line.toLowerCase(), lowerQuery, queryTokens);
|
|
156
|
+
if (!fullMatch && tokenOverlap === 0) continue;
|
|
157
|
+
const snippet = buildSnippet(lines, lineIndex, limits.maxSnippetLines);
|
|
158
|
+
matches.push({
|
|
159
|
+
sourceSpec,
|
|
160
|
+
sourceIndex,
|
|
161
|
+
relativePath,
|
|
162
|
+
line: lineIndex + 1,
|
|
163
|
+
fullMatch,
|
|
164
|
+
tokenOverlap,
|
|
165
|
+
snippetStartLine: snippet.startLine,
|
|
166
|
+
snippetEndLine: snippet.endLine,
|
|
167
|
+
snippet: snippet.text,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function compareMatches(left, right) {
|
|
173
|
+
if (left.fullMatch !== right.fullMatch) return left.fullMatch ? -1 : 1;
|
|
174
|
+
if (left.tokenOverlap !== right.tokenOverlap) return right.tokenOverlap - left.tokenOverlap;
|
|
175
|
+
if (left.sourceIndex !== right.sourceIndex) return left.sourceIndex - right.sourceIndex;
|
|
176
|
+
if (left.relativePath !== right.relativePath) return left.relativePath < right.relativePath ? -1 : 1;
|
|
177
|
+
return left.line - right.line;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Deterministically search one resolved OpenSrc source root for query text.
|
|
182
|
+
*
|
|
183
|
+
* Traversal is lexicographically sorted, directory symlinks are never
|
|
184
|
+
* followed, file symlinks must resolve inside the source root, and every
|
|
185
|
+
* bound (entry count, file count, file size, shared byte budget, match count,
|
|
186
|
+
* recall deadline) is enforced before further allocation or reads. Generated
|
|
187
|
+
* directory names are skipped at any depth; ordinary files are never skipped
|
|
188
|
+
* by name. Stopping points are deterministic for identical trees, queries,
|
|
189
|
+
* and budgets.
|
|
190
|
+
*/
|
|
191
|
+
export async function searchSourceRoot({
|
|
192
|
+
sourceRoot,
|
|
193
|
+
sourceSpec,
|
|
194
|
+
sourceIndex = 0,
|
|
195
|
+
query,
|
|
196
|
+
limits = OPENSRC_SEARCH_LIMITS,
|
|
197
|
+
hooks = {},
|
|
198
|
+
budget = null,
|
|
199
|
+
deadline = null,
|
|
200
|
+
clockImpl = Date.now,
|
|
201
|
+
} = {}) {
|
|
202
|
+
if (typeof sourceRoot !== "string" || sourceRoot === "" || typeof sourceSpec !== "string" || sourceSpec === "") {
|
|
203
|
+
throw new Error("OpenSrc search requires a source root and source spec");
|
|
204
|
+
}
|
|
205
|
+
if (typeof query !== "string" || query.trim() === "") {
|
|
206
|
+
throw new Error("OpenSrc search requires a non-empty query");
|
|
207
|
+
}
|
|
208
|
+
if (deadline !== null && deadline !== undefined && typeof clockImpl !== "function") {
|
|
209
|
+
throw new Error("OpenSrc search requires a clock implementation when a deadline is set");
|
|
210
|
+
}
|
|
211
|
+
const { lstat: lstatImpl, readdir: readdirImpl, readFile: readFileImpl, realpath: realpathImpl } = {
|
|
212
|
+
...defaultHooks(),
|
|
213
|
+
...hooks,
|
|
214
|
+
};
|
|
215
|
+
let resolvedRoot = path.resolve(sourceRoot);
|
|
216
|
+
try {
|
|
217
|
+
resolvedRoot = await realpathImpl(resolvedRoot);
|
|
218
|
+
} catch {
|
|
219
|
+
// The root is stat'ed lazily below; keep the resolved form on failure.
|
|
220
|
+
}
|
|
221
|
+
const lowerQuery = query.toLowerCase();
|
|
222
|
+
const queryTokens = tokenizeQuery(query);
|
|
223
|
+
const matches = [];
|
|
224
|
+
const state = { filesSeen: 0, entriesVisited: 0 };
|
|
225
|
+
const sharedBudget = budget ?? { remainingReadBytes: limits.maxTotalReadBytes ?? OPENSRC_SEARCH_LIMITS.maxTotalReadBytes };
|
|
226
|
+
const maxEntries = limits.maxEntriesPerSource ?? OPENSRC_SEARCH_LIMITS.maxEntriesPerSource;
|
|
227
|
+
|
|
228
|
+
const stack = ["."];
|
|
229
|
+
const context = {
|
|
230
|
+
resolvedRoot, sourceSpec, sourceIndex, lowerQuery, queryTokens, limits, matches, state,
|
|
231
|
+
budget: sharedBudget, deadline, clockImpl, maxEntries,
|
|
232
|
+
lstatImpl, realpathImpl, readdirImpl, readFileImpl,
|
|
233
|
+
};
|
|
234
|
+
while (stack.length > 0) {
|
|
235
|
+
assertDeadline(deadline, clockImpl);
|
|
236
|
+
if (await scanDirectory(stack, context)) break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
matches.sort(compareMatches);
|
|
240
|
+
return matches;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function scanDirectory(stack, context) {
|
|
244
|
+
const { resolvedRoot, limits, matches, state, maxEntries, readdirImpl } = context;
|
|
245
|
+
const relativeDir = stack.pop();
|
|
246
|
+
const absoluteDir = path.join(resolvedRoot, relativeDir);
|
|
247
|
+
let entries;
|
|
248
|
+
try {
|
|
249
|
+
entries = await readdirImpl(absoluteDir, { withFileTypes: true });
|
|
250
|
+
} catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const names = entries.map((entry) => entry.name).sort();
|
|
254
|
+
for (let index = names.length - 1; index >= 0; index -= 1) {
|
|
255
|
+
if (matches.length >= limits.maxMatchesPerSource) return true;
|
|
256
|
+
if (state.entriesVisited >= maxEntries) return true;
|
|
257
|
+
state.entriesVisited += 1;
|
|
258
|
+
const name = names[index];
|
|
259
|
+
const relativePath = relativeDir === "." ? name : `${relativeDir}/${name}`;
|
|
260
|
+
const outcome = await processDirectoryEntry(resolvedRoot, relativePath, name, context);
|
|
261
|
+
if (outcome === "directory" && !SKIPPED_DIRECTORIES.has(name)) stack.push(relativePath);
|
|
262
|
+
}
|
|
263
|
+
return matches.length >= limits.maxMatchesPerSource || state.entriesVisited >= maxEntries;
|
|
264
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
2
|
+
import { DECISION_ERROR_CODES, decisionError } from "../../core/decision/errors.js";
|
|
3
|
+
import { PINNED_JEV_MODEL } from "../../core/decision/constants.js";
|
|
4
|
+
|
|
5
|
+
export function hasTypesafeCredentials(env = process.env) {
|
|
6
|
+
return typeof env?.TYPESAFE_API_KEY === "string" && env.TYPESAFE_API_KEY.trim().length > 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function validateTypesafeCredential(value) {
|
|
10
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
11
|
+
throw decisionError(DECISION_ERROR_CODES.AUTH_REQUIRED, "TYPESAFE_API_KEY is required for a semantic decision.");
|
|
12
|
+
}
|
|
13
|
+
if (value !== value.trim() || /[\r\n\u0000-\u001F\u007F]/u.test(value) || value.length > 4096) {
|
|
14
|
+
throw decisionError(DECISION_ERROR_CODES.AUTH_INVALID, "TYPESAFE_API_KEY has an invalid format.");
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createTypesafeClient(policy, { env = process.env } = {}) {
|
|
20
|
+
validateTypesafeCredential(env?.TYPESAFE_API_KEY);
|
|
21
|
+
if (policy?.model !== PINNED_JEV_MODEL) throw decisionError(DECISION_ERROR_CODES.MODEL_UNSUPPORTED, "ForgeLoop requires the pinned Jev model.");
|
|
22
|
+
return new TypeSafeClient({
|
|
23
|
+
defaultModel: PINNED_JEV_MODEL,
|
|
24
|
+
timeout: policy.requestTimeoutMs,
|
|
25
|
+
retry: { maxRetries: policy.maxRetries },
|
|
26
|
+
logLevel: "warn",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { choice, noul } from "@typesafe-ai/sdk";
|
|
2
|
+
import { createTypesafeClient, hasTypesafeCredentials } from "./client.js";
|
|
3
|
+
import { normalizeTypesafeResult } from "./normalize.js";
|
|
4
|
+
import { DECISION_ENGINE_ID, PINNED_JEV_MODEL } from "../../core/decision/constants.js";
|
|
5
|
+
import { DECISION_ERROR_CODES, decisionError, safeDecisionError } from "../../core/decision/errors.js";
|
|
6
|
+
|
|
7
|
+
function safeFailureFields(error) {
|
|
8
|
+
const diagnostics = error?.diagnostics;
|
|
9
|
+
if (!diagnostics) return {};
|
|
10
|
+
return {
|
|
11
|
+
httpStatus: diagnostics.httpStatus ?? null,
|
|
12
|
+
providerErrorType: diagnostics.providerErrorType ?? null,
|
|
13
|
+
requestId: diagnostics.requestId ?? null,
|
|
14
|
+
networkClass: diagnostics.networkClass ?? null,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sdkQuestions(questionSet) {
|
|
19
|
+
return Object.fromEntries(Object.entries(questionSet.questions).map(([name, question]) => {
|
|
20
|
+
if (question.type === "choice") return [name, choice(`Answer the bounded ${name} question.`, question.criteria)];
|
|
21
|
+
return [name, noul(`Answer the bounded ${name} question.`, question.criteria)];
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createTypesafeEngine({ policy }) {
|
|
26
|
+
const engine = {
|
|
27
|
+
id: DECISION_ENGINE_ID,
|
|
28
|
+
model: PINNED_JEV_MODEL,
|
|
29
|
+
async evaluate(request) {
|
|
30
|
+
if (!hasTypesafeCredentials()) throw decisionError(DECISION_ERROR_CODES.AUTH_REQUIRED, "TYPESAFE_API_KEY is required for a semantic decision.");
|
|
31
|
+
const started = Date.now();
|
|
32
|
+
try {
|
|
33
|
+
const client = createTypesafeClient(policy);
|
|
34
|
+
const raw = await client.systemOne({ state: request.state, questions: sdkQuestions(request.questionSet), model: PINNED_JEV_MODEL }, { timeout: policy.requestTimeoutMs, retry: { maxRetries: policy.maxRetries } });
|
|
35
|
+
return { ...normalizeTypesafeResult(raw, request.questionSet), latencyMs: Date.now() - started };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw safeDecisionError(error);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async health() {
|
|
41
|
+
if (!hasTypesafeCredentials()) return { status: "missing", engine: DECISION_ENGINE_ID, model: PINNED_JEV_MODEL };
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
try {
|
|
44
|
+
const client = createTypesafeClient(policy);
|
|
45
|
+
const raw = await client.systemOne({ state: { healthCheck: true }, questions: { healthy: noul("Is this bounded health-check request well-formed?", { yes: "The request is well-formed", no: "The request is not well-formed" }) }, model: PINNED_JEV_MODEL }, { timeout: policy.requestTimeoutMs, retry: { maxRetries: 0 } });
|
|
46
|
+
if (raw.model !== PINNED_JEV_MODEL) throw decisionError(DECISION_ERROR_CODES.MODEL_UNSUPPORTED, "The semantic decision service returned an unsupported model.");
|
|
47
|
+
return { status: "healthy", engine: DECISION_ENGINE_ID, model: PINNED_JEV_MODEL, latencyMs: Date.now() - started, usage: raw.usage ?? null };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const normalized = safeDecisionError(error);
|
|
50
|
+
return {
|
|
51
|
+
status: normalized.code === DECISION_ERROR_CODES.RATE_LIMITED ? "rate-limited"
|
|
52
|
+
: normalized.code === DECISION_ERROR_CODES.AUTH_INVALID ? "invalid"
|
|
53
|
+
: "unreachable",
|
|
54
|
+
engine: DECISION_ENGINE_ID,
|
|
55
|
+
model: PINNED_JEV_MODEL,
|
|
56
|
+
errorCode: normalized.code,
|
|
57
|
+
...safeFailureFields(normalized),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
return Object.freeze(engine);
|
|
63
|
+
}
|