@functional-systems/srcx 0.1.0-dev.24 → 0.1.0-dev.25
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 +16 -0
- package/DESIGN.md +35 -7
- package/README.md +64 -3
- package/VALIDATION.md +94 -0
- package/dist/build.d.ts +5 -1
- package/dist/build.js +50 -25
- package/dist/build.js.map +1 -1
- package/dist/chunk.d.ts +2 -1
- package/dist/chunk.js +5 -4
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +9 -2
- package/dist/cli.js.map +1 -1
- package/dist/file-policy.d.ts +20 -0
- package/dist/file-policy.js +96 -0
- package/dist/file-policy.js.map +1 -0
- package/package.json +2 -2
- package/runtime/grammars/README.md +26 -2
- package/runtime/grammars/tree-sitter-bash.LICENSE +21 -0
- package/runtime/grammars/tree-sitter-bash.wasm +0 -0
- package/src/build.ts +71 -33
- package/src/chunk.ts +6 -6
- package/src/cli.ts +18 -2
- package/src/file-policy.ts +157 -0
package/src/build.ts
CHANGED
|
@@ -15,6 +15,15 @@ import {
|
|
|
15
15
|
type Doc,
|
|
16
16
|
optionalJson,
|
|
17
17
|
} from "./common.js";
|
|
18
|
+
import {
|
|
19
|
+
FILE_POLICY,
|
|
20
|
+
normalizeFilePolicy,
|
|
21
|
+
hardExclusion,
|
|
22
|
+
privateKeyContent,
|
|
23
|
+
fileDecision,
|
|
24
|
+
type FilePolicy,
|
|
25
|
+
type FileDecision,
|
|
26
|
+
} from "./file-policy.js";
|
|
18
27
|
export const ANALYZERS = ["english", "japanese", "korean", "standard"] as const;
|
|
19
28
|
export type Analyzer = (typeof ANALYZERS)[number];
|
|
20
29
|
export function normalizeAnalyzers(value: unknown): Analyzer[] {
|
|
@@ -42,6 +51,7 @@ export type Preset = {
|
|
|
42
51
|
/** Internal evaluation override; the CLI keeps its original enrichment. */
|
|
43
52
|
enrichment?: "path-only-v1";
|
|
44
53
|
maxFileBytes: number;
|
|
54
|
+
filePolicy: FilePolicy;
|
|
45
55
|
embedding: {
|
|
46
56
|
model: string;
|
|
47
57
|
dimensions: number;
|
|
@@ -57,6 +67,7 @@ export const PRESET: Preset = {
|
|
|
57
67
|
chunker: CHUNKER,
|
|
58
68
|
mode: "syntax",
|
|
59
69
|
maxFileBytes: 1024 * 1024,
|
|
70
|
+
filePolicy: FILE_POLICY,
|
|
60
71
|
embedding: null,
|
|
61
72
|
};
|
|
62
73
|
/** Embedding variants share the same chunking behavior. */
|
|
@@ -82,6 +93,7 @@ export const MANAGED_LARGE_PRESET: Preset = {
|
|
|
82
93
|
export function presetFor(
|
|
83
94
|
embedding = "none",
|
|
84
95
|
analyzers: readonly string[] = PRESET.analyzers,
|
|
96
|
+
filePolicy: unknown = FILE_POLICY,
|
|
85
97
|
): Preset {
|
|
86
98
|
invariant(
|
|
87
99
|
["none", "text-embedding-3-small", "text-embedding-3-large"].includes(
|
|
@@ -96,17 +108,21 @@ export function presetFor(
|
|
|
96
108
|
? MANAGED_LARGE_PRESET
|
|
97
109
|
: MANAGED_PRESET;
|
|
98
110
|
const normalized = normalizeAnalyzers(analyzers);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
const configured = {
|
|
112
|
+
...base,
|
|
113
|
+
analyzers: normalized,
|
|
114
|
+
filePolicy: normalizeFilePolicy(filePolicy),
|
|
115
|
+
};
|
|
116
|
+
return hash(configured) === hash(base) ? base : configured;
|
|
102
117
|
}
|
|
103
118
|
export function supportedPreset(preset: unknown): preset is Preset {
|
|
104
119
|
if (!preset || typeof preset !== "object" || Array.isArray(preset))
|
|
105
120
|
return false;
|
|
106
121
|
try {
|
|
107
122
|
const analyzers = normalizeAnalyzers((preset as Preset).analyzers);
|
|
123
|
+
const filePolicy = normalizeFilePolicy((preset as Preset).filePolicy);
|
|
108
124
|
return [PRESET, MANAGED_PRESET, MANAGED_LARGE_PRESET].some(
|
|
109
|
-
(base) => hash({ ...base, analyzers }) === hash(preset),
|
|
125
|
+
(base) => hash({ ...base, analyzers, filePolicy }) === hash(preset),
|
|
110
126
|
);
|
|
111
127
|
} catch {
|
|
112
128
|
return false;
|
|
@@ -154,6 +170,8 @@ export type InventoryItem = {
|
|
|
154
170
|
bytes: number;
|
|
155
171
|
status: "included" | "excluded";
|
|
156
172
|
reason?: string;
|
|
173
|
+
retrieval?: "lexical" | "semantic";
|
|
174
|
+
policyReason?: string;
|
|
157
175
|
targetBase64?: string;
|
|
158
176
|
fileId?: string;
|
|
159
177
|
chunkIds?: string[];
|
|
@@ -215,22 +233,6 @@ export async function loadBuild(directory: string): Promise<Build> {
|
|
|
215
233
|
b.directory = resolve(directory);
|
|
216
234
|
return b;
|
|
217
235
|
}
|
|
218
|
-
function exclude(path: string): string | undefined {
|
|
219
|
-
if (
|
|
220
|
-
/(^|\/)(node_modules|vendor|vendored|dist|build|target|coverage|\.git|\.next)(\/|$)/.test(
|
|
221
|
-
path,
|
|
222
|
-
)
|
|
223
|
-
)
|
|
224
|
-
return "dependency-or-build-output";
|
|
225
|
-
if (
|
|
226
|
-
/\.(png|jpe?g|gif|ico|pdf|zip|gz|jar|class|woff2?|mp[34]|exe|dll|so|dylib|wasm)$/i.test(
|
|
227
|
-
path,
|
|
228
|
-
)
|
|
229
|
-
)
|
|
230
|
-
return "binary-extension";
|
|
231
|
-
if (/\.(min\.(js|css)|map)$/.test(path)) return "generated-output";
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
236
|
export async function materialize(args: {
|
|
235
237
|
identity: Identity;
|
|
236
238
|
ref: string;
|
|
@@ -245,7 +247,8 @@ export async function materialize(args: {
|
|
|
245
247
|
configHash = hash(preset),
|
|
246
248
|
prev = args.previous;
|
|
247
249
|
invariant(
|
|
248
|
-
hash(preset.
|
|
250
|
+
hash(preset.filePolicy) === hash(normalizeFilePolicy(preset.filePolicy)) &&
|
|
251
|
+
hash(preset.analyzers) === hash(normalizeAnalyzers(preset.analyzers)) &&
|
|
249
252
|
hash(preset.chunker) === hash(CHUNKER) &&
|
|
250
253
|
["syntax", "window"].includes(preset.mode) &&
|
|
251
254
|
(preset.enrichment === undefined ||
|
|
@@ -317,7 +320,10 @@ export async function materialize(args: {
|
|
|
317
320
|
item.targetBase64 = (await blobs.read(e)).toString("base64");
|
|
318
321
|
continue;
|
|
319
322
|
}
|
|
320
|
-
const
|
|
323
|
+
const pathDecision = fileDecision(e.path, preset.filePolicy);
|
|
324
|
+
const reason =
|
|
325
|
+
hardExclusion(e.path) ??
|
|
326
|
+
(pathDecision.action === "exclude" ? pathDecision.reason : undefined);
|
|
321
327
|
if (reason) {
|
|
322
328
|
item.reason = reason;
|
|
323
329
|
continue;
|
|
@@ -350,6 +356,11 @@ export async function materialize(args: {
|
|
|
350
356
|
item.reason = "lfs-pointer";
|
|
351
357
|
continue;
|
|
352
358
|
}
|
|
359
|
+
if (privateKeyContent(source)) {
|
|
360
|
+
item.reason = "private-key-content";
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const decision = fileDecision(e.path, preset.filePolicy, source);
|
|
353
364
|
const parsed = await chunk(
|
|
354
365
|
source,
|
|
355
366
|
e.path,
|
|
@@ -378,6 +389,8 @@ export async function materialize(args: {
|
|
|
378
389
|
}
|
|
379
390
|
Object.assign(item, {
|
|
380
391
|
status: "included",
|
|
392
|
+
retrieval: decision.action,
|
|
393
|
+
policyReason: decision.reason,
|
|
381
394
|
contentHash,
|
|
382
395
|
fileId,
|
|
383
396
|
chunkIds: [],
|
|
@@ -404,7 +417,9 @@ export async function materialize(args: {
|
|
|
404
417
|
? "structural-only"
|
|
405
418
|
: "embedding-disabled",
|
|
406
419
|
};
|
|
407
|
-
const eligible =
|
|
420
|
+
const eligible =
|
|
421
|
+
decision.action === "semantic" &&
|
|
422
|
+
!["imports", "structural"].includes(span.chunkKind);
|
|
408
423
|
if (preset.embedding && eligible) {
|
|
409
424
|
const inputHash = hash([preset.embedding, span.searchText]);
|
|
410
425
|
if (preset.embedding.managed) {
|
|
@@ -443,9 +458,11 @@ export async function materialize(args: {
|
|
|
443
458
|
}
|
|
444
459
|
} else {
|
|
445
460
|
doc.embeddingSkipReason = preset.embedding
|
|
446
|
-
?
|
|
447
|
-
?
|
|
448
|
-
: "
|
|
461
|
+
? decision.action === "lexical"
|
|
462
|
+
? decision.reason
|
|
463
|
+
: span.chunkKind === "imports"
|
|
464
|
+
? "imports-only"
|
|
465
|
+
: "structural-only"
|
|
449
466
|
: "embedding-disabled";
|
|
450
467
|
item.skipped!++;
|
|
451
468
|
}
|
|
@@ -545,7 +562,10 @@ export async function validateBuild(b: Build): Promise<void> {
|
|
|
545
562
|
"Build commit identity mismatch; rebuild from the intended Git commit.",
|
|
546
563
|
);
|
|
547
564
|
invariant(
|
|
548
|
-
hash(b.preset.
|
|
565
|
+
hash(b.preset.filePolicy) ===
|
|
566
|
+
hash(normalizeFilePolicy(b.preset.filePolicy)) &&
|
|
567
|
+
hash(b.preset.analyzers) ===
|
|
568
|
+
hash(normalizeAnalyzers(b.preset.analyzers)) &&
|
|
549
569
|
hash(b.preset) === b.configHash &&
|
|
550
570
|
hash(b.inventory) === b.inventoryHash,
|
|
551
571
|
"Build metadata hash mismatch.",
|
|
@@ -559,6 +579,7 @@ export async function validateBuild(b: Build): Promise<void> {
|
|
|
559
579
|
.filter((e) => e.status === "included")
|
|
560
580
|
.map((e) => [e.fileId!, e]),
|
|
561
581
|
);
|
|
582
|
+
let activeDecision: FileDecision | undefined;
|
|
562
583
|
const completedFiles = new Set<string>();
|
|
563
584
|
const finish = () => {
|
|
564
585
|
if (activeFile) {
|
|
@@ -598,6 +619,19 @@ export async function validateBuild(b: Build): Promise<void> {
|
|
|
598
619
|
end = 0;
|
|
599
620
|
count = 0;
|
|
600
621
|
const entry = expectedFile.get(doc.id);
|
|
622
|
+
activeDecision = fileDecision(
|
|
623
|
+
doc.path as string,
|
|
624
|
+
b.preset.filePolicy,
|
|
625
|
+
doc.sourceText as string,
|
|
626
|
+
);
|
|
627
|
+
invariant(
|
|
628
|
+
!hardExclusion(doc.path as string) &&
|
|
629
|
+
!privateKeyContent(doc.sourceText as string) &&
|
|
630
|
+
activeDecision.action !== "exclude" &&
|
|
631
|
+
entry?.retrieval === activeDecision.action &&
|
|
632
|
+
entry?.policyReason === activeDecision.reason,
|
|
633
|
+
"File policy or inventory decision mismatch.",
|
|
634
|
+
);
|
|
601
635
|
const raw = Buffer.from(doc.sourceText as string);
|
|
602
636
|
invariant(
|
|
603
637
|
entry &&
|
|
@@ -648,9 +682,9 @@ export async function validateBuild(b: Build): Promise<void> {
|
|
|
648
682
|
"Invalid chunk source/position/token binding.",
|
|
649
683
|
);
|
|
650
684
|
if (b.preset.embedding?.managed) {
|
|
651
|
-
const eligible =
|
|
652
|
-
|
|
653
|
-
|
|
685
|
+
const eligible =
|
|
686
|
+
activeDecision!.action === "semantic" &&
|
|
687
|
+
!["imports", "structural"].includes(String(doc.chunkKind));
|
|
654
688
|
invariant(
|
|
655
689
|
eligible
|
|
656
690
|
? doc.embeddingStatus === "managed" &&
|
|
@@ -662,14 +696,18 @@ export async function validateBuild(b: Build): Promise<void> {
|
|
|
662
696
|
doc.embeddingText === undefined &&
|
|
663
697
|
doc.embeddingInputHash === undefined &&
|
|
664
698
|
doc.embeddingSkipReason ===
|
|
665
|
-
(
|
|
666
|
-
?
|
|
667
|
-
: "
|
|
699
|
+
(activeDecision!.action === "lexical"
|
|
700
|
+
? activeDecision!.reason
|
|
701
|
+
: doc.chunkKind === "imports"
|
|
702
|
+
? "imports-only"
|
|
703
|
+
: "structural-only"),
|
|
668
704
|
"Invalid managed embedding input or eligibility.",
|
|
669
705
|
);
|
|
670
706
|
} else if (doc.embeddingStatus === "embedded")
|
|
671
707
|
invariant(
|
|
672
708
|
b.preset.embedding &&
|
|
709
|
+
activeDecision!.action === "semantic" &&
|
|
710
|
+
!["imports", "structural"].includes(String(doc.chunkKind)) &&
|
|
673
711
|
Array.isArray(doc.embedding) &&
|
|
674
712
|
doc.embedding.length === b.preset.embedding.dimensions &&
|
|
675
713
|
doc.embedding.every(Number.isFinite),
|
package/src/chunk.ts
CHANGED
|
@@ -14,12 +14,14 @@ export const CHUNKER = {
|
|
|
14
14
|
grammars: "tree-sitter-wasms@0.1.13",
|
|
15
15
|
sqlGrammar:
|
|
16
16
|
"tree-sitter-wasm@2.0.2/sql/b77530893b1dd6d1d4814eacf34d25bcbe4a12ec9a25a8c45b320d447265f42b",
|
|
17
|
+
shellGrammar:
|
|
18
|
+
"tree-sitter-wasm@2.0.2/bash/5daf3f2ac1cea01a64cfd6878ef7d247310925a4c62238eb4b8c1feb5dce355a",
|
|
17
19
|
tokenizer: "js-tiktoken@1.0.21/cl100k_base",
|
|
18
20
|
targetTokens: 800,
|
|
19
21
|
maxTokens: 1500,
|
|
20
22
|
fallbackOverlap: 0.1,
|
|
21
23
|
enrichment: "path-scope-symbol-v1",
|
|
22
|
-
policy: "source-
|
|
24
|
+
policy: "source-v2",
|
|
23
25
|
} as const;
|
|
24
26
|
export type Span = {
|
|
25
27
|
startByte: number;
|
|
@@ -49,16 +51,14 @@ async function language(name: string): Promise<Language> {
|
|
|
49
51
|
let l = languages.get(name);
|
|
50
52
|
if (!l) {
|
|
51
53
|
l = await Language.load(
|
|
52
|
-
name === "sql"
|
|
54
|
+
name === "sql" || name === "shell"
|
|
53
55
|
? fileURLToPath(
|
|
54
56
|
new URL(
|
|
55
|
-
|
|
57
|
+
`../runtime/grammars/tree-sitter-${name === "shell" ? "bash" : "sql"}.wasm`,
|
|
56
58
|
import.meta.url,
|
|
57
59
|
),
|
|
58
60
|
)
|
|
59
|
-
: require.resolve(
|
|
60
|
-
`tree-sitter-wasms/out/tree-sitter-${name === "shell" ? "bash" : name}.wasm`,
|
|
61
|
-
),
|
|
61
|
+
: require.resolve(`tree-sitter-wasms/out/tree-sitter-${name}.wasm`),
|
|
62
62
|
);
|
|
63
63
|
languages.set(name, l);
|
|
64
64
|
}
|
package/src/cli.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
normalizeAnalyzers,
|
|
18
18
|
ANALYZERS,
|
|
19
19
|
} from "./build.js";
|
|
20
|
+
import { normalizeFilePolicy } from "./file-policy.js";
|
|
20
21
|
import { invariant, optionalJson } from "./common.js";
|
|
21
22
|
import { LambdaRemote } from "./remote.js";
|
|
22
23
|
import {
|
|
@@ -64,6 +65,13 @@ const analyzerOption = (description: string) =>
|
|
|
64
65
|
).argParser((value) =>
|
|
65
66
|
normalizeAnalyzers(value.split(",").map((name) => name.trim())),
|
|
66
67
|
);
|
|
68
|
+
const filePolicyOption = () =>
|
|
69
|
+
new Option(
|
|
70
|
+
"--file-policy <json>",
|
|
71
|
+
"Path to file policy JSON; pinned at registration or local dry run",
|
|
72
|
+
).argParser((path) =>
|
|
73
|
+
normalizeFilePolicy(JSON.parse(readFileSync(path, "utf8"))),
|
|
74
|
+
);
|
|
67
75
|
async function connected() {
|
|
68
76
|
const settings = await loadSettings();
|
|
69
77
|
return { settings, remote: new LambdaRemote(settings) };
|
|
@@ -131,6 +139,7 @@ repo
|
|
|
131
139
|
.option("--description <text>")
|
|
132
140
|
.option("--tag <key=value>", "One optional Collection metadata tag")
|
|
133
141
|
.addOption(analyzerOption("Comma-separated text analyzers"))
|
|
142
|
+
.addOption(filePolicyOption())
|
|
134
143
|
.addOption(
|
|
135
144
|
new Option(
|
|
136
145
|
"--embedding <model>",
|
|
@@ -153,7 +162,7 @@ repo
|
|
|
153
162
|
remote: o.remote,
|
|
154
163
|
description: o.description,
|
|
155
164
|
labels,
|
|
156
|
-
preset: presetFor(o.embedding, o.analyzers),
|
|
165
|
+
preset: presetFor(o.embedding, o.analyzers, o.filePolicy),
|
|
157
166
|
}),
|
|
158
167
|
);
|
|
159
168
|
});
|
|
@@ -181,6 +190,7 @@ cli
|
|
|
181
190
|
.option("--repo <name>", "Registered remote repository")
|
|
182
191
|
.option("--ref <ref>", "Local branch, Git tag, or commit OID")
|
|
183
192
|
.option("--dry-run", "Only materialize and validate a local build")
|
|
193
|
+
.addOption(filePolicyOption())
|
|
184
194
|
.addOption(
|
|
185
195
|
new Option(
|
|
186
196
|
"--embedding <model>",
|
|
@@ -212,6 +222,10 @@ cli
|
|
|
212
222
|
!o.analyzers || (o.dryRun && o.path),
|
|
213
223
|
"--analyzers is only accepted with --dry-run --path; connected imports use the repository preset.",
|
|
214
224
|
);
|
|
225
|
+
invariant(
|
|
226
|
+
!o.filePolicy || (o.dryRun && o.path),
|
|
227
|
+
"--file-policy is only accepted with --dry-run --path; connected imports use the repository preset.",
|
|
228
|
+
);
|
|
215
229
|
invariant(!(o.path && o.repo), "Choose --path or --repo.");
|
|
216
230
|
invariant(
|
|
217
231
|
!o.previous || o.dryRun,
|
|
@@ -261,7 +275,7 @@ cli
|
|
|
261
275
|
await validateBuild(b);
|
|
262
276
|
} else {
|
|
263
277
|
let source;
|
|
264
|
-
let preset = presetFor(o.embedding, o.analyzers);
|
|
278
|
+
let preset = presetFor(o.embedding, o.analyzers, o.filePolicy);
|
|
265
279
|
if (o.path) source = await identity(o.path, o.remote);
|
|
266
280
|
else {
|
|
267
281
|
invariant(o.repo, "Use --path for a credential-free dry run.");
|
|
@@ -289,6 +303,8 @@ cli
|
|
|
289
303
|
path: e.path,
|
|
290
304
|
status: e.status,
|
|
291
305
|
reason: e.reason,
|
|
306
|
+
retrieval: e.retrieval,
|
|
307
|
+
policyReason: e.policyReason,
|
|
292
308
|
parseStatus: e.parseStatus,
|
|
293
309
|
})),
|
|
294
310
|
uploaded: false,
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
/** Explicit rules override defaults, in order; the final matching rule wins. */
|
|
111
|
+
export function fileDecision(
|
|
112
|
+
path: string,
|
|
113
|
+
policy: FilePolicy,
|
|
114
|
+
source?: string,
|
|
115
|
+
): FileDecision {
|
|
116
|
+
let decision: FileDecision = { action: "semantic", reason: "source" };
|
|
117
|
+
if (
|
|
118
|
+
/(^|\/)(node_modules|vendor|vendored|third_party|third-party|dist|build|target|coverage|\.git|\.next|\.nuxt|\.venv|venv|__pycache__|\.cache|\.pytest_cache|\.mypy_cache|\.ruff_cache|\.tox|\.nox|\.srcx)(\/|$)/i.test(
|
|
119
|
+
path,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
decision = { action: "exclude", reason: "dependency-or-build-output" };
|
|
123
|
+
else if (/\.(min\.(js|css)|map)$/i.test(path))
|
|
124
|
+
decision = { action: "exclude", reason: "generated-output" };
|
|
125
|
+
else if (/\.(log|log\.\d+)$/i.test(path))
|
|
126
|
+
decision = { action: "exclude", reason: "log-output" };
|
|
127
|
+
else if (
|
|
128
|
+
/(^|\/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|go\.sum|[^/]+\.lock|packages\.lock\.json|Package\.resolved)$/i.test(
|
|
129
|
+
path,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
decision = { action: "lexical", reason: "lockfile" };
|
|
133
|
+
else if (
|
|
134
|
+
/(^|\/)(licen[sc]e|copying|copyright|notice|authors|contributors)([.-][^/]*)?$/i.test(
|
|
135
|
+
path,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
decision = { action: "lexical", reason: "legal-or-attribution" };
|
|
139
|
+
else if (
|
|
140
|
+
/\.(csv|tsv|jsonl|ndjson|snap)$/i.test(path) ||
|
|
141
|
+
/(^|\/)__snapshots__\//i.test(path)
|
|
142
|
+
)
|
|
143
|
+
decision = { action: "lexical", reason: "data-or-snapshot" };
|
|
144
|
+
else if (
|
|
145
|
+
/\.(pb\.go|generated\.[^/]+|g\.[^/]+)$/i.test(path) ||
|
|
146
|
+
/_pb2(?:_grpc)?\.py$/i.test(path) ||
|
|
147
|
+
(source !== undefined &&
|
|
148
|
+
/(?:^|\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(
|
|
149
|
+
Buffer.from(source, "utf8").subarray(0, 8192).toString("utf8"),
|
|
150
|
+
))
|
|
151
|
+
)
|
|
152
|
+
decision = { action: "lexical", reason: "generated-source" };
|
|
153
|
+
for (const rule of policy.rules)
|
|
154
|
+
if (glob(rule.pattern).test(path))
|
|
155
|
+
decision = { action: rule.action, reason: `file-rule:${rule.action}` };
|
|
156
|
+
return decision;
|
|
157
|
+
}
|