@planu/cli 4.11.7 → 4.11.9

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 (52) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cli/commands/serve.js +4 -0
  3. package/dist/config/license-plans.json +1 -0
  4. package/dist/engine/browser-validator.js +26 -21
  5. package/dist/engine/crash-shield/file-collector.d.ts +20 -3
  6. package/dist/engine/crash-shield/file-collector.js +137 -8
  7. package/dist/engine/crash-shield/index.d.ts +18 -1
  8. package/dist/engine/crash-shield/index.js +58 -17
  9. package/dist/engine/dogfooding/runtime-gap-detector.d.ts +3 -0
  10. package/dist/engine/dogfooding/runtime-gap-detector.js +386 -0
  11. package/dist/engine/figma/visual-qa.d.ts +2 -1
  12. package/dist/engine/figma/visual-qa.js +8 -7
  13. package/dist/engine/qa-gate.js +2 -1
  14. package/dist/engine/session-safeguard/checkpoint-runner.js +3 -7
  15. package/dist/engine/spec-state-machine/transition-spec.d.ts +16 -1
  16. package/dist/engine/spec-state-machine/transition-spec.js +19 -4
  17. package/dist/engine/triagier/classifier.d.ts +2 -2
  18. package/dist/engine/triagier/classifier.js +12 -15
  19. package/dist/index.js +12 -4
  20. package/dist/storage/approval-operation-lock.d.ts +10 -0
  21. package/dist/storage/approval-operation-lock.js +44 -0
  22. package/dist/storage/approval-store.d.ts +2 -0
  23. package/dist/storage/approval-store.js +9 -1
  24. package/dist/storage/spec-store.d.ts +29 -2
  25. package/dist/storage/spec-store.js +307 -7
  26. package/dist/tools/approval-handler.js +255 -124
  27. package/dist/tools/browser-validate-handler.js +17 -3
  28. package/dist/tools/dogfood-watch.d.ts +6 -0
  29. package/dist/tools/dogfood-watch.js +48 -0
  30. package/dist/tools/figma/visual-qa.js +2 -1
  31. package/dist/tools/tool-registry/core-tools.js +12 -0
  32. package/dist/tools/tool-registry/group-quality-compliance.js +12 -1
  33. package/dist/tools/update-status/file-sync.js +3 -2
  34. package/dist/tools/update-status/index.d.ts +2 -0
  35. package/dist/tools/update-status/index.js +1083 -821
  36. package/dist/tools/update-status/response-builder.js +11 -0
  37. package/dist/tools/update-status/side-effects.d.ts +16 -1
  38. package/dist/tools/update-status/side-effects.js +140 -0
  39. package/dist/tools/update-status/transition-guard.js +1 -1
  40. package/dist/tools/update-status-actions.d.ts +10 -2
  41. package/dist/tools/update-status-actions.js +166 -192
  42. package/dist/tools/update-status-convention-gate.d.ts +3 -1
  43. package/dist/tools/update-status-convention-gate.js +135 -7
  44. package/dist/types/browser-validator.d.ts +2 -0
  45. package/dist/types/dogfooding.d.ts +34 -0
  46. package/dist/types/dogfooding.js +2 -0
  47. package/dist/types/index.d.ts +1 -0
  48. package/dist/types/index.js +1 -0
  49. package/dist/types/spec/core.d.ts +28 -1
  50. package/package.json +25 -25
  51. package/planu-native.json +1 -1
  52. package/planu-plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [4.11.9] - 2026-07-20
2
+
3
+ ### Bug Fixes
4
+ - fix: keep release session context current
5
+
6
+
7
+ ## [4.11.8] - 2026-07-20
8
+
9
+ ### Bug Fixes
10
+ - fix: make pre-commit checks fail closed
11
+ - fix: pin release-eligible transitive dependencies
12
+ - fix: sync release lockfile before build
13
+ - fix: stabilize quality gate execution
14
+ - fix: harden lifecycle and release reliability
15
+
16
+
1
17
  ## [4.11.7] - 2026-07-18
2
18
 
3
19
  ### Bug Fixes
@@ -29,6 +29,10 @@ export const serveCommand = {
29
29
  registerSpecTools(server);
30
30
  registerPlatformTools(server);
31
31
  await selectTransport(server, ['--transport', 'http', '--port', port], () => server);
32
+ const { recoverPendingPostCommitTasksAtStartup } = await import('../../tools/update-status/index.js');
33
+ void recoverPendingPostCommitTasksAtStartup().catch((error) => {
34
+ process.stderr.write(`Planu post-commit recovery failed: ${error instanceof Error ? error.message : String(error)}\n`);
35
+ });
32
36
  },
33
37
  };
34
38
  //# sourceMappingURL=serve.js.map
@@ -81,6 +81,7 @@
81
81
  "discover_docs_url",
82
82
  "discover_mcps",
83
83
  "discover_registry",
84
+ "dogfood_watch",
84
85
  "ears_lint",
85
86
  "ecosystem_status",
86
87
  "enable_auto_update",
@@ -87,62 +87,67 @@ export function extractUIAssertions(criteriaMarkdown) {
87
87
  }
88
88
  return assertions;
89
89
  }
90
- /** Escape a string for use inside a JS template literal. */
91
- function escapeTemplateLiteral(str) {
92
- return str.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
90
+ function escapeRegExpLiteral(value) {
91
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ }
93
+ function safeComment(value) {
94
+ return value.replace(/[\r\n\u2028\u2029]+/g, ' ');
93
95
  }
94
96
  /**
95
97
  * Generate a complete Playwright test file content from UI assertions.
96
98
  * Uses @playwright/test syntax with test.step() per assertion.
97
99
  */
98
100
  export function generatePlaywrightTest(assertions, specId, baseUrl) {
101
+ const manualStep = (message) => `throw new Error(${JSON.stringify(message)});`;
99
102
  const steps = assertions.map((a, i) => {
100
- const stepName = escapeTemplateLiteral(a.criterion);
101
- const locator = a.locator ? escapeTemplateLiteral(a.locator) : null;
103
+ const stepName = JSON.stringify(`${String(i + 1)}. ${a.criterion}`);
104
+ const locator = a.locator ? JSON.stringify(a.locator) : null;
102
105
  let body;
103
106
  switch (a.type) {
104
107
  case 'visibility':
105
108
  body = locator
106
- ? `await expect(page.locator(\`${locator}\`)).toBeVisible();`
107
- : `// TODO: add locator for: ${a.criterion}`;
109
+ ? `await expect(page.locator(${locator})).toBeVisible();`
110
+ : manualStep(`Manual locator required for visibility assertion: ${a.criterion}`);
108
111
  break;
109
112
  case 'text':
110
113
  body =
111
114
  locator && a.expected
112
- ? `await expect(page.locator(\`${locator}\`)).toContainText(\`${escapeTemplateLiteral(a.expected)}\`);`
115
+ ? `await expect(page.locator(${locator})).toContainText(${JSON.stringify(a.expected)});`
113
116
  : locator
114
- ? `await expect(page.locator(\`${locator}\`)).toBeVisible();`
115
- : `// TODO: verify text for: ${a.criterion}`;
117
+ ? `await expect(page.locator(${locator})).toBeVisible();`
118
+ : manualStep(`Manual text verification required: ${a.criterion}`);
116
119
  break;
117
120
  case 'navigation':
118
121
  body = a.expected
119
- ? `await expect(page).toHaveURL(/${escapeTemplateLiteral(a.expected)}/);`
120
- : `// TODO: verify navigation for: ${a.criterion}`;
122
+ ? `await expect(page).toHaveURL(new RegExp(${JSON.stringify(escapeRegExpLiteral(a.expected))}));`
123
+ : manualStep(`Manual navigation verification required: ${a.criterion}`);
121
124
  break;
122
125
  case 'form':
123
126
  body = locator
124
- ? `await expect(page.locator(\`${locator}\`)).toBeVisible();`
125
- : `// TODO: interact with form for: ${a.criterion}`;
127
+ ? `await expect(page.locator(${locator})).toBeVisible();`
128
+ : manualStep(`Manual form interaction required: ${a.criterion}`);
126
129
  break;
127
130
  case 'generic':
128
- body = `// TODO: verify: ${a.criterion}`;
131
+ body = manualStep(`Manual assertion required: ${a.criterion}`);
129
132
  break;
130
133
  default: {
131
134
  const _exhaustive = a.type;
132
- body = `// TODO: verify: ${String(_exhaustive)}`;
135
+ body = manualStep(`Unsupported assertion type requires manual validation: ${String(_exhaustive)}`);
133
136
  }
134
137
  }
135
- return ` await test.step('${String(i + 1)}. ${stepName}', async () => {\n ${body}\n });`;
138
+ return ` await test.step(${stepName}, async () => {\n ${body}\n });`;
136
139
  });
140
+ const testTitle = JSON.stringify(`${specId} — UI acceptance criteria`);
141
+ const serializedBaseUrl = JSON.stringify(baseUrl);
137
142
  return [
138
143
  `import { test, expect } from '@playwright/test';`,
139
144
  ``,
140
145
  `// Generated by Planu — SPEC-308 browser validator`,
141
- `// Spec: ${specId}`,
142
- `// Base URL: ${baseUrl}`,
146
+ `// Spec: ${safeComment(specId)}`,
147
+ `// Base URL: ${safeComment(baseUrl)}`,
143
148
  ``,
144
- `test('${specId} — UI acceptance criteria', async ({ page }) => {`,
145
- ` await page.goto('${baseUrl}');`,
149
+ `test(${testTitle}, async ({ page }) => {`,
150
+ ` await page.goto(${serializedBaseUrl});`,
146
151
  ``,
147
152
  ...steps,
148
153
  `});`,
@@ -1,10 +1,27 @@
1
1
  import type { FileCollectionResult } from '../../types/index.js';
2
2
  export type { FileCollectionResult };
3
+ export declare const DEFAULT_FILE_COLLECTION_BUDGET: Readonly<{
4
+ maxFiles: 1000;
5
+ maxBytes: number;
6
+ }>;
3
7
  /**
4
- * Collects files from the project path, detects the stack, and returns up to MAX_FILES paths.
5
- * Optionally filters by enabled languages.
8
+ * Collects eligible runtime files in deterministic order within an explicit budget.
9
+ * Coverage metadata makes every partial collection visible to callers.
6
10
  */
7
- export declare function collectFiles(projectPath: string, enabledLanguages?: string[]): Promise<FileCollectionResult>;
11
+ export declare function collectFiles(projectPath: string, enabledLanguages?: string[], budget?: {
12
+ maxFiles?: number;
13
+ maxBytes?: number;
14
+ }): Promise<FileCollectionResult & {
15
+ coverage: {
16
+ complete: boolean;
17
+ discoveredFiles: number;
18
+ selectedFiles: number;
19
+ selectedBytes: number;
20
+ maxFiles: number;
21
+ maxBytes: number;
22
+ truncatedBy: ('file-count' | 'byte-budget' | 'metadata-error')[];
23
+ };
24
+ }>;
8
25
  /**
9
26
  * Processes items in sequential batches to avoid unbounded concurrency.
10
27
  * Each batch runs in parallel internally, but batches are processed one at a time.
@@ -1,6 +1,11 @@
1
1
  // engine/crash-shield/file-collector.ts — File discovery for crash shield (SPEC-442)
2
2
  import { glob } from 'glob';
3
- import { join } from 'node:path';
3
+ import { stat } from 'node:fs/promises';
4
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ export const DEFAULT_FILE_COLLECTION_BUDGET = Object.freeze({
6
+ maxFiles: 1_000,
7
+ maxBytes: 50 * 1024 * 1024,
8
+ });
4
9
  const STACK_EXTENSIONS = {
5
10
  TypeScript: ['.ts', '.tsx'],
6
11
  JavaScript: ['.js', '.jsx', '.mjs', '.cjs'],
@@ -11,30 +16,154 @@ const STACK_EXTENSIONS = {
11
16
  PHP: ['.php'],
12
17
  Ruby: ['.rb'],
13
18
  };
14
- const MAX_FILES = 400;
15
19
  const IGNORE_PATTERNS = [
16
20
  '**/node_modules/**',
21
+ '**/vendor/**',
17
22
  '**/dist/**',
23
+ '**/build/**',
24
+ '**/target/**',
25
+ '**/coverage/**',
26
+ '**/.next/**',
27
+ '**/.nuxt/**',
28
+ '**/.cache/**',
18
29
  '**/.git/**',
30
+ '**/tests/**',
31
+ '**/test/**',
32
+ '**/__tests__/**',
33
+ '**/fixtures/**',
34
+ '**/__fixtures__/**',
35
+ '**/generated/**',
36
+ '**/*.test.*',
37
+ '**/*.spec.*',
38
+ '**/test_*.py',
39
+ '**/*_test.go',
19
40
  '**/*.d.ts',
20
41
  '**/*.min.js',
21
42
  ];
22
43
  /**
23
- * Collects files from the project path, detects the stack, and returns up to MAX_FILES paths.
24
- * Optionally filters by enabled languages.
44
+ * Collects eligible runtime files in deterministic order within an explicit budget.
45
+ * Coverage metadata makes every partial collection visible to callers.
25
46
  */
26
- export async function collectFiles(projectPath, enabledLanguages) {
47
+ export async function collectFiles(projectPath, enabledLanguages, budget = {}) {
27
48
  const extensions = buildExtensionList(enabledLanguages);
28
49
  const pattern = buildGlobPattern(extensions);
50
+ const limits = normalizeBudget(budget);
29
51
  const allFiles = await glob(pattern, {
30
52
  cwd: projectPath,
31
53
  ignore: IGNORE_PATTERNS,
32
54
  absolute: true,
33
55
  nodir: true,
34
56
  });
35
- const files = allFiles.slice(0, MAX_FILES).map((f) => join(f));
36
- const detectedStack = detectStack(files);
37
- return { files, detectedStack };
57
+ const eligibleFiles = normalizeEligibleFiles(projectPath, allFiles);
58
+ const detectedStack = detectStack(eligibleFiles);
59
+ const files = [];
60
+ let selectedBytes = 0;
61
+ let metadataErrors = 0;
62
+ let byteBudgetReached = false;
63
+ for (const file of eligibleFiles) {
64
+ if (files.length >= limits.maxFiles) {
65
+ break;
66
+ }
67
+ let fileBytes;
68
+ try {
69
+ fileBytes = (await stat(join(projectPath, file))).size;
70
+ }
71
+ catch {
72
+ metadataErrors += 1;
73
+ continue;
74
+ }
75
+ if (selectedBytes + fileBytes > limits.maxBytes) {
76
+ byteBudgetReached = true;
77
+ break;
78
+ }
79
+ files.push(file);
80
+ selectedBytes += fileBytes;
81
+ }
82
+ const truncatedBy = [];
83
+ if (files.length + metadataErrors < eligibleFiles.length && files.length >= limits.maxFiles) {
84
+ truncatedBy.push('file-count');
85
+ }
86
+ if (byteBudgetReached) {
87
+ truncatedBy.push('byte-budget');
88
+ }
89
+ if (metadataErrors > 0) {
90
+ truncatedBy.push('metadata-error');
91
+ }
92
+ return {
93
+ files,
94
+ detectedStack,
95
+ coverage: {
96
+ complete: truncatedBy.length === 0,
97
+ discoveredFiles: eligibleFiles.length,
98
+ selectedFiles: files.length,
99
+ selectedBytes,
100
+ maxFiles: limits.maxFiles,
101
+ maxBytes: limits.maxBytes,
102
+ truncatedBy,
103
+ },
104
+ };
105
+ }
106
+ function normalizeEligibleFiles(projectPath, files) {
107
+ return [
108
+ ...new Set(files
109
+ .map((file) => normalizeProjectRelativePath(projectPath, file))
110
+ .filter((file) => file !== null)
111
+ .filter(isRuntimeSourcePath)),
112
+ ].sort((left, right) => left.localeCompare(right));
113
+ }
114
+ function normalizeProjectRelativePath(projectPath, filePath) {
115
+ const projectRoot = resolve(projectPath);
116
+ const absolutePath = isAbsolute(filePath) ? resolve(filePath) : resolve(projectRoot, filePath);
117
+ const relativePath = relative(projectRoot, absolutePath);
118
+ if (relativePath.length === 0 ||
119
+ relativePath === '..' ||
120
+ relativePath.startsWith(`..${sep}`) ||
121
+ isAbsolute(relativePath)) {
122
+ return null;
123
+ }
124
+ return relativePath;
125
+ }
126
+ function normalizeBudget(budget) {
127
+ return {
128
+ maxFiles: normalizePositiveInteger(budget.maxFiles, DEFAULT_FILE_COLLECTION_BUDGET.maxFiles),
129
+ maxBytes: normalizePositiveInteger(budget.maxBytes, DEFAULT_FILE_COLLECTION_BUDGET.maxBytes),
130
+ };
131
+ }
132
+ function normalizePositiveInteger(value, fallback) {
133
+ return typeof value === 'number' && Number.isFinite(value) && value > 0
134
+ ? Math.floor(value)
135
+ : fallback;
136
+ }
137
+ function isRuntimeSourcePath(filePath) {
138
+ const normalized = filePath.replaceAll('\\', '/');
139
+ const segments = normalized.split('/');
140
+ const excludedDirectories = new Set([
141
+ 'node_modules',
142
+ 'vendor',
143
+ 'dist',
144
+ 'build',
145
+ 'target',
146
+ 'coverage',
147
+ '.next',
148
+ '.nuxt',
149
+ '.cache',
150
+ '.git',
151
+ 'tests',
152
+ 'test',
153
+ '__tests__',
154
+ 'fixtures',
155
+ '__fixtures__',
156
+ 'generated',
157
+ ]);
158
+ if (segments.some((segment) => excludedDirectories.has(segment))) {
159
+ return false;
160
+ }
161
+ const fileName = segments.at(-1) ?? '';
162
+ return (!/\.(?:test|spec)\.[^.]+$/i.test(fileName) &&
163
+ !/^test_.*\.py$/i.test(fileName) &&
164
+ !/_test\.go$/i.test(fileName) &&
165
+ !/\.d\.ts$/i.test(fileName) &&
166
+ !/\.min\.js$/i.test(fileName));
38
167
  }
39
168
  function buildExtensionList(enabledLanguages) {
40
169
  if (!enabledLanguages || enabledLanguages.length === 0) {
@@ -4,6 +4,23 @@ export { fixCrashRisks } from './auto-fixer/index.js';
4
4
  * Scans a project for crash risks and returns a SafetyReport.
5
5
  * @param projectPath - absolute path to the project root
6
6
  * @param stackHint - optional list of languages to restrict scanning (e.g. ['TypeScript'])
7
+ * @param options - optional file, byte, and read-batch budgets
7
8
  */
8
- export declare function scanCrashRisks(projectPath: string, stackHint?: string[]): Promise<SafetyReport>;
9
+ export declare function scanCrashRisks(projectPath: string, stackHint?: string[], options?: {
10
+ maxFiles?: number;
11
+ maxBytes?: number;
12
+ readBatchSize?: number;
13
+ }): Promise<SafetyReport & {
14
+ coverage: {
15
+ complete: boolean;
16
+ discoveredFiles: number;
17
+ selectedFiles: number;
18
+ selectedBytes: number;
19
+ maxFiles: number;
20
+ maxBytes: number;
21
+ truncatedBy: ('file-count' | 'byte-budget' | 'metadata-error')[];
22
+ scannedFiles: number;
23
+ unreadableFiles: number;
24
+ };
25
+ }>;
9
26
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
2
3
  import { collectFiles } from './file-collector.js';
3
4
  import { calculateScore } from './score-calculator.js';
4
5
  import { typescriptDetector } from './detectors/typescript-detector.js';
@@ -17,41 +18,81 @@ const ALL_DETECTORS = [
17
18
  javaDetector,
18
19
  frameworkDetector,
19
20
  ];
21
+ const DEFAULT_READ_BATCH_SIZE = 50;
20
22
  /**
21
23
  * Scans a project for crash risks and returns a SafetyReport.
22
24
  * @param projectPath - absolute path to the project root
23
25
  * @param stackHint - optional list of languages to restrict scanning (e.g. ['TypeScript'])
26
+ * @param options - optional file, byte, and read-batch budgets
24
27
  */
25
- export async function scanCrashRisks(projectPath, stackHint) {
28
+ export async function scanCrashRisks(projectPath, stackHint, options = {}) {
26
29
  const startTime = Date.now();
27
- const { files, detectedStack } = await collectFiles(projectPath, stackHint);
28
- const allRisks = await scanFiles(files, detectedStack);
30
+ const { files, detectedStack, coverage } = await collectFiles(projectPath, stackHint, options);
31
+ const scan = await scanFiles(projectPath, files, detectedStack, normalizeBatchSize(options.readBatchSize));
32
+ const allRisks = scan.risks.sort(compareRisks);
29
33
  const score = calculateScore(allRisks);
30
34
  const scanDurationMs = Date.now() - startTime;
31
35
  return {
32
36
  score,
33
37
  risks: allRisks,
34
- scannedFiles: files.length,
38
+ scannedFiles: scan.scannedFiles,
35
39
  detectedStack,
36
40
  scanDurationMs,
41
+ coverage: {
42
+ ...coverage,
43
+ complete: coverage.complete && scan.unreadableFiles === 0,
44
+ scannedFiles: scan.scannedFiles,
45
+ unreadableFiles: scan.unreadableFiles,
46
+ },
37
47
  };
38
48
  }
39
- async function scanFiles(files, detectedStack) {
49
+ function compareRisks(left, right) {
50
+ return (left.file.localeCompare(right.file) ||
51
+ left.line - right.line ||
52
+ left.pattern.localeCompare(right.pattern) ||
53
+ left.severity.localeCompare(right.severity));
54
+ }
55
+ async function scanFiles(projectPath, files, detectedStack, batchSize) {
40
56
  const applicableDetectors = selectDetectors(detectedStack);
41
- const fileContents = fastReadFiles(files);
42
- const seen = new Set(fileContents.map((file) => file.path));
43
- const results = fileContents.map(({ path, content }) => detectRisksInContent(path, content, applicableDetectors));
44
- const unreadByBridge = files.filter((file) => !seen.has(file));
45
- const fallbackResults = await Promise.all(unreadByBridge.map(async (file) => {
46
- try {
47
- const content = await readFile(file, 'utf-8');
48
- return detectRisksInContent(file, content, applicableDetectors);
57
+ const risks = [];
58
+ let scannedFiles = 0;
59
+ let unreadableFiles = 0;
60
+ for (let offset = 0; offset < files.length; offset += batchSize) {
61
+ const batch = files.slice(offset, offset + batchSize);
62
+ const absoluteToRelative = new Map(batch.map((file) => [join(projectPath, file), file]));
63
+ const fileContents = fastReadFiles([...absoluteToRelative.keys()]);
64
+ const seen = new Set(fileContents.map((file) => file.path));
65
+ for (const { path, content } of fileContents) {
66
+ risks.push(...detectRisksInContent(absoluteToRelative.get(path) ?? path, content, applicableDetectors));
67
+ scannedFiles += 1;
49
68
  }
50
- catch {
51
- return [];
69
+ const fallbackResults = await Promise.all(batch
70
+ .filter((file) => !seen.has(join(projectPath, file)))
71
+ .map(async (file) => {
72
+ try {
73
+ const content = await readFile(join(projectPath, file), 'utf-8');
74
+ return detectRisksInContent(file, content, applicableDetectors);
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }));
80
+ for (const result of fallbackResults) {
81
+ if (result === null) {
82
+ unreadableFiles += 1;
83
+ }
84
+ else {
85
+ risks.push(...result);
86
+ scannedFiles += 1;
87
+ }
52
88
  }
53
- }));
54
- return [...results, ...fallbackResults].flat();
89
+ }
90
+ return { risks, scannedFiles, unreadableFiles };
91
+ }
92
+ function normalizeBatchSize(value) {
93
+ return typeof value === 'number' && Number.isFinite(value) && value > 0
94
+ ? Math.floor(value)
95
+ : DEFAULT_READ_BATCH_SIZE;
55
96
  }
56
97
  function detectRisksInContent(path, content, applicableDetectors) {
57
98
  const fileDetectors = applicableDetectors.filter((d) => d.fileExtensions.some((ext) => path.endsWith(ext)));
@@ -0,0 +1,3 @@
1
+ import type { DogfoodReport } from '../../types/dogfooding.js';
2
+ export declare function analyzeRuntimeDogfooding(projectPath: string): Promise<DogfoodReport>;
3
+ //# sourceMappingURL=runtime-gap-detector.d.ts.map