claudeos-core 2.4.4 → 2.5.1
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 +125 -0
- package/README.de.md +12 -10
- package/README.es.md +12 -10
- package/README.fr.md +12 -10
- package/README.hi.md +12 -10
- package/README.ja.md +12 -10
- package/README.ko.md +12 -10
- package/README.md +12 -10
- package/README.ru.md +12 -10
- package/README.vi.md +12 -10
- package/README.zh-CN.md +12 -10
- package/bin/commands/init.js +121 -24
- package/bin/commands/lint.js +2 -0
- package/bin/commands/memory.js +10 -3
- package/content-validator/index.js +82 -13
- package/lib/env-parser.js +98 -12
- package/lib/memory-scaffold.js +35 -16
- package/manifest-generator/index.js +15 -4
- package/package.json +92 -92
- package/pass-json-validator/index.js +1 -1
- package/pass-prompts/templates/angular/pass3.md +2 -1
- package/pass-prompts/templates/common/claude-md-scaffold.md +1 -1
- package/pass-prompts/templates/common/pass3a-facts.md +11 -9
- package/pass-prompts/templates/common/pass4.md +3 -3
- package/pass-prompts/templates/java-spring/pass1.md +10 -2
- package/pass-prompts/templates/java-spring/pass3.md +5 -4
- package/pass-prompts/templates/kotlin-spring/pass3.md +2 -2
- package/pass-prompts/templates/node-express/pass3.md +1 -1
- package/pass-prompts/templates/node-fastify/pass3.md +1 -0
- package/pass-prompts/templates/node-nestjs/pass3.md +1 -0
- package/pass-prompts/templates/node-nextjs/pass3.md +1 -1
- package/pass-prompts/templates/node-vite/pass3.md +1 -0
- package/pass-prompts/templates/python-django/pass3.md +1 -1
- package/pass-prompts/templates/python-fastapi/pass3.md +1 -1
- package/pass-prompts/templates/python-flask/pass3.md +1 -0
- package/pass-prompts/templates/vue-nuxt/pass3.md +1 -0
- package/plan-installer/domain-grouper.js +4 -1
- package/plan-installer/index.js +26 -7
- package/plan-installer/jvm-detect.js +562 -0
- package/plan-installer/pass3-context-builder.js +10 -0
- package/plan-installer/prompt-generator.js +18 -2
- package/plan-installer/scanners/scan-frontend.js +67 -6
- package/plan-installer/scanners/scan-java.js +214 -15
- package/plan-installer/scanners/scan-kotlin.js +68 -3
- package/plan-installer/scanners/scan-node.js +115 -0
- package/plan-installer/scanners/scan-python.js +56 -0
- package/plan-installer/source-paths.js +61 -0
- package/plan-installer/stack-detector.js +726 -51
- package/plan-installer/structure-scanner.js +15 -4
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
const path = require("path");
|
|
9
9
|
const { glob } = require("glob");
|
|
10
10
|
|
|
11
|
+
// v2.5.0 — Layer-first stem de-duplication (shared shape with scan-python.js).
|
|
12
|
+
// For every plural key whose singular form is ALSO a key, fold the plural
|
|
13
|
+
// into the singular via `merge(into, from)` and drop the plural. Handles
|
|
14
|
+
// `-ies`→`-y`, `-(s|x|z|ch|sh)es`→base, and plain `-s`. No merge happens
|
|
15
|
+
// when only one form exists — the scanner never invents a singular.
|
|
16
|
+
function mergePluralStems(byDomain, merge) {
|
|
17
|
+
for (const name of Object.keys(byDomain)) {
|
|
18
|
+
const cands = [];
|
|
19
|
+
if (/ies$/.test(name)) cands.push(name.slice(0, -3) + "y");
|
|
20
|
+
if (/(?:s|x|z|ch|sh)es$/.test(name)) cands.push(name.slice(0, -2));
|
|
21
|
+
if (/[^s]s$/.test(name)) cands.push(name.slice(0, -1));
|
|
22
|
+
const singular = cands.find(c => c !== name && byDomain[c]);
|
|
23
|
+
if (!singular) continue;
|
|
24
|
+
merge(byDomain[singular], byDomain[name]);
|
|
25
|
+
delete byDomain[name];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
async function scanNodeDomains(stack, ROOT) {
|
|
12
30
|
const backendDomains = [];
|
|
13
31
|
const skipDirs = ["common", "shared", "config", "utils", "lib", "core", "main", "interfaces", "types", "constants", "guards", "decorators", "pipes", "filters", "interceptors"];
|
|
@@ -27,7 +45,104 @@ async function scanNodeDomains(stack, ROOT) {
|
|
|
27
45
|
}
|
|
28
46
|
}
|
|
29
47
|
|
|
48
|
+
// v2.5.0 — Layer-first layouts (Express / Fastify / Koa): src/controllers/,
|
|
49
|
+
// src/routes/, src/services/, src/models/. Every folder is a LAYER, so the
|
|
50
|
+
// loop below used to emit "controllers", "services", "routes" as domains.
|
|
51
|
+
// When (almost) every candidate folder is a layer name, derive domains from
|
|
52
|
+
// the file stems inside those layers instead: user.controller.js,
|
|
53
|
+
// users.routes.ts, orderService.js → user, users, order.
|
|
54
|
+
const LAYER_DIRS = new Set(["controllers", "controller", "routes", "route", "routers", "router", "services", "service",
|
|
55
|
+
"models", "model", "repositories", "repository", "middlewares", "middleware", "handlers", "handler",
|
|
56
|
+
"validators", "validations", "schemas", "dtos", "entities", "dao", "helpers", "api"]);
|
|
57
|
+
// Infrastructure folders that are neither layers nor features.
|
|
58
|
+
const NODE_GENERIC_DIRS = new Set(["db", "database", "migrations", "seeds", "scripts", "public", "static", "views", "templates",
|
|
59
|
+
"assets", "test", "tests", "__tests__", "mocks", "__mocks__", "docs", "node_modules", "dist", "build"]);
|
|
60
|
+
const candidateNames = srcDirs.map(d => path.basename(d.replace(/\/$/, ""))).filter(n => !skipDirs.includes(n) && !NODE_GENERIC_DIRS.has(n));
|
|
61
|
+
const layerNames = candidateNames.filter(n => LAYER_DIRS.has(n));
|
|
62
|
+
// A tree is layer-first only when a ROUTING layer folder exists at the
|
|
63
|
+
// top (`controllers/`, `routes/`, `handlers/`, `api/`). Data-only layer
|
|
64
|
+
// folders (`entities/`, `dtos/`, `schemas/`, `models/`) also appear in
|
|
65
|
+
// module-first NestJS trees (`src/users/`, `src/orders/` + shared
|
|
66
|
+
// `src/entities/`), where the modules are the domains — treating that as
|
|
67
|
+
// layer-first would rename `users` to `user` and invent one-file domains.
|
|
68
|
+
const ROUTING_LAYERS = new Set(["controllers", "controller", "routes", "route", "routers", "router", "handlers", "handler", "api"]);
|
|
69
|
+
const hasRoutingLayer = layerNames.some(n => ROUTING_LAYERS.has(n));
|
|
70
|
+
// Then: at least two layer folders (or the only candidate is a layer).
|
|
71
|
+
// Remaining non-layer siblings (`src/jobs/`, `src/billing/`) are MIXED-
|
|
72
|
+
// layout feature folders and become whole-folder domains — they must not
|
|
73
|
+
// disable the layer-first path, which would resurrect `controllers` /
|
|
74
|
+
// `routes` / `services` as domains.
|
|
75
|
+
if (hasRoutingLayer && (layerNames.length >= 2 || (layerNames.length === 1 && candidateNames.length === 1))) {
|
|
76
|
+
const byDomain = {};
|
|
77
|
+
const featureNames = candidateNames.filter(n => !LAYER_DIRS.has(n));
|
|
78
|
+
for (const dir of srcDirs) {
|
|
79
|
+
const name = path.basename(dir.replace(/\/$/, ""));
|
|
80
|
+
if (!featureNames.includes(name)) continue;
|
|
81
|
+
const files = await glob(`${dir.replace(/\\/g, "/").replace(/\/?$/, "/")}**/*.{ts,js,mjs,cjs}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.d.ts", "**/node_modules/**"] });
|
|
82
|
+
if (files.length === 0) continue;
|
|
83
|
+
const e = (byDomain[name] = byDomain[name] || { name, type: "backend", controllers: 0, services: 0, dtos: 0, totalFiles: 0 });
|
|
84
|
+
for (const f of files) {
|
|
85
|
+
if (/controller|router|route|handler/.test(f)) e.controllers++;
|
|
86
|
+
if (/service/.test(f)) e.services++;
|
|
87
|
+
if (/dto|schema|type|validator/.test(f)) e.dtos++;
|
|
88
|
+
e.totalFiles++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Layer/role suffixes are stripped REPEATEDLY (`email.service.impl.ts` →
|
|
92
|
+
// `email`, not `emailimpl`); `impl`, `base`, `abstract`, `interface`,
|
|
93
|
+
// `types`, `spec` are role words, not domains.
|
|
94
|
+
// A suffix counts only after a separator (`user.controller`, `user-controller`)
|
|
95
|
+
// or as a PascalCase word (`userController`, `UserService`) — never as a bare
|
|
96
|
+
// substring, so `prototype` is not cut down to `proto`.
|
|
97
|
+
const DOTTED_SUFFIX_RE = /[.\-_](?:controller|router|routes?|service|handler|model|repository|repo|middleware|validator|schema|dto|entity|dao|helper|impl|base|abstract|interface|types?|spec|mock|factory|utils?)s?$/i;
|
|
98
|
+
const CAMEL_SUFFIX_RE = /(?<=[a-z0-9])(?:Controller|Router|Routes?|Service|Handler|Model|Repository|Repo|Middleware|Validator|Schema|Dto|DTO|Entity|Dao|DAO|Helper|Impl|Base|Abstract|Interface|Types?|Spec|Mock|Factory|Utils?)s?$/;
|
|
99
|
+
const stemOf = (f) => {
|
|
100
|
+
const base = path.basename(f).replace(/\.(ts|js|mjs|cjs)$/, "");
|
|
101
|
+
// user.controller | user-controller | userController | users.routes | UserService | index
|
|
102
|
+
let stem = base;
|
|
103
|
+
for (let i = 0; i < 6; i++) {
|
|
104
|
+
const next = stem.replace(DOTTED_SUFFIX_RE, "").replace(CAMEL_SUFFIX_RE, "");
|
|
105
|
+
if (next === stem) break;
|
|
106
|
+
stem = next;
|
|
107
|
+
}
|
|
108
|
+
stem = stem.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/^[.\-_]+|[.\-_]+$/g, "");
|
|
109
|
+
return stem && stem !== "index" && stem !== "app" && stem !== "main" ? stem : null;
|
|
110
|
+
};
|
|
111
|
+
// Role comes from the LAYER FOLDER (authoritative), never from substrings
|
|
112
|
+
// of the file path (`models/prototype.ts` is not a dto because it contains
|
|
113
|
+
// "type").
|
|
114
|
+
const ROLE_OF_LAYER = (layer) => ROUTING_LAYERS.has(layer) ? "controllers"
|
|
115
|
+
: /^services?$/.test(layer) ? "services"
|
|
116
|
+
: /^(dtos?|schemas?|validators?|validations|entities)$/.test(layer) ? "dtos" : null;
|
|
117
|
+
for (const dir of srcDirs) {
|
|
118
|
+
const layer = path.basename(dir.replace(/\/$/, ""));
|
|
119
|
+
if (!LAYER_DIRS.has(layer)) continue;
|
|
120
|
+
const role = ROLE_OF_LAYER(layer);
|
|
121
|
+
const files = await glob(`${dir.replace(/\\/g, "/").replace(/\/?$/, "/")}**/*.{ts,js,mjs,cjs}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.d.ts"] });
|
|
122
|
+
for (const f of files) {
|
|
123
|
+
const d = stemOf(f);
|
|
124
|
+
if (!d) continue;
|
|
125
|
+
const e = (byDomain[d] = byDomain[d] || { name: d, type: "backend", controllers: 0, services: 0, dtos: 0, totalFiles: 0 });
|
|
126
|
+
if (role) e[role]++;
|
|
127
|
+
e.totalFiles++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Merge a plural stem into its singular twin when BOTH exist
|
|
131
|
+
// (`users.routes.js` + `user.controller.js` → `user`). Only pairs are
|
|
132
|
+
// merged — a lone `orders` stays `orders`, so no guessed singularization.
|
|
133
|
+
mergePluralStems(byDomain, (into, from) => {
|
|
134
|
+
into.controllers += from.controllers; into.services += from.services;
|
|
135
|
+
into.dtos += from.dtos; into.totalFiles += from.totalFiles;
|
|
136
|
+
});
|
|
137
|
+
for (const d of Object.values(byDomain)) backendDomains.push({ ...d, pattern: "layer-first" });
|
|
138
|
+
if (backendDomains.length > 0) return { backendDomains };
|
|
139
|
+
// fall through to the directory loop if stems yielded nothing
|
|
140
|
+
}
|
|
141
|
+
|
|
30
142
|
for (let dir of srcDirs) {
|
|
143
|
+
// A folder named after a layer (`entities/`, `dtos/`, `controllers/`) is
|
|
144
|
+
// never a feature domain, whichever layout won above.
|
|
145
|
+
if (LAYER_DIRS.has(path.basename(dir.replace(/\/$/, "")))) continue;
|
|
31
146
|
if (!dir.endsWith("/")) dir += "/";
|
|
32
147
|
const name = path.basename(dir.replace(/\/$/, ""));
|
|
33
148
|
if (skipDirs.includes(name)) continue;
|
|
@@ -7,6 +7,22 @@
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const { glob } = require("glob");
|
|
9
9
|
|
|
10
|
+
// v2.5.0 — Layer-first stem de-duplication (same rule as scan-node.js).
|
|
11
|
+
// Fold a plural key into its singular twin ONLY when both exist
|
|
12
|
+
// (`routers/users.py` + `models/user.py` → `user`); a lone plural is kept.
|
|
13
|
+
function mergePluralStems(byDomain, merge) {
|
|
14
|
+
for (const name of Object.keys(byDomain)) {
|
|
15
|
+
const cands = [];
|
|
16
|
+
if (/ies$/.test(name)) cands.push(name.slice(0, -3) + "y");
|
|
17
|
+
if (/(?:s|x|z|ch|sh)es$/.test(name)) cands.push(name.slice(0, -2));
|
|
18
|
+
if (/[^s]s$/.test(name)) cands.push(name.slice(0, -1));
|
|
19
|
+
const singular = cands.find(c => c !== name && byDomain[c]);
|
|
20
|
+
if (!singular) continue;
|
|
21
|
+
merge(byDomain[singular], byDomain[name]);
|
|
22
|
+
delete byDomain[name];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
async function scanPythonDomains(stack, ROOT) {
|
|
11
27
|
const backendDomains = [];
|
|
12
28
|
|
|
@@ -46,6 +62,46 @@ async function scanPythonDomains(stack, ROOT) {
|
|
|
46
62
|
const appFiles = await glob(`${dir.replace(/\\/g, "/")}/*.py`, { cwd: ROOT });
|
|
47
63
|
backendDomains.push({ name, type: "backend", totalFiles: appFiles.length });
|
|
48
64
|
}
|
|
65
|
+
// v2.5.0 — Layer-first FastAPI/Flask: app/routers/users.py, app/models/user.py,
|
|
66
|
+
// app/schemas/user.py. Folders are layers, so derive domains from file stems.
|
|
67
|
+
if (backendDomains.filter(d => d.type === "backend").length === 0) {
|
|
68
|
+
const PY_LAYER_DIRS = new Set(["routers", "router", "routes", "route", "api", "endpoints", "views", "controllers",
|
|
69
|
+
"services", "service", "models", "model", "schemas", "schema", "repositories", "repository", "crud", "dao", "handlers"]);
|
|
70
|
+
const PY_GENERIC_DIRS = new Set(["core", "common", "utils", "__pycache__", "config", "db", "database", "tests", "test", "app", "static", "templates", "migrations",
|
|
71
|
+
"env", "venv", ".venv", "virtualenv", "node_modules", "site-packages", "scripts", "docs"]);
|
|
72
|
+
const allSub = (await glob("{app,src/app,src}/*/", { cwd: ROOT, ignore: ["**/venv/**", "**/.venv/**", "**/env/**", "**/virtualenv/**", "**/node_modules/**", "**/site-packages/**", "**/__pycache__/**"] }))
|
|
73
|
+
.map(d => d.replace(/\\/g, "/").replace(/\/?$/, "/"));
|
|
74
|
+
const baseOf = (d) => path.basename(d.replace(/\/$/, ""));
|
|
75
|
+
const parentOf = (d) => d.replace(/\/$/, "").split("/").slice(0, -1).join("/") + "/";
|
|
76
|
+
const layerDirs = allSub.filter(d => PY_LAYER_DIRS.has(baseOf(d)));
|
|
77
|
+
// Feature packages that sit NEXT TO the layer folders (same parent):
|
|
78
|
+
// `src/routers/` + `src/tasks/` is a mixed layout — `tasks` must become a
|
|
79
|
+
// domain too, not be silently dropped. Scanned symmetrically with the
|
|
80
|
+
// layer folders (both come from the same glob), never from other trees.
|
|
81
|
+
const layerParents = new Set(layerDirs.map(parentOf));
|
|
82
|
+
const featureDirs = allSub.filter(d => layerParents.has(parentOf(d)) && !PY_LAYER_DIRS.has(baseOf(d)) && !PY_GENERIC_DIRS.has(baseOf(d)));
|
|
83
|
+
if (layerDirs.length > 0) {
|
|
84
|
+
const byDomain = {};
|
|
85
|
+
for (const dir of layerDirs) {
|
|
86
|
+
const files = await glob(`${dir}*.py`, { cwd: ROOT });
|
|
87
|
+
for (const f of files) {
|
|
88
|
+
let stem = path.basename(f, ".py").replace(/_(router|routes?|service|model|schema|repository|crud|handler|views?|api)s?$/i, "").toLowerCase();
|
|
89
|
+
if (!stem || ["__init__", "base", "deps", "dependencies", "main", "app", "utils", "common"].includes(stem)) continue;
|
|
90
|
+
const e = (byDomain[stem] = byDomain[stem] || { name: stem, type: "backend", totalFiles: 0 });
|
|
91
|
+
e.totalFiles++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const dir of featureDirs) {
|
|
95
|
+
const files = await glob(`${dir}**/*.py`, { cwd: ROOT, ignore: ["**/__pycache__/**"] });
|
|
96
|
+
if (files.length === 0) continue;
|
|
97
|
+
const name = baseOf(dir).toLowerCase();
|
|
98
|
+
const e = (byDomain[name] = byDomain[name] || { name, type: "backend", totalFiles: 0 });
|
|
99
|
+
e.totalFiles += files.length;
|
|
100
|
+
}
|
|
101
|
+
mergePluralStems(byDomain, (into, from) => { into.totalFiles += from.totalFiles; });
|
|
102
|
+
for (const d of Object.values(byDomain)) backendDomains.push({ ...d, pattern: "layer-first" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
49
105
|
if (backendDomains.filter(d => d.type === "backend").length === 0) {
|
|
50
106
|
const appDirs = await glob("{app,src/app}/*/", { cwd: ROOT });
|
|
51
107
|
for (let dir of appDirs) {
|
|
@@ -234,9 +234,70 @@ function renderAllowedPathsSection(collected) {
|
|
|
234
234
|
return lines.join("\n");
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Deterministically insert (or replace) the `## Allowed Source Paths` section
|
|
239
|
+
* in a pass3a-facts.md body. Called by the orchestrator right after Pass 3a
|
|
240
|
+
* finishes, so the allowlist reaches Pass 3b/3c/3d verbatim — without asking
|
|
241
|
+
* the LLM to hand-copy up to 500 paths out of pass3-context.json.
|
|
242
|
+
*
|
|
243
|
+
* - If a `## Allowed Source Paths` section already exists (LLM wrote one),
|
|
244
|
+
* it is replaced wholesale up to the next `## ` heading / EOF.
|
|
245
|
+
* - Otherwise the section is appended.
|
|
246
|
+
* - `collected` is the `allowedSourcePaths` object from project-analysis.json.
|
|
247
|
+
* A missing/empty allowlist yields the documented fallback line so
|
|
248
|
+
* downstream prompts see an explicit "unavailable" marker, not silence.
|
|
249
|
+
*
|
|
250
|
+
* Returns the new markdown string.
|
|
251
|
+
*/
|
|
252
|
+
function injectAllowedPathsSection(factsMd, collected) {
|
|
253
|
+
const heading = "## Allowed Source Paths";
|
|
254
|
+
const hasPaths = collected && Array.isArray(collected.paths) && collected.paths.length > 0;
|
|
255
|
+
const body = hasPaths
|
|
256
|
+
? renderAllowedPathsSection(collected)
|
|
257
|
+
: "(allowlist unavailable — fall back to pass2-merged.json verification per file)";
|
|
258
|
+
const section = `${heading}\n\n${body}\n`;
|
|
259
|
+
|
|
260
|
+
const src = typeof factsMd === "string" ? factsMd : "";
|
|
261
|
+
const lines = src.split(/\r?\n/);
|
|
262
|
+
// Locate EVERY existing section heading outside fenced blocks. A model on a
|
|
263
|
+
// retry/resume may have written the section twice; all copies are removed
|
|
264
|
+
// and exactly one is put back at the position of the first.
|
|
265
|
+
const ranges = []; // [start, end) line indices
|
|
266
|
+
let inFence = false, cur = -1;
|
|
267
|
+
for (let i = 0; i < lines.length; i++) {
|
|
268
|
+
if (/^(```|~~~)/.test(lines[i].trimStart())) inFence = !inFence;
|
|
269
|
+
if (inFence) continue;
|
|
270
|
+
const isTarget = /^##\s+Allowed Source Paths\b/i.test(lines[i]);
|
|
271
|
+
const isHeading = /^##\s+/.test(lines[i]);
|
|
272
|
+
if (cur >= 0 && isHeading) { ranges.push([cur, i]); cur = -1; }
|
|
273
|
+
if (cur < 0 && isTarget) cur = i;
|
|
274
|
+
}
|
|
275
|
+
if (cur >= 0) ranges.push([cur, lines.length]);
|
|
276
|
+
if (ranges.length > 0) {
|
|
277
|
+
const out = [];
|
|
278
|
+
let pos = 0;
|
|
279
|
+
ranges.forEach(([a, b], idx) => {
|
|
280
|
+
out.push(...lines.slice(pos, a));
|
|
281
|
+
if (idx === 0) out.push(...section.split("\n"));
|
|
282
|
+
pos = b;
|
|
283
|
+
});
|
|
284
|
+
out.push(...lines.slice(pos));
|
|
285
|
+
return out.join("\n");
|
|
286
|
+
}
|
|
287
|
+
// Rebuild from `lines` (not `src`) so a CRLF input comes out as consistent LF
|
|
288
|
+
// instead of a mixed-ending file (CRLF body + LF appended section).
|
|
289
|
+
let trimmed = lines.join("\n").replace(/\s+$/, "");
|
|
290
|
+
// If the file ends inside an unclosed fence (LLM forgot the closing ```),
|
|
291
|
+
// close it first — otherwise the appended heading would sit inside a code
|
|
292
|
+
// block and be invisible to fence-aware readers of this file.
|
|
293
|
+
if (inFence) trimmed += "\n```";
|
|
294
|
+
return (trimmed ? trimmed + "\n\n" : "") + section;
|
|
295
|
+
}
|
|
296
|
+
|
|
237
297
|
module.exports = {
|
|
238
298
|
collectSourcePaths,
|
|
239
299
|
renderAllowedPathsSection,
|
|
300
|
+
injectAllowedPathsSection,
|
|
240
301
|
// Exported for test visibility.
|
|
241
302
|
_constants: { SOURCE_EXTENSIONS, EXCLUDED_DIRS, MAX_PATHS, MIN_FILES_PER_DIR, MAX_DIRS },
|
|
242
303
|
};
|