javi-forge 1.25.1 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/ci-local/hooks/commit-msg +7 -0
  2. package/ci-local/hooks/pre-commit +8 -0
  3. package/ci-local/hooks/pre-push +8 -0
  4. package/dist/cli/dispatch/ci.js +1 -1
  5. package/dist/cli/dispatch/skills-cmd.js +8 -0
  6. package/dist/commands/ci.js +19 -5
  7. package/dist/commands/doctor.d.ts +8 -0
  8. package/dist/commands/doctor.js +121 -0
  9. package/dist/commands/hooks/sections/deps.d.ts +28 -0
  10. package/dist/commands/hooks/sections/deps.js +126 -0
  11. package/dist/commands/hooks/sections/permissions.d.ts +33 -0
  12. package/dist/commands/hooks/sections/permissions.js +101 -0
  13. package/dist/commands/hooks/sections/secrets.d.ts +58 -0
  14. package/dist/commands/hooks/sections/secrets.js +182 -0
  15. package/dist/commands/hooks.d.ts +4 -4
  16. package/dist/commands/hooks.js +13 -4
  17. package/dist/commands/init/steps/ghagga.d.ts +3 -4
  18. package/dist/commands/init/steps/ghagga.js +5 -15
  19. package/dist/commands/init/steps/security.d.ts +10 -20
  20. package/dist/commands/init/steps/security.js +58 -78
  21. package/dist/commands/init.js +1 -2
  22. package/dist/commands/skills/analysis.js +31 -2
  23. package/dist/commands/skills/benchmark.js +10 -0
  24. package/dist/commands/skills/constants.d.ts +5 -0
  25. package/dist/commands/skills/constants.js +5 -0
  26. package/dist/commands/skills/parsing.d.ts +18 -3
  27. package/dist/commands/skills/parsing.js +29 -3
  28. package/dist/commands/skills/scoring.d.ts +7 -6
  29. package/dist/commands/skills/scoring.js +31 -1
  30. package/dist/constants.d.ts +6 -1
  31. package/dist/constants.js +12 -7
  32. package/dist/lib/context.d.ts +22 -0
  33. package/dist/lib/context.js +120 -79
  34. package/dist/lib/safe-read.d.ts +62 -0
  35. package/dist/lib/safe-read.js +221 -0
  36. package/dist/lib/security-analysis.d.ts +19 -2
  37. package/dist/lib/security-analysis.js +65 -13
  38. package/dist/lib/skill-scanner.d.ts +22 -1
  39. package/dist/lib/skill-scanner.js +76 -4
  40. package/dist/types/index.d.ts +18 -0
  41. package/dist/ui/Skills.js +12 -7
  42. package/package.json +1 -1
  43. package/templates/github/ghagga-review.yml +0 -30
  44. package/templates/security-hooks/commit-msg-signing +0 -29
  45. package/templates/security-hooks/pre-commit-permissions +0 -74
  46. package/templates/security-hooks/pre-commit-secrets +0 -74
  47. package/templates/security-hooks/pre-push-branch-protection +0 -62
  48. package/templates/security-hooks/pre-push-deps +0 -83
  49. package/templates/security-hooks/pre-push-signing +0 -67
@@ -1,7 +1,8 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import { parseFrontmatter } from "../../lib/frontmatter.js";
4
- import { CHARS_PER_TOKEN } from "./constants.js";
4
+ import { describeSafeReadFailure, safeReadFile, } from "../../lib/safe-read.js";
5
+ import { CHARS_PER_TOKEN, MAX_SKILL_BYTES } from "./constants.js";
5
6
  // ── Helpers ──────────────────────────────────────────────────────────────────
6
7
  /** Estimate token count from a string */
7
8
  export function estimateTokens(text) {
@@ -11,7 +12,24 @@ export function estimateTokens(text) {
11
12
  export async function parseSkillFile(skillPath) {
12
13
  if (!(await fs.pathExists(skillPath)))
13
14
  return null;
14
- const raw = await fs.readFile(skillPath, "utf-8");
15
+ const read = await safeReadFile(skillPath, {
16
+ hardRejectOverBytes: MAX_SKILL_BYTES,
17
+ });
18
+ if (!read.ok) {
19
+ // A path that vanished between the existence check and the read is the
20
+ // same "no such skill" case the caller already handles with null.
21
+ if (read.reason === "not-found" || read.reason === "not-a-file")
22
+ return null;
23
+ return {
24
+ name: path.basename(path.dirname(skillPath)),
25
+ rules: [],
26
+ rawContent: "",
27
+ triggers: [],
28
+ skip: { reason: read.reason, message: describeSafeReadFailure(read) },
29
+ truncated: false,
30
+ };
31
+ }
32
+ const raw = read.content;
15
33
  const fm = parseFrontmatter(raw);
16
34
  const rawName = fm?.data?.name;
17
35
  const name = typeof rawName === "string"
@@ -23,7 +41,15 @@ export async function parseSkillFile(skillPath) {
23
41
  const rawDesc = fm?.data?.description;
24
42
  const description = typeof rawDesc === "string" ? rawDesc : "";
25
43
  const triggers = extractTriggers(description);
26
- return { name, rules, rawContent: raw, triggers };
44
+ // rawContent is the kept text, so token estimates reflect what was analysed.
45
+ return {
46
+ name,
47
+ rules,
48
+ rawContent: raw,
49
+ triggers,
50
+ skip: null,
51
+ truncated: read.truncated,
52
+ };
27
53
  }
28
54
  /** Extract critical rules from markdown content */
29
55
  export function extractCriticalRules(content) {
@@ -1,10 +1,11 @@
1
1
  import type { SkillGrade, SkillRegistryGateResult, SkillScore } from "../../types/index.js";
2
- type ParsedSkill = {
3
- name: string;
4
- rules: string[];
5
- rawContent: string;
6
- triggers: string[];
7
- };
2
+ import { type ParsedSkillFile } from "./parsing.js";
3
+ /**
4
+ * The fields the dimension scorers read. Derived from `ParsedSkillFile` with
5
+ * `Pick` (not a hand-copied shape) so the `skip` field can never be silently
6
+ * dropped: a skipped parse is caught in `scoreSkill` before it reaches a scorer.
7
+ */
8
+ type ParsedSkill = Pick<ParsedSkillFile, "name" | "rules" | "rawContent" | "triggers">;
8
9
  /**
9
10
  * Score completeness (0-100): frontmatter fields, critical rules, structure.
10
11
  */
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_REGISTRY_THRESHOLD, DEFAULT_THRESHOLD } from "./constants.js";
2
- import { estimateTokens, parseSkillFile } from "./parsing.js";
2
+ import { estimateTokens, parseSkillFile, } from "./parsing.js";
3
3
  import { ACTION_VERBS, VAGUE_TERMS } from "./rules.js";
4
4
  // ── Dangerous content patterns (private to scoreSafety) ─────────────────────
5
5
  /** Dangerous patterns in skill content that indicate safety risks */
@@ -243,6 +243,26 @@ export async function scoreSkill(skillPath, threshold = DEFAULT_THRESHOLD) {
243
243
  const parsed = await parseSkillFile(skillPath);
244
244
  if (!parsed)
245
245
  return null;
246
+ // A file that could not be read is NOT a zero-content skill. Scoring it would
247
+ // invent a grade over an empty string — worse, `scoreSafety` starts at 100
248
+ // and finds no dangerous patterns in nothing, so an unreadable file would
249
+ // report perfect safety. Represent it as unread and fail closed instead.
250
+ if (parsed.skip) {
251
+ return {
252
+ skillName: parsed.name,
253
+ completeness: 0,
254
+ clarity: 0,
255
+ testability: 0,
256
+ tokenEfficiency: 0,
257
+ safety: 0,
258
+ agentReadiness: 0,
259
+ overall: 0,
260
+ grade: "F",
261
+ threshold,
262
+ passing: false,
263
+ unread: parsed.skip.message,
264
+ };
265
+ }
246
266
  const completeness = scoreCompleteness(parsed);
247
267
  const clarity = scoreClarity(parsed);
248
268
  const testability = scoreTestability(parsed);
@@ -279,6 +299,16 @@ export async function registryGate(skillPath, threshold = DEFAULT_REGISTRY_THRES
279
299
  const score = await scoreSkill(skillPath, threshold);
280
300
  if (!score)
281
301
  return null;
302
+ // An unread skill can never be accepted into the registry: it was not scored,
303
+ // so it cannot clear any threshold. Reject it with an explicit reason.
304
+ if (score.unread) {
305
+ return {
306
+ skillName: score.skillName,
307
+ score,
308
+ accepted: false,
309
+ reason: `Rejected: could not read skill (${score.unread}) — not scored`,
310
+ };
311
+ }
282
312
  const accepted = score.passing;
283
313
  let reason;
284
314
  if (!accepted) {
@@ -37,7 +37,12 @@ export declare const STACK_CONTEXT_MAP: Record<string, StackContextEntry>;
37
37
  export declare const DEPLOY_TEMPLATE_MAP: Record<string, string>;
38
38
  /** Deploy destination path mapping (per CI provider) */
39
39
  export declare const DEPLOY_DESTINATION_MAP: Record<string, string>;
40
- /** Hook reliability profile definitions */
40
+ /**
41
+ * Hook security-preset definitions (hook-consolidation S4). The selected
42
+ * profile drives WHICH `hooks:` security sections get merged into
43
+ * `.javi-forge/ci.yaml` (see src/commands/init/steps/security.ts PROFILE_PRESET).
44
+ * `hooks` lists the enabled dispatcher sections for display only.
45
+ */
41
46
  export declare const HOOK_PROFILES: Record<HookProfile, {
42
47
  label: string;
43
48
  description: string;
package/dist/constants.js CHANGED
@@ -186,22 +186,27 @@ export const DEPLOY_DESTINATION_MAP = {
186
186
  gitlab: ".gitlab-ci-deploy.yml",
187
187
  woodpecker: ".woodpecker/deploy.yml",
188
188
  };
189
- /** Hook reliability profile definitions */
189
+ /**
190
+ * Hook security-preset definitions (hook-consolidation S4). The selected
191
+ * profile drives WHICH `hooks:` security sections get merged into
192
+ * `.javi-forge/ci.yaml` (see src/commands/init/steps/security.ts PROFILE_PRESET).
193
+ * `hooks` lists the enabled dispatcher sections for display only.
194
+ */
190
195
  export const HOOK_PROFILES = {
191
196
  minimal: {
192
197
  label: "Minimal",
193
- description: "pre-commit only: lint + format check",
194
- hooks: ["pre-commit"],
198
+ description: "CI gate only — no security scans",
199
+ hooks: ["ci"],
195
200
  },
196
201
  standard: {
197
202
  label: "Standard",
198
- description: "pre-commit + pre-push + CI gate check",
199
- hooks: ["pre-commit", "pre-push", "ci-gate"],
203
+ description: "secret scan + dependency audit",
204
+ hooks: ["secrets", "deps", "ci"],
200
205
  },
201
206
  strict: {
202
207
  label: "Strict",
203
- description: "all standard + commit-msg validation + security scan on every push",
204
- hooks: ["pre-commit", "pre-push", "ci-gate", "commit-msg", "security-scan"],
208
+ description: "secret scan + permission checks + dependency audit",
209
+ hooks: ["secrets", "permissions", "deps", "ci"],
205
210
  },
206
211
  };
207
212
  /** Stack-to-CI template filename mapping */
@@ -1,9 +1,28 @@
1
1
  import type { InitOptions, StackContextEntry } from "../types/index.js";
2
2
  export declare function buildIndexMd(projectName: string, stackCtx: StackContextEntry, ciProvider: string, memory: string): string;
3
3
  export declare function buildSummaryMd(projectName: string, stack: string, ciProvider: string, memory: string, modules: string[], dependencies?: string[]): string;
4
+ /**
5
+ * Dependency detection outcome. `warnings` explains every manifest that could
6
+ * not be read or parsed, so a failure is distinguishable from "no dependencies"
7
+ * instead of being swallowed by a blanket catch.
8
+ */
9
+ export interface DependencyDetection {
10
+ dependencies: string[];
11
+ warnings: string[];
12
+ }
13
+ /**
14
+ * Detect top-level dependencies from project manifest files, reporting why a
15
+ * manifest was skipped. Returns up to 10 dependency names (key deps only).
16
+ */
17
+ export declare function detectDependenciesDetailed(projectDir: string, stack: string): Promise<DependencyDetection>;
4
18
  /**
5
19
  * Detect top-level dependencies from project manifest files.
6
20
  * Returns up to 10 dependency names (key deps only, not devDeps).
21
+ *
22
+ * List-returning convenience over `detectDependenciesDetailed`. It DISCARDS the
23
+ * read/parse warnings — an unreadable manifest is indistinguishable from an
24
+ * honest empty list through this function. Callers that must tell those two
25
+ * apart (like `refreshContextDir`) call `detectDependenciesDetailed` directly.
7
26
  */
8
27
  export declare function detectDependencies(projectDir: string, stack: string): Promise<string[]>;
9
28
  /**
@@ -23,5 +42,8 @@ export declare function refreshContextDir(projectDir: string): Promise<{
23
42
  index: string;
24
43
  summary: string;
25
44
  updated: boolean;
45
+ /** Manifest read/parse warnings — surfaced so an unreadable manifest is not
46
+ * silently reported as a project with no dependencies. */
47
+ warnings: string[];
26
48
  } | null>;
27
49
  //# sourceMappingURL=context.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import { STACK_CONTEXT_MAP } from "../constants.js";
4
+ import { describeSafeReadFailure, safeReadFile } from "./safe-read.js";
4
5
  // =============================================================================
5
6
  // Internal helpers
6
7
  // =============================================================================
@@ -53,95 +54,135 @@ ${stack}-based project scaffolded with javi-forge.
53
54
  // =============================================================================
54
55
  // Dependency detection
55
56
  // =============================================================================
57
+ const MAX_DEPS = 10;
56
58
  /**
57
- * Detect top-level dependencies from project manifest files.
58
- * Returns up to 10 dependency names (key deps only, not devDeps).
59
+ * Byte ceiling for a dependency manifest. A package.json or go.mod past this is
60
+ * not a manifest we can learn anything useful from — it is generated noise.
59
61
  */
60
- export async function detectDependencies(projectDir, stack) {
61
- const MAX_DEPS = 10;
62
- try {
63
- switch (stack) {
64
- case "node": {
65
- const pkgPath = path.join(projectDir, "package.json");
66
- if (!(await fs.pathExists(pkgPath)))
67
- return [];
68
- const pkg = await fs.readJson(pkgPath).catch(() => ({}));
69
- const deps = Object.keys(pkg.dependencies ?? {});
70
- return deps.slice(0, MAX_DEPS);
62
+ const MAX_MANIFEST_BYTES = 512 * 1024;
63
+ /** Read a manifest under a byte budget; returns null and a warning on failure. */
64
+ async function readManifest(manifestPath, warnings) {
65
+ const read = await safeReadFile(manifestPath, {
66
+ hardRejectOverBytes: MAX_MANIFEST_BYTES,
67
+ });
68
+ if (!read.ok) {
69
+ warnings.push(`${path.basename(manifestPath)}: ${describeSafeReadFailure(read)}`);
70
+ return null;
71
+ }
72
+ if (read.truncated) {
73
+ warnings.push(`${path.basename(manifestPath)}: truncated at ${read.bytesRead} of ${read.totalBytes} bytes`);
74
+ }
75
+ return read.content;
76
+ }
77
+ /**
78
+ * Detect top-level dependencies from project manifest files, reporting why a
79
+ * manifest was skipped. Returns up to 10 dependency names (key deps only).
80
+ */
81
+ export async function detectDependenciesDetailed(projectDir, stack) {
82
+ const warnings = [];
83
+ switch (stack) {
84
+ case "node": {
85
+ const pkgPath = path.join(projectDir, "package.json");
86
+ if (!(await fs.pathExists(pkgPath)))
87
+ return { dependencies: [], warnings };
88
+ const content = await readManifest(pkgPath, warnings);
89
+ if (content === null)
90
+ return { dependencies: [], warnings };
91
+ let pkg;
92
+ try {
93
+ pkg = JSON.parse(content);
71
94
  }
72
- case "python": {
73
- const pyprojectPath = path.join(projectDir, "pyproject.toml");
74
- if (await fs.pathExists(pyprojectPath)) {
75
- const content = await fs.readFile(pyprojectPath, "utf-8");
76
- const match = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
77
- if (match?.[1]) {
78
- const deps = match[1]
79
- .split("\n")
80
- .map((l) => l.replace(/[",]/g, "").trim())
81
- .filter((l) => l.length > 0 && !l.startsWith("#"))
82
- .map((l) => l.split(/[>=<~!]/)[0].trim())
83
- .filter(Boolean);
84
- return deps.slice(0, MAX_DEPS);
85
- }
86
- }
87
- const reqPath = path.join(projectDir, "requirements.txt");
88
- if (await fs.pathExists(reqPath)) {
89
- const content = await fs.readFile(reqPath, "utf-8");
90
- const deps = content
95
+ catch (err) {
96
+ warnings.push(`package.json: invalid JSON (${err instanceof Error ? err.message : String(err)})`);
97
+ return { dependencies: [], warnings };
98
+ }
99
+ const deps = Object.keys(pkg?.dependencies ?? {});
100
+ return { dependencies: deps.slice(0, MAX_DEPS), warnings };
101
+ }
102
+ case "python": {
103
+ const pyprojectPath = path.join(projectDir, "pyproject.toml");
104
+ if (await fs.pathExists(pyprojectPath)) {
105
+ const content = await readManifest(pyprojectPath, warnings);
106
+ const match = content?.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
107
+ if (match?.[1]) {
108
+ const deps = match[1]
91
109
  .split("\n")
92
- .map((l) => l.trim())
93
- .filter((l) => l.length > 0 && !l.startsWith("#") && !l.startsWith("-"))
110
+ .map((l) => l.replace(/[",]/g, "").trim())
111
+ .filter((l) => l.length > 0 && !l.startsWith("#"))
94
112
  .map((l) => l.split(/[>=<~!]/)[0].trim())
95
113
  .filter(Boolean);
96
- return deps.slice(0, MAX_DEPS);
114
+ return { dependencies: deps.slice(0, MAX_DEPS), warnings };
97
115
  }
98
- return [];
99
116
  }
100
- case "go": {
101
- const goModPath = path.join(projectDir, "go.mod");
102
- if (!(await fs.pathExists(goModPath)))
103
- return [];
104
- const content = await fs.readFile(goModPath, "utf-8");
105
- const requireBlock = content.match(/require\s*\(([\s\S]*?)\)/);
106
- if (requireBlock?.[1]) {
107
- const deps = requireBlock[1]
108
- .split("\n")
109
- .map((l) => l.trim())
110
- .filter((l) => l.length > 0 && !l.startsWith("//"))
111
- .map((l) => {
112
- const parts = l.split(/\s+/);
113
- const mod = parts[0] ?? "";
114
- return mod.split("/").pop() ?? mod;
115
- })
116
- .filter(Boolean);
117
- return deps.slice(0, MAX_DEPS);
118
- }
119
- return [];
117
+ const reqPath = path.join(projectDir, "requirements.txt");
118
+ if (await fs.pathExists(reqPath)) {
119
+ const content = await readManifest(reqPath, warnings);
120
+ if (content === null)
121
+ return { dependencies: [], warnings };
122
+ const deps = content
123
+ .split("\n")
124
+ .map((l) => l.trim())
125
+ .filter((l) => l.length > 0 && !l.startsWith("#") && !l.startsWith("-"))
126
+ .map((l) => l.split(/[>=<~!]/)[0].trim())
127
+ .filter(Boolean);
128
+ return { dependencies: deps.slice(0, MAX_DEPS), warnings };
120
129
  }
121
- case "rust": {
122
- const cargoPath = path.join(projectDir, "Cargo.toml");
123
- if (!(await fs.pathExists(cargoPath)))
124
- return [];
125
- const content = await fs.readFile(cargoPath, "utf-8");
126
- const depsSection = content.match(/\[dependencies\]([\s\S]*?)(?=\n\[|$)/);
127
- if (depsSection?.[1]) {
128
- const deps = depsSection[1]
129
- .split("\n")
130
- .map((l) => l.trim())
131
- .filter((l) => l.length > 0 && !l.startsWith("#"))
132
- .map((l) => l.split(/\s*=/)[0].trim())
133
- .filter(Boolean);
134
- return deps.slice(0, MAX_DEPS);
135
- }
136
- return [];
130
+ return { dependencies: [], warnings };
131
+ }
132
+ case "go": {
133
+ const goModPath = path.join(projectDir, "go.mod");
134
+ if (!(await fs.pathExists(goModPath)))
135
+ return { dependencies: [], warnings };
136
+ const content = await readManifest(goModPath, warnings);
137
+ const requireBlock = content?.match(/require\s*\(([\s\S]*?)\)/);
138
+ if (requireBlock?.[1]) {
139
+ const deps = requireBlock[1]
140
+ .split("\n")
141
+ .map((l) => l.trim())
142
+ .filter((l) => l.length > 0 && !l.startsWith("//"))
143
+ .map((l) => {
144
+ const parts = l.split(/\s+/);
145
+ const mod = parts[0] ?? "";
146
+ return mod.split("/").pop() ?? mod;
147
+ })
148
+ .filter(Boolean);
149
+ return { dependencies: deps.slice(0, MAX_DEPS), warnings };
137
150
  }
138
- default:
139
- return [];
151
+ return { dependencies: [], warnings };
140
152
  }
153
+ case "rust": {
154
+ const cargoPath = path.join(projectDir, "Cargo.toml");
155
+ if (!(await fs.pathExists(cargoPath)))
156
+ return { dependencies: [], warnings };
157
+ const content = await readManifest(cargoPath, warnings);
158
+ const depsSection = content?.match(/\[dependencies\]([\s\S]*?)(?=\n\[|$)/);
159
+ if (depsSection?.[1]) {
160
+ const deps = depsSection[1]
161
+ .split("\n")
162
+ .map((l) => l.trim())
163
+ .filter((l) => l.length > 0 && !l.startsWith("#"))
164
+ .map((l) => l.split(/\s*=/)[0].trim())
165
+ .filter(Boolean);
166
+ return { dependencies: deps.slice(0, MAX_DEPS), warnings };
167
+ }
168
+ return { dependencies: [], warnings };
169
+ }
170
+ default:
171
+ return { dependencies: [], warnings };
141
172
  }
142
- catch {
143
- return [];
144
- }
173
+ }
174
+ /**
175
+ * Detect top-level dependencies from project manifest files.
176
+ * Returns up to 10 dependency names (key deps only, not devDeps).
177
+ *
178
+ * List-returning convenience over `detectDependenciesDetailed`. It DISCARDS the
179
+ * read/parse warnings — an unreadable manifest is indistinguishable from an
180
+ * honest empty list through this function. Callers that must tell those two
181
+ * apart (like `refreshContextDir`) call `detectDependenciesDetailed` directly.
182
+ */
183
+ export async function detectDependencies(projectDir, stack) {
184
+ const { dependencies } = await detectDependenciesDetailed(projectDir, stack);
185
+ return dependencies;
145
186
  }
146
187
  // =============================================================================
147
188
  // Public API
@@ -190,7 +231,7 @@ export async function refreshContextDir(projectDir) {
190
231
  return null;
191
232
  }
192
233
  const stackCtx = getStackContext(manifest.stack);
193
- const dependencies = await detectDependencies(projectDir, manifest.stack);
234
+ const { dependencies, warnings } = await detectDependenciesDetailed(projectDir, manifest.stack);
194
235
  const index = buildIndexMd(manifest.projectName, stackCtx, manifest.ciProvider, manifest.memory);
195
236
  const summary = buildSummaryMd(manifest.projectName, manifest.stack, manifest.ciProvider, manifest.memory, manifest.modules, dependencies);
196
237
  // Write updated files
@@ -199,6 +240,6 @@ export async function refreshContextDir(projectDir) {
199
240
  // Update manifest timestamp
200
241
  manifest.updatedAt = new Date().toISOString();
201
242
  await fs.writeJson(manifestPath, manifest, { spaces: 2 });
202
- return { index, summary, updated: true };
243
+ return { index, summary, updated: true, warnings };
203
244
  }
204
245
  //# sourceMappingURL=context.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Bounded, non-throwing file reads.
3
+ *
4
+ * Every whole-file read in this CLI used to be an unguarded
5
+ * `fs.readFile(path, "utf-8")`: a 400 MB log, a minified bundle or a binary
6
+ * blob under a scanned directory could exhaust memory or stall a scan. This
7
+ * module is the single guarded entry point — it caps bytes during the read,
8
+ * rejects binaries by content sniffing, clamps pathological single lines, and
9
+ * returns a discriminated union instead of throwing for expected conditions.
10
+ *
11
+ * Deliberately NOT included: no path allow/block list. This is a local CLI
12
+ * operating on the user's own repository, so any path they can name they can
13
+ * already `cat`; a blocklist would add friction without adding a boundary.
14
+ */
15
+ /** Default byte budget for a single read (1 MiB). */
16
+ export declare const DEFAULT_MAX_BYTES: number;
17
+ /** Default per-line character clamp — catches minified bundles and data URIs. */
18
+ export declare const DEFAULT_MAX_LINE_LENGTH = 10000;
19
+ export interface SafeReadOptions {
20
+ /** Maximum bytes kept. Anything beyond is dropped and `truncated` is set. */
21
+ maxBytes?: number;
22
+ /** Per-line character clamp. Use `0` or `Infinity` to disable. */
23
+ maxLineLength?: number;
24
+ /**
25
+ * If the file is larger than this, fail with `too-large` instead of
26
+ * truncating. Off by default — truncation is the normal behavior.
27
+ */
28
+ hardRejectOverBytes?: number;
29
+ }
30
+ export type SafeReadFailureReason = "not-found" | "not-a-file" | "binary" | "too-large" | "io-error";
31
+ export interface SafeReadSuccess {
32
+ ok: true;
33
+ content: string;
34
+ /** True when the file had more bytes than the budget allowed. */
35
+ truncated: boolean;
36
+ /** Bytes actually decoded into `content` (before BOM stripping). */
37
+ bytesRead: number;
38
+ /** File size reported by `stat` at the time of the read. */
39
+ totalBytes: number;
40
+ /** True when at least one line hit the per-line clamp. */
41
+ longLinesClamped: boolean;
42
+ }
43
+ export interface SafeReadFailure {
44
+ ok: false;
45
+ reason: SafeReadFailureReason;
46
+ detail?: string;
47
+ }
48
+ export type SafeReadResult = SafeReadSuccess | SafeReadFailure;
49
+ /**
50
+ * Read a text file with a byte budget, binary rejection and line clamping.
51
+ *
52
+ * Never throws for expected conditions (missing file, directory, binary,
53
+ * oversized, permission denied) — inspect `result.ok` and branch on `reason`.
54
+ *
55
+ * Newlines are returned verbatim: CRLF is NOT normalized, because the callers
56
+ * migrated to this helper already tolerate `\r` (they `trim()` split lines) and
57
+ * silently rewriting bytes would make reported offsets diverge from the file.
58
+ */
59
+ export declare function safeReadFile(filePath: string, opts?: SafeReadOptions): Promise<SafeReadResult>;
60
+ /** Human-readable one-liner for a failed read — for CLI notes and findings. */
61
+ export declare function describeSafeReadFailure(failure: SafeReadFailure): string;
62
+ //# sourceMappingURL=safe-read.d.ts.map