@functional-systems/srcx 0.1.0-dev.4 → 0.1.0-dev.40
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/CHANGELOG.md +179 -0
- package/CONTRIBUTING.md +4 -0
- package/DESIGN.md +378 -58
- package/README.md +628 -35
- package/RELEASING.md +15 -3
- package/VALIDATION.md +1120 -16
- package/dist/artifact.d.ts +31 -0
- package/dist/artifact.js +237 -0
- package/dist/artifact.js.map +1 -0
- package/dist/build.d.ts +86 -3
- package/dist/build.js +233 -71
- package/dist/build.js.map +1 -1
- package/dist/chunk.d.ts +9 -3
- package/dist/chunk.js +341 -14
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +130 -21
- package/dist/cli.js.map +1 -1
- package/dist/file-policy.d.ts +20 -0
- package/dist/file-policy.js +103 -0
- package/dist/file-policy.js.map +1 -0
- package/dist/implementation.d.ts +11 -0
- package/dist/implementation.js +191 -0
- package/dist/implementation.js.map +1 -0
- package/dist/publish.d.ts +18 -0
- package/dist/publish.js +224 -46
- package/dist/publish.js.map +1 -1
- package/dist/releases.js +21 -3
- package/dist/releases.js.map +1 -1
- package/dist/remote.d.ts +1 -0
- package/dist/remote.js +50 -9
- package/dist/remote.js.map +1 -1
- package/dist/repository.d.ts +4 -1
- package/dist/repository.js +26 -11
- package/dist/repository.js.map +1 -1
- package/dist/rerank.d.ts +16 -0
- package/dist/rerank.js +87 -0
- package/dist/rerank.js.map +1 -0
- package/dist/search.d.ts +18 -3
- package/dist/search.js +143 -23
- package/dist/search.js.map +1 -1
- package/dist/skills.d.ts +24 -0
- package/dist/skills.js +155 -0
- package/dist/skills.js.map +1 -0
- package/package.json +17 -6
- package/runtime/grammars/README.md +50 -0
- package/runtime/grammars/tree-sitter-bash.LICENSE +21 -0
- package/runtime/grammars/tree-sitter-bash.wasm +0 -0
- package/runtime/grammars/tree-sitter-sql.LICENSE +21 -0
- package/runtime/grammars/tree-sitter-sql.wasm +0 -0
- package/runtime/grammars/tree-sitter-wasm.LICENSE +21 -0
- package/runtime/qwen.py +132 -0
- package/runtime/requirements.txt +34 -0
- package/skills/srcx-search/SKILL.md +103 -0
- package/src/artifact.ts +303 -0
- package/src/build.ts +313 -84
- package/src/chunk.ts +360 -12
- package/src/cli.ts +263 -23
- package/src/file-policy.ts +166 -0
- package/src/implementation.ts +253 -0
- package/src/publish.ts +321 -47
- package/src/releases.ts +36 -3
- package/src/remote.ts +70 -13
- package/src/repository.ts +40 -11
- package/src/rerank.ts +131 -0
- package/src/search.ts +229 -28
- package/src/skills.ts +205 -0
package/src/cli.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { Command, InvalidArgumentError } from "commander";
|
|
3
|
+
import { Command, InvalidArgumentError, Option } from "commander";
|
|
4
4
|
import { mkdir } from "node:fs/promises";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { configure, loadSettings, stateRoot } from "./settings.js";
|
|
8
|
-
import { identity } from "./git.js";
|
|
9
|
-
import {
|
|
8
|
+
import { identity, resolveCommit } from "./git.js";
|
|
9
|
+
import {
|
|
10
|
+
loadBuild,
|
|
11
|
+
materialize,
|
|
12
|
+
validateBuild,
|
|
13
|
+
type Build,
|
|
14
|
+
type Preset,
|
|
15
|
+
PRESET,
|
|
16
|
+
presetFor,
|
|
17
|
+
normalizeAnalyzers,
|
|
18
|
+
ANALYZERS,
|
|
19
|
+
} from "./build.js";
|
|
20
|
+
import { normalizeFilePolicy } from "./file-policy.js";
|
|
10
21
|
import { invariant, optionalJson } from "./common.js";
|
|
11
22
|
import { LambdaRemote } from "./remote.js";
|
|
12
23
|
import {
|
|
@@ -16,9 +27,23 @@ import {
|
|
|
16
27
|
register,
|
|
17
28
|
selectRepository,
|
|
18
29
|
} from "./repository.js";
|
|
19
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
exclusive,
|
|
32
|
+
lastBuildPath,
|
|
33
|
+
publish,
|
|
34
|
+
published,
|
|
35
|
+
type Published,
|
|
36
|
+
} from "./publish.js";
|
|
20
37
|
import { resolveVersion, syncTags } from "./releases.js";
|
|
21
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
directHandle,
|
|
40
|
+
loadHandle,
|
|
41
|
+
readHandle,
|
|
42
|
+
search,
|
|
43
|
+
lookup,
|
|
44
|
+
} from "./search.js";
|
|
45
|
+
import { candidateLimit } from "./rerank.js";
|
|
46
|
+
import { manageSkill, SKILL_AGENTS, SKILL_SCOPES } from "./skills.js";
|
|
22
47
|
const cli = new Command()
|
|
23
48
|
.name("srcx")
|
|
24
49
|
.description("Version-aware code search on LambdaDB")
|
|
@@ -39,10 +64,48 @@ const integer = (value: string) => {
|
|
|
39
64
|
throw new InvalidArgumentError("Integer is too large.");
|
|
40
65
|
return n;
|
|
41
66
|
};
|
|
67
|
+
const analyzerOption = (description: string) =>
|
|
68
|
+
new Option(
|
|
69
|
+
"--analyzers <names>",
|
|
70
|
+
description + ` (${ANALYZERS.join(", ")}; default: standard)`,
|
|
71
|
+
).argParser((value) =>
|
|
72
|
+
normalizeAnalyzers(value.split(",").map((name) => name.trim())),
|
|
73
|
+
);
|
|
74
|
+
const filePolicyOption = () =>
|
|
75
|
+
new Option(
|
|
76
|
+
"--file-policy <json>",
|
|
77
|
+
"Path to file policy JSON; pinned at registration or local dry run",
|
|
78
|
+
).argParser((path) =>
|
|
79
|
+
normalizeFilePolicy(JSON.parse(readFileSync(path, "utf8"))),
|
|
80
|
+
);
|
|
42
81
|
async function connected() {
|
|
43
82
|
const settings = await loadSettings();
|
|
44
83
|
return { settings, remote: new LambdaRemote(settings) };
|
|
45
84
|
}
|
|
85
|
+
const skills = cli
|
|
86
|
+
.command("skills")
|
|
87
|
+
.description(
|
|
88
|
+
"Install and manage the bundled agent skill (no credentials required)",
|
|
89
|
+
);
|
|
90
|
+
for (const operation of ["install", "update", "remove", "status"] as const) {
|
|
91
|
+
skills
|
|
92
|
+
.command(operation)
|
|
93
|
+
.addOption(
|
|
94
|
+
new Option("--agent <name>", "Agent to configure")
|
|
95
|
+
.choices(SKILL_AGENTS)
|
|
96
|
+
.makeOptionMandatory(),
|
|
97
|
+
)
|
|
98
|
+
.addOption(
|
|
99
|
+
new Option("--scope <scope>", "Installation scope")
|
|
100
|
+
.choices(SKILL_SCOPES)
|
|
101
|
+
.default("user"),
|
|
102
|
+
)
|
|
103
|
+
.option(
|
|
104
|
+
"--path <directory>",
|
|
105
|
+
"Project directory (project scope only; default: current directory)",
|
|
106
|
+
)
|
|
107
|
+
.action(async (options) => output(await manageSkill(operation, options)));
|
|
108
|
+
}
|
|
46
109
|
cli
|
|
47
110
|
.command("configure")
|
|
48
111
|
.requiredOption("--endpoint <origin>")
|
|
@@ -81,6 +144,16 @@ repo
|
|
|
81
144
|
.option("--remote <name>")
|
|
82
145
|
.option("--description <text>")
|
|
83
146
|
.option("--tag <key=value>", "One optional Collection metadata tag")
|
|
147
|
+
.addOption(analyzerOption("Comma-separated text analyzers"))
|
|
148
|
+
.addOption(filePolicyOption())
|
|
149
|
+
.addOption(
|
|
150
|
+
new Option(
|
|
151
|
+
"--embedding <model>",
|
|
152
|
+
"Managed model (source text is sent to the provider on import)",
|
|
153
|
+
)
|
|
154
|
+
.choices(["none", "text-embedding-3-small", "text-embedding-3-large"])
|
|
155
|
+
.default("none"),
|
|
156
|
+
)
|
|
84
157
|
.action(async (o) => {
|
|
85
158
|
const { remote } = await connected();
|
|
86
159
|
let labels: Record<string, string> | undefined;
|
|
@@ -95,6 +168,7 @@ repo
|
|
|
95
168
|
remote: o.remote,
|
|
96
169
|
description: o.description,
|
|
97
170
|
labels,
|
|
171
|
+
preset: presetFor(o.embedding, o.analyzers, o.filePolicy),
|
|
98
172
|
}),
|
|
99
173
|
);
|
|
100
174
|
});
|
|
@@ -122,7 +196,27 @@ cli
|
|
|
122
196
|
.option("--repo <name>", "Registered remote repository")
|
|
123
197
|
.option("--ref <ref>", "Local branch, Git tag, or commit OID")
|
|
124
198
|
.option("--dry-run", "Only materialize and validate a local build")
|
|
199
|
+
.addOption(filePolicyOption())
|
|
200
|
+
.addOption(
|
|
201
|
+
new Option(
|
|
202
|
+
"--embedding <model>",
|
|
203
|
+
"Preset for --dry-run --path; connected imports use the repository preset",
|
|
204
|
+
).choices(["none", "text-embedding-3-small", "text-embedding-3-large"]),
|
|
205
|
+
)
|
|
206
|
+
.addOption(
|
|
207
|
+
analyzerOption(
|
|
208
|
+
"Text analyzers for --dry-run --path; connected imports use the repository preset",
|
|
209
|
+
),
|
|
210
|
+
)
|
|
125
211
|
.option("--output <directory>", "New artifact directory")
|
|
212
|
+
.addOption(
|
|
213
|
+
new Option(
|
|
214
|
+
"--artifact-storage <storage>",
|
|
215
|
+
"Experimental local artifact storage",
|
|
216
|
+
)
|
|
217
|
+
.choices(["jsonl", "segments"])
|
|
218
|
+
.default("jsonl"),
|
|
219
|
+
)
|
|
126
220
|
.option(
|
|
127
221
|
"--previous <directory>",
|
|
128
222
|
"Previous build artifact for a dry-run diff",
|
|
@@ -133,7 +227,19 @@ cli
|
|
|
133
227
|
"Resume the same pending artifact after inspecting its journal",
|
|
134
228
|
)
|
|
135
229
|
.option("--timeout <seconds>", "Validation wait deadline", integer, 120)
|
|
136
|
-
.action(async (o) => {
|
|
230
|
+
.action(async (o, command) => {
|
|
231
|
+
invariant(
|
|
232
|
+
!o.embedding || (o.dryRun && o.path),
|
|
233
|
+
"--embedding is only accepted with --dry-run --path; connected imports use the repository preset.",
|
|
234
|
+
);
|
|
235
|
+
invariant(
|
|
236
|
+
!o.analyzers || (o.dryRun && o.path),
|
|
237
|
+
"--analyzers is only accepted with --dry-run --path; connected imports use the repository preset.",
|
|
238
|
+
);
|
|
239
|
+
invariant(
|
|
240
|
+
!o.filePolicy || (o.dryRun && o.path),
|
|
241
|
+
"--file-policy is only accepted with --dry-run --path; connected imports use the repository preset.",
|
|
242
|
+
);
|
|
137
243
|
invariant(!(o.path && o.repo), "Choose --path or --repo.");
|
|
138
244
|
invariant(
|
|
139
245
|
!o.previous || o.dryRun,
|
|
@@ -144,19 +250,41 @@ cli
|
|
|
144
250
|
!(o.artifact && (o.ref || o.path || o.output || o.previous)),
|
|
145
251
|
"--artifact cannot be combined with --ref/--path/--output/--previous.",
|
|
146
252
|
);
|
|
253
|
+
invariant(
|
|
254
|
+
!o.artifact || command.getOptionValueSource("artifactStorage") !== "cli",
|
|
255
|
+
"--artifact-storage cannot be combined with --artifact; the artifact records its storage format.",
|
|
256
|
+
);
|
|
147
257
|
const buildLocal = async (
|
|
148
258
|
source: Awaited<ReturnType<typeof identity>>,
|
|
149
259
|
previous?: Build,
|
|
260
|
+
state?: string,
|
|
261
|
+
preset: Preset = PRESET,
|
|
150
262
|
) => {
|
|
151
263
|
invariant(o.ref, "--ref is required to build a commit.");
|
|
264
|
+
const commit = await resolveCommit(source.path, o.ref);
|
|
265
|
+
if (state) {
|
|
266
|
+
const last = await optionalJson<{ artifact: string }>(
|
|
267
|
+
lastBuildPath(state, commit.branch),
|
|
268
|
+
);
|
|
269
|
+
if (last) {
|
|
270
|
+
try {
|
|
271
|
+
previous = await loadBuild(last.artifact);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
152
277
|
const parent = join(stateRoot(), "builds");
|
|
153
278
|
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
154
279
|
const path = o.output ? resolve(o.output) : join(parent, randomUUID());
|
|
155
280
|
return materialize({
|
|
156
281
|
identity: source,
|
|
157
282
|
ref: o.ref,
|
|
283
|
+
resolvedCommit: commit,
|
|
158
284
|
output: path,
|
|
159
285
|
previous,
|
|
286
|
+
storage: o.artifactStorage,
|
|
287
|
+
preset,
|
|
160
288
|
});
|
|
161
289
|
};
|
|
162
290
|
if (o.dryRun) {
|
|
@@ -166,23 +294,26 @@ cli
|
|
|
166
294
|
await validateBuild(b);
|
|
167
295
|
} else {
|
|
168
296
|
let source;
|
|
297
|
+
let preset = presetFor(o.embedding, o.analyzers, o.filePolicy);
|
|
169
298
|
if (o.path) source = await identity(o.path, o.remote);
|
|
170
299
|
else {
|
|
171
300
|
invariant(o.repo, "Use --path for a credential-free dry run.");
|
|
172
301
|
const { remote, settings } = await connected();
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
);
|
|
302
|
+
const selected = await selectRepository(remote, o.repo);
|
|
303
|
+
preset = selected.preset ?? PRESET;
|
|
304
|
+
source = await attachment(settings, selected);
|
|
177
305
|
}
|
|
178
306
|
b = await buildLocal(
|
|
179
307
|
source,
|
|
180
308
|
o.previous ? await loadBuild(o.previous) : undefined,
|
|
309
|
+
undefined,
|
|
310
|
+
preset,
|
|
181
311
|
);
|
|
182
312
|
}
|
|
183
313
|
output({
|
|
184
314
|
status: "locally-validated",
|
|
185
315
|
artifact: b.directory,
|
|
316
|
+
artifactFormat: b.format,
|
|
186
317
|
commitOid: b.commitOid,
|
|
187
318
|
configHash: b.configHash,
|
|
188
319
|
counts: b.counts,
|
|
@@ -192,6 +323,8 @@ cli
|
|
|
192
323
|
path: e.path,
|
|
193
324
|
status: e.status,
|
|
194
325
|
reason: e.reason,
|
|
326
|
+
retrieval: e.retrieval,
|
|
327
|
+
policyReason: e.policyReason,
|
|
195
328
|
parseStatus: e.parseStatus,
|
|
196
329
|
})),
|
|
197
330
|
uploaded: false,
|
|
@@ -212,20 +345,34 @@ cli
|
|
|
212
345
|
// Pending attempts reconcile against the journal's immutable remote
|
|
213
346
|
// baseline; they do not need the previous build's local files.
|
|
214
347
|
const pending = await optionalJson(join(state, "pending.json"));
|
|
348
|
+
const b = o.artifact
|
|
349
|
+
? await loadBuild(o.artifact)
|
|
350
|
+
: await buildLocal(
|
|
351
|
+
await attachment(settings, r),
|
|
352
|
+
undefined,
|
|
353
|
+
pending ? undefined : state,
|
|
354
|
+
r.preset ?? PRESET,
|
|
355
|
+
);
|
|
215
356
|
let baseline: { version: Published; build: Build } | undefined;
|
|
216
|
-
|
|
357
|
+
// Tracked branches discover their own immutable baseline remotely.
|
|
358
|
+
// Manual SHA/tag imports retain the previous frozen workspace optimization.
|
|
359
|
+
if (!pending && !b.branchRef) {
|
|
217
360
|
const last = await optionalJson<{
|
|
218
361
|
artifact: string;
|
|
219
362
|
version: Published;
|
|
220
|
-
}>(
|
|
363
|
+
}>(lastBuildPath(state));
|
|
221
364
|
if (last) {
|
|
222
|
-
|
|
223
|
-
|
|
365
|
+
try {
|
|
366
|
+
baseline = {
|
|
367
|
+
version: last.version,
|
|
368
|
+
build: await loadBuild(last.artifact),
|
|
369
|
+
};
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT")
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
224
374
|
}
|
|
225
375
|
}
|
|
226
|
-
const b = o.artifact
|
|
227
|
-
? await loadBuild(o.artifact)
|
|
228
|
-
: await buildLocal(await attachment(settings, r), baseline?.build);
|
|
229
376
|
return publish({
|
|
230
377
|
store,
|
|
231
378
|
binding: r,
|
|
@@ -249,7 +396,7 @@ cli
|
|
|
249
396
|
cli
|
|
250
397
|
.command("resolve")
|
|
251
398
|
.requiredOption("--repo <name>")
|
|
252
|
-
.requiredOption("--ref <commit-or-release>")
|
|
399
|
+
.requiredOption("--ref <commit-branch-or-release>")
|
|
253
400
|
.action(async (o) => {
|
|
254
401
|
const { remote } = await connected();
|
|
255
402
|
const r = await selectRepository(remote, o.repo);
|
|
@@ -258,18 +405,102 @@ cli
|
|
|
258
405
|
cli
|
|
259
406
|
.command("search")
|
|
260
407
|
.requiredOption("--repo <name>")
|
|
261
|
-
.requiredOption("--version <commit-or-release>")
|
|
262
|
-
.requiredOption(
|
|
408
|
+
.requiredOption("--version <commit-branch-or-release>")
|
|
409
|
+
.requiredOption(
|
|
410
|
+
"--query <text>",
|
|
411
|
+
"Search text; use the target source language",
|
|
412
|
+
)
|
|
413
|
+
.addHelpText(
|
|
414
|
+
"after",
|
|
415
|
+
"\nQuery language: any language is accepted; queries are not automatically translated.\n" +
|
|
416
|
+
"For English code/comments, prefer English terms and preserve identifiers, API names and error messages.\n" +
|
|
417
|
+
"For Korean documentation, use Korean queries with the korean analyzer.\n" +
|
|
418
|
+
"Agents can translate the search query while answering in the user's language.\n",
|
|
419
|
+
)
|
|
263
420
|
.option("--limit <count>", "Maximum hits", integer, 10)
|
|
421
|
+
.addOption(
|
|
422
|
+
new Option(
|
|
423
|
+
"--mode <mode>",
|
|
424
|
+
"Retrieval method; semantic/hybrid incur managed query embedding usage",
|
|
425
|
+
)
|
|
426
|
+
.choices(["lexical", "semantic", "hybrid"])
|
|
427
|
+
.default("lexical"),
|
|
428
|
+
)
|
|
429
|
+
.addOption(
|
|
430
|
+
new Option(
|
|
431
|
+
"--rerank <model>",
|
|
432
|
+
"Opt-in local reranker (requires separate Python/model setup)",
|
|
433
|
+
).choices(["qwen"]),
|
|
434
|
+
)
|
|
435
|
+
.option(
|
|
436
|
+
"--candidates <count>",
|
|
437
|
+
"Rerank pool size (default: max(50, limit); max: 100)",
|
|
438
|
+
integer,
|
|
439
|
+
)
|
|
264
440
|
.option("--path <path>", "Exact path filter")
|
|
265
|
-
.option("--language <name>")
|
|
441
|
+
.option("--language <name>", "Exact programming-language filter")
|
|
266
442
|
.action(async (o) => {
|
|
443
|
+
const started = performance.now();
|
|
444
|
+
candidateLimit(o.limit, o);
|
|
267
445
|
const { remote, settings } = await connected();
|
|
268
446
|
const r = await selectRepository(remote, o.repo),
|
|
269
447
|
store = remote.store(r.collection),
|
|
270
448
|
v = await resolveVersion(store, r, o.version);
|
|
271
449
|
output(
|
|
272
|
-
await search(
|
|
450
|
+
await search(
|
|
451
|
+
store,
|
|
452
|
+
settings,
|
|
453
|
+
r,
|
|
454
|
+
v,
|
|
455
|
+
o.query,
|
|
456
|
+
o.limit,
|
|
457
|
+
{
|
|
458
|
+
path: o.path,
|
|
459
|
+
language: o.language,
|
|
460
|
+
},
|
|
461
|
+
o.mode,
|
|
462
|
+
{
|
|
463
|
+
rerank: o.rerank,
|
|
464
|
+
candidates: o.candidates,
|
|
465
|
+
onTiming: (timing) =>
|
|
466
|
+
process.stderr.write(
|
|
467
|
+
JSON.stringify({
|
|
468
|
+
event: "rerank-timing",
|
|
469
|
+
...timing,
|
|
470
|
+
commandMs: performance.now() - started,
|
|
471
|
+
}) + "\n",
|
|
472
|
+
),
|
|
473
|
+
},
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
});
|
|
477
|
+
cli
|
|
478
|
+
.command("lookup")
|
|
479
|
+
.description(
|
|
480
|
+
"Find exact indexed symbol definitions and return their source chunks",
|
|
481
|
+
)
|
|
482
|
+
.requiredOption("--repo <name>")
|
|
483
|
+
.requiredOption("--version <commit-branch-or-release>")
|
|
484
|
+
.requiredOption("--symbol <name>", "Exact case-sensitive indexed symbol name")
|
|
485
|
+
.option("--path <path>", "Exact path filter for ambiguous names")
|
|
486
|
+
.option("--language <name>", "Exact programming-language filter")
|
|
487
|
+
.option(
|
|
488
|
+
"--limit <count>",
|
|
489
|
+
"Maximum source chunks (1–20; results may be partial)",
|
|
490
|
+
integer,
|
|
491
|
+
5,
|
|
492
|
+
)
|
|
493
|
+
.action(async (o) => {
|
|
494
|
+
invariant(
|
|
495
|
+
o.limit >= 1 && o.limit <= 20,
|
|
496
|
+
"Lookup limit must be between 1 and 20.",
|
|
497
|
+
);
|
|
498
|
+
const { remote, settings } = await connected();
|
|
499
|
+
const r = await selectRepository(remote, o.repo),
|
|
500
|
+
store = remote.store(r.collection);
|
|
501
|
+
const v = await resolveVersion(store, r, o.version);
|
|
502
|
+
output(
|
|
503
|
+
await lookup(store, settings, r, v, o.symbol, o.limit, {
|
|
273
504
|
path: o.path,
|
|
274
505
|
language: o.language,
|
|
275
506
|
}),
|
|
@@ -279,12 +510,20 @@ cli
|
|
|
279
510
|
.command("read")
|
|
280
511
|
.option("--result <id>")
|
|
281
512
|
.option("--repo <name>")
|
|
282
|
-
.option("--version <commit-or-release>")
|
|
513
|
+
.option("--version <commit-branch-or-release>")
|
|
283
514
|
.option("--path <path>")
|
|
284
515
|
.option("--lines <start:end>")
|
|
285
516
|
.option("--context <lines>", "Expand an evidence span", integer, 0)
|
|
286
517
|
.option("--full-file")
|
|
518
|
+
.option(
|
|
519
|
+
"--implementation",
|
|
520
|
+
"Read a linked Python/TypeScript overload implementation",
|
|
521
|
+
)
|
|
287
522
|
.action(async (o) => {
|
|
523
|
+
invariant(
|
|
524
|
+
!o.implementation || (o.result && !o.fullFile && !o.lines && !o.context),
|
|
525
|
+
"--implementation requires --result and cannot be combined with range options.",
|
|
526
|
+
);
|
|
288
527
|
invariant(
|
|
289
528
|
!(o.fullFile && (o.lines || o.context)),
|
|
290
529
|
"--full-file cannot be combined with --lines or --context.",
|
|
@@ -331,6 +570,7 @@ cli
|
|
|
331
570
|
context: o.context,
|
|
332
571
|
fullFile: o.fullFile || (!o.result && !o.lines),
|
|
333
572
|
lines,
|
|
573
|
+
implementation: o.implementation,
|
|
334
574
|
},
|
|
335
575
|
),
|
|
336
576
|
);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { invariant } from "./common.js";
|
|
2
|
+
|
|
3
|
+
export type FileAction = "exclude" | "lexical" | "semantic";
|
|
4
|
+
export type FileRule = { pattern: string; action: FileAction };
|
|
5
|
+
export type FilePolicy = { version: 1; rules: FileRule[] };
|
|
6
|
+
export const FILE_POLICY: FilePolicy = { version: 1, rules: [] };
|
|
7
|
+
export type FileDecision = { action: FileAction; reason: string };
|
|
8
|
+
|
|
9
|
+
/** Deliberately small glob syntax: repo-relative paths, *, ?, and whole-segment **. */
|
|
10
|
+
function glob(pattern: string): RegExp {
|
|
11
|
+
let expression = "^";
|
|
12
|
+
const parts = pattern.split("/");
|
|
13
|
+
for (const [index, part] of parts.entries()) {
|
|
14
|
+
if (part === "**")
|
|
15
|
+
expression += index === parts.length - 1 ? "[\\s\\S]*" : "(?:[^/]+/)*";
|
|
16
|
+
else {
|
|
17
|
+
for (const char of part)
|
|
18
|
+
expression +=
|
|
19
|
+
char === "*"
|
|
20
|
+
? "[^/]*"
|
|
21
|
+
: char === "?"
|
|
22
|
+
? "[^/]"
|
|
23
|
+
: char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
+
if (index !== parts.length - 1) expression += "/";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return new RegExp(expression + "$", "u");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeFilePolicy(value: unknown): FilePolicy {
|
|
31
|
+
invariant(
|
|
32
|
+
value !== null && typeof value === "object" && !Array.isArray(value),
|
|
33
|
+
"File policy must be an object.",
|
|
34
|
+
);
|
|
35
|
+
const policy = value as FilePolicy;
|
|
36
|
+
invariant(
|
|
37
|
+
Object.keys(policy).sort().join(",") === "rules,version" &&
|
|
38
|
+
policy.version === 1 &&
|
|
39
|
+
Array.isArray(policy.rules) &&
|
|
40
|
+
policy.rules.length <= 100,
|
|
41
|
+
"File policy requires version: 1 and at most 100 rules.",
|
|
42
|
+
);
|
|
43
|
+
const rules = policy.rules.map((rule) => {
|
|
44
|
+
invariant(
|
|
45
|
+
rule &&
|
|
46
|
+
typeof rule === "object" &&
|
|
47
|
+
Object.keys(rule).sort().join(",") === "action,pattern" &&
|
|
48
|
+
["exclude", "lexical", "semantic"].includes(rule.action),
|
|
49
|
+
"Each file rule requires pattern and action (exclude, lexical, semantic).",
|
|
50
|
+
);
|
|
51
|
+
invariant(
|
|
52
|
+
typeof rule.pattern === "string" &&
|
|
53
|
+
rule.pattern.length > 0 &&
|
|
54
|
+
rule.pattern.length <= 512 &&
|
|
55
|
+
!/[\\\x00-\x1f\x7f\[\]{}!]/.test(rule.pattern) &&
|
|
56
|
+
rule.pattern
|
|
57
|
+
.split("/")
|
|
58
|
+
.every(
|
|
59
|
+
(part) =>
|
|
60
|
+
part &&
|
|
61
|
+
part !== "." &&
|
|
62
|
+
part !== ".." &&
|
|
63
|
+
(!part.includes("**") || part === "**"),
|
|
64
|
+
),
|
|
65
|
+
"File policy patterns must be relative paths using only *, ?, and whole-segment ** wildcards.",
|
|
66
|
+
);
|
|
67
|
+
glob(rule.pattern);
|
|
68
|
+
return { pattern: rule.pattern, action: rule.action };
|
|
69
|
+
});
|
|
70
|
+
return { version: 1, rules };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** These bodies cannot be re-enabled by repository rules. This is not a secret scanner. */
|
|
74
|
+
export function hardExclusion(path: string): string | undefined {
|
|
75
|
+
const name = path.split("/").at(-1)!;
|
|
76
|
+
if (
|
|
77
|
+
(/^\.env(?:\.|$)/i.test(name) &&
|
|
78
|
+
!/\.(example|sample|template)$/i.test(name)) ||
|
|
79
|
+
/^(\.npmrc|\.pypirc|\.netrc|_netrc|id_rsa|id_dsa|id_ecdsa|id_ed25519)$/i.test(
|
|
80
|
+
name,
|
|
81
|
+
) ||
|
|
82
|
+
/(^|\/)\.(aws|ssh)(\/|$)/i.test(path)
|
|
83
|
+
)
|
|
84
|
+
return "credential-file";
|
|
85
|
+
if (
|
|
86
|
+
/\.(pem|key|crt|cer|csr|der|p12|pfx|p7b|p7c|jks|keystore|srl)$/i.test(path)
|
|
87
|
+
)
|
|
88
|
+
return "crypto-material";
|
|
89
|
+
if (
|
|
90
|
+
/\.(svgz?|png|apng|jpe?g|jfif|gif|webp|avif|bmp|dib|tiff?|ico|icns|heic|heif|jxl|psd|ai|eps|pnm|pbm|pgm|ppm|xbm|xpm)$/i.test(
|
|
91
|
+
path,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
return "image-extension";
|
|
95
|
+
if (
|
|
96
|
+
/\.(pdf|zip|gz|bz2|xz|7z|tar|rar|jar|class|woff2?|ttf|otf|mp[34]|wav|ogg|mov|exe|dll|so|dylib|wasm|pyc|pyo|o|a|sqlite3?|db)$/i.test(
|
|
97
|
+
path,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
return "binary-extension";
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function privateKeyContent(source: string): boolean {
|
|
105
|
+
return /(?:^|\r?\n)-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----\r?\n/.test(
|
|
106
|
+
source,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function excludedDirectory(path: string): boolean {
|
|
111
|
+
// Under a Java source-set root, build/dist/target/coverage can be package
|
|
112
|
+
// names. Output directories above that root still exclude the whole tree.
|
|
113
|
+
const javaRoot = /(^|\/)src\/[^/]+\/java\//i.exec(path);
|
|
114
|
+
const outputPath =
|
|
115
|
+
javaRoot && /\.java$/i.test(path) ? path.slice(0, javaRoot.index) : path;
|
|
116
|
+
return (
|
|
117
|
+
/(^|\/)(node_modules|vendor|vendored|third_party|third-party|\.git|\.next|\.nuxt|\.venv|venv|__pycache__|\.cache|\.pytest_cache|\.mypy_cache|\.ruff_cache|\.tox|\.nox|\.srcx)(\/|$)/i.test(
|
|
118
|
+
path,
|
|
119
|
+
) || /(^|\/)(dist|build|target|coverage)(\/|$)/i.test(outputPath)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Explicit rules override defaults, in order; the final matching rule wins. */
|
|
124
|
+
export function fileDecision(
|
|
125
|
+
path: string,
|
|
126
|
+
policy: FilePolicy,
|
|
127
|
+
source?: string,
|
|
128
|
+
): FileDecision {
|
|
129
|
+
let decision: FileDecision = { action: "semantic", reason: "source" };
|
|
130
|
+
if (excludedDirectory(path))
|
|
131
|
+
decision = { action: "exclude", reason: "dependency-or-build-output" };
|
|
132
|
+
else if (/\.(min\.(js|css)|map)$/i.test(path))
|
|
133
|
+
decision = { action: "exclude", reason: "generated-output" };
|
|
134
|
+
else if (/\.(log|log\.\d+)$/i.test(path))
|
|
135
|
+
decision = { action: "exclude", reason: "log-output" };
|
|
136
|
+
else if (
|
|
137
|
+
/(^|\/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|go\.sum|[^/]+\.lock|packages\.lock\.json|Package\.resolved)$/i.test(
|
|
138
|
+
path,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
decision = { action: "lexical", reason: "lockfile" };
|
|
142
|
+
else if (
|
|
143
|
+
/(^|\/)(licen[sc]e|copying|copyright|notice|authors|contributors)([.-][^/]*)?$/i.test(
|
|
144
|
+
path,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
decision = { action: "lexical", reason: "legal-or-attribution" };
|
|
148
|
+
else if (
|
|
149
|
+
/\.(csv|tsv|jsonl|ndjson|snap)$/i.test(path) ||
|
|
150
|
+
/(^|\/)__snapshots__\//i.test(path)
|
|
151
|
+
)
|
|
152
|
+
decision = { action: "lexical", reason: "data-or-snapshot" };
|
|
153
|
+
else if (
|
|
154
|
+
/\.(pb\.go|generated\.[^/]+|g\.[^/]+)$/i.test(path) ||
|
|
155
|
+
/_pb2(?:_grpc)?\.py$/i.test(path) ||
|
|
156
|
+
(source !== undefined &&
|
|
157
|
+
/(?:^|\n)[ \t]*(?:\/\/|#|\/\*|\*|<!--)[^\r\n]*(?:code generated[^\r\n]*do not edit|(?:auto[- ]?generated|generated by)[^\r\n]*do not edit|do not edit[^\r\n]*generated|@generated(?:[ \t]|$))/i.test(
|
|
158
|
+
Buffer.from(source, "utf8").subarray(0, 8192).toString("utf8"),
|
|
159
|
+
))
|
|
160
|
+
)
|
|
161
|
+
decision = { action: "lexical", reason: "generated-source" };
|
|
162
|
+
for (const rule of policy.rules)
|
|
163
|
+
if (glob(rule.pattern).test(path))
|
|
164
|
+
decision = { action: rule.action, reason: `file-rule:${rule.action}` };
|
|
165
|
+
return decision;
|
|
166
|
+
}
|