@jamie-tam/forge 6.0.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.
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/agents/architect.md +92 -0
- package/agents/builder.md +122 -0
- package/agents/code-reviewer.md +107 -0
- package/agents/concept-designer.md +207 -0
- package/agents/craft-reviewer.md +132 -0
- package/agents/critic.md +130 -0
- package/agents/doc-writer.md +85 -0
- package/agents/dreamer.md +129 -0
- package/agents/e2e-runner.md +89 -0
- package/agents/gotcha-hunter.md +127 -0
- package/agents/prototype-builder.md +193 -0
- package/agents/prototype-codifier.md +204 -0
- package/agents/prototype-reviewer.md +163 -0
- package/agents/security-reviewer.md +108 -0
- package/agents/spec-reviewer.md +94 -0
- package/agents/tracer.md +98 -0
- package/agents/wireframer.md +109 -0
- package/commands/abort.md +25 -0
- package/commands/bugfix.md +151 -0
- package/commands/evolve.md +118 -0
- package/commands/feature.md +236 -0
- package/commands/forge.md +100 -0
- package/commands/greenfield.md +185 -0
- package/commands/hotfix.md +98 -0
- package/commands/refactor.md +147 -0
- package/commands/resume.md +25 -0
- package/commands/setup.md +201 -0
- package/commands/status.md +27 -0
- package/commands/task-force.md +110 -0
- package/commands/validate.md +12 -0
- package/dist/__tests__/active-manifest.test.js +272 -0
- package/dist/__tests__/copy.test.js +96 -0
- package/dist/__tests__/gate-check.test.js +384 -0
- package/dist/__tests__/wiki.test.js +472 -0
- package/dist/__tests__/work-manifest.test.js +304 -0
- package/dist/active-manifest.js +229 -0
- package/dist/cli.js +158 -0
- package/dist/copy.js +124 -0
- package/dist/gate-check.js +326 -0
- package/dist/hooks.js +60 -0
- package/dist/init.js +140 -0
- package/dist/manifest.js +90 -0
- package/dist/merge.js +77 -0
- package/dist/paths.js +36 -0
- package/dist/uninstall.js +216 -0
- package/dist/update.js +158 -0
- package/dist/verify-manifest.js +65 -0
- package/dist/verify.js +98 -0
- package/dist/wiki-ui.js +310 -0
- package/dist/wiki.js +364 -0
- package/dist/work-manifest.js +798 -0
- package/hooks/config/gate-requirements.json +79 -0
- package/hooks/hooks.json +143 -0
- package/hooks/scripts/analyze-telemetry.sh +114 -0
- package/hooks/scripts/gate-enforcer.sh +164 -0
- package/hooks/scripts/pre-compact.sh +90 -0
- package/hooks/scripts/session-start.sh +81 -0
- package/hooks/scripts/telemetry.sh +41 -0
- package/hooks/scripts/wiki-lint.sh +87 -0
- package/hooks/templates/AGENTS.md.template +48 -0
- package/hooks/templates/CLAUDE.md.template +45 -0
- package/package.json +55 -0
- package/protocols/README.md +40 -0
- package/protocols/codex.md +151 -0
- package/protocols/graphify.md +156 -0
- package/references/common/agent-coordination.md +65 -0
- package/references/common/coding-standards.md +54 -0
- package/references/common/feature-tracking.md +21 -0
- package/references/common/io-protocol.md +36 -0
- package/references/common/phases.md +57 -0
- package/references/common/quality-gates.md +130 -0
- package/references/common/skill-authoring.md +154 -0
- package/references/common/skill-compliance.md +30 -0
- package/references/python/standards.md +44 -0
- package/references/react/standards.md +61 -0
- package/references/typescript/standards.md +42 -0
- package/rules/common/forge-system.md +59 -0
- package/rules/common/git-workflow.md +40 -0
- package/rules/common/guardrails.md +37 -0
- package/rules/common/quality-gates.md +18 -0
- package/rules/common/security.md +50 -0
- package/rules/common/skill-selection.md +78 -0
- package/rules/common/testing.md +58 -0
- package/rules/common/verification.md +39 -0
- package/skills/build-pr-workflow/SKILL.md +301 -0
- package/skills/build-pr-workflow/references/pr-template.md +62 -0
- package/skills/build-pr-workflow/references/subagent-merge.md +47 -0
- package/skills/build-pr-workflow/references/worktree-setup.md +125 -0
- package/skills/build-prototype/SKILL.md +264 -0
- package/skills/build-scaffold/SKILL.md +340 -0
- package/skills/build-tdd/SKILL.md +89 -0
- package/skills/build-wireframe/SKILL.md +110 -0
- package/skills/build-wireframe/assets/baseline-template.html +486 -0
- package/skills/build-wireframe/references/demo-walkthroughs.md +170 -0
- package/skills/build-wireframe/references/gotchas.md +188 -0
- package/skills/build-wireframe/references/legend-lines.md +141 -0
- package/skills/concept-slides/SKILL.md +192 -0
- package/skills/deliver-db-migration/SKILL.md +466 -0
- package/skills/deliver-deploy/SKILL.md +407 -0
- package/skills/deliver-onboarding/SKILL.md +198 -0
- package/skills/deliver-onboarding/references/document-templates.md +393 -0
- package/skills/deliver-onboarding/templates/getting-started.md +122 -0
- package/skills/discover-codebase-analysis/SKILL.md +448 -0
- package/skills/discover-requirements/SKILL.md +418 -0
- package/skills/discover-requirements/templates/prd.md +99 -0
- package/skills/discover-requirements/templates/technical-spec.md +123 -0
- package/skills/discover-requirements/templates/user-stories.md +76 -0
- package/skills/harden/SKILL.md +214 -0
- package/skills/iterate-prototype/SKILL.md +241 -0
- package/skills/plan-architecture/SKILL.md +457 -0
- package/skills/plan-architecture/templates/adr-template.md +52 -0
- package/skills/plan-architecture/templates/api-contract.md +99 -0
- package/skills/plan-architecture/templates/db-schema.md +81 -0
- package/skills/plan-architecture/templates/system-design.md +111 -0
- package/skills/plan-brainstorm/SKILL.md +433 -0
- package/skills/plan-design-system/SKILL.md +279 -0
- package/skills/plan-task-decompose/SKILL.md +454 -0
- package/skills/quality-code-review/SKILL.md +286 -0
- package/skills/quality-security-audit/SKILL.md +292 -0
- package/skills/quality-security-audit/references/audit-report-template.md +89 -0
- package/skills/quality-security-audit/references/owasp-checks.md +178 -0
- package/skills/quality-test-execution/SKILL.md +435 -0
- package/skills/quality-test-plan/SKILL.md +297 -0
- package/skills/quality-test-plan/references/test-type-guide.md +263 -0
- package/skills/quality-test-plan/templates/e2e-test-plan.md +72 -0
- package/skills/quality-test-plan/templates/integration-test-plan.md +74 -0
- package/skills/quality-test-plan/templates/load-test-plan.md +111 -0
- package/skills/quality-test-plan/templates/smoke-test-plan.md +68 -0
- package/skills/quality-test-plan/templates/unit-test-plan.md +56 -0
- package/skills/quality-uiux/SKILL.md +481 -0
- package/skills/support-debug/SKILL.md +464 -0
- package/skills/support-dream/SKILL.md +213 -0
- package/skills/support-gotcha/SKILL.md +249 -0
- package/skills/support-runtime-reachability/SKILL.md +190 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/app.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/handlers/cases.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/app.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/handlers/cases.ts +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/App.tsx +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/components/RingingBanner.tsx +7 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/hooks/useTwilio.ts +6 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/App.tsx +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/components/MyComp.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/App.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/components/Orphan.tsx +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/lib/Service.ts +6 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/main.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/lib/Lonely.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/main.ts +2 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/handler.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/handler.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/main.ts +2 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/lib.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/index.ts +1 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/internal.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/main.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.test.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-13-gated-pending-annotation/src/future.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-14-untraceable-annotation/src/decorated.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-15-untraceable-empty/src/lazy.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/lib.py +15 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/main.py +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/parent.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/routes/cases.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/lib/foo.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/other.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/cases.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/users.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/main.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/handlers/cases.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/main.ts +4 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/lib.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/main.ts +5 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/lib.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/main.ts +8 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/lib.ts +3 -0
- package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/main.ts +7 -0
- package/skills/support-runtime-reachability/scripts/check.mjs +638 -0
- package/skills/support-runtime-reachability/scripts/check.test.mjs +244 -0
- package/skills/support-skill-validator/SKILL.md +194 -0
- package/skills/support-skill-validator/references/false-positives.md +59 -0
- package/skills/support-skill-validator/references/validation-checks.md +280 -0
- package/skills/support-system-guide/SKILL.md +311 -0
- package/skills/support-task-force/SKILL.md +265 -0
- package/skills/support-task-force/references/dispatch-pattern.md +178 -0
- package/skills/support-task-force/references/synthesis-template.md +126 -0
- package/skills/support-wiki-bootstrap/SKILL.md +37 -0
- package/skills/support-wiki-lint/SKILL.md +196 -0
- package/skills/support-wiki-lint/scripts/lint.mjs +488 -0
- package/skills/support-wiki-lint/scripts/lint.test.mjs +196 -0
- package/templates/README.md +23 -0
- package/templates/aiwiki/CLAUDE.md.template +78 -0
- package/templates/aiwiki/schemas/architecture.md +118 -0
- package/templates/aiwiki/schemas/convention.md +112 -0
- package/templates/aiwiki/schemas/decision.md +144 -0
- package/templates/aiwiki/schemas/gotcha.md +118 -0
- package/templates/aiwiki/schemas/oracle.md +105 -0
- package/templates/aiwiki/schemas/session.md +125 -0
- package/templates/manifests/bugfix.yaml +41 -0
- package/templates/manifests/feature.yaml +69 -0
- package/templates/manifests/greenfield.yaml +61 -0
- package/templates/manifests/hotfix.yaml +45 -0
- package/templates/manifests/refactor.yaml +44 -0
- package/templates/manifests/v5/SCHEMA.md +327 -0
- package/templates/manifests/v5/feature.yaml +77 -0
- package/templates/manifests/v6/SCHEMA.md +199 -0
- package/templates/wiki-html/dream-detail.html +378 -0
- package/templates/wiki-html/dreams-list.html +155 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runtime reachability check (v5.0).
|
|
3
|
+
// Walks the slice-diff files, identifies top-level exports, and searches the
|
|
4
|
+
// codebase for production callers + escape-hatch annotations. Outputs JSON to
|
|
5
|
+
// stdout. Manifest cross-check (validating gated-pending against slice_graph)
|
|
6
|
+
// is the agent's job, not the script's.
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
// ---- Defaults --------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TEST_GLOBS = [
|
|
15
|
+
'**/*.test.*',
|
|
16
|
+
'**/*.spec.*',
|
|
17
|
+
'**/__tests__/**',
|
|
18
|
+
'**/tests/**',
|
|
19
|
+
'**/e2e/**',
|
|
20
|
+
'**/cypress/**',
|
|
21
|
+
'**/__fixtures__/**',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const DEFAULT_IGNORE_DIRS = new Set([
|
|
25
|
+
'node_modules',
|
|
26
|
+
'dist',
|
|
27
|
+
'build',
|
|
28
|
+
'.git',
|
|
29
|
+
'coverage',
|
|
30
|
+
'.next',
|
|
31
|
+
'.nuxt',
|
|
32
|
+
'.svelte-kit',
|
|
33
|
+
'.cache',
|
|
34
|
+
'.turbo',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const TSJS_EXT = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
38
|
+
const PY_EXT = /\.py$/;
|
|
39
|
+
|
|
40
|
+
// ---- Public API ------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export function check({ files, root, testGlobs }) {
|
|
43
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
44
|
+
throw new Error('files must be a non-empty array');
|
|
45
|
+
}
|
|
46
|
+
if (typeof root !== 'string') throw new Error('root must be a string');
|
|
47
|
+
|
|
48
|
+
const tg = testGlobs ?? DEFAULT_TEST_GLOBS;
|
|
49
|
+
const exports = [];
|
|
50
|
+
|
|
51
|
+
// Index codebase files once (reused across all exports).
|
|
52
|
+
const codebaseFiles = [...walkFiles(root)];
|
|
53
|
+
|
|
54
|
+
for (const relFile of files) {
|
|
55
|
+
const absFile = path.join(root, relFile);
|
|
56
|
+
if (!fs.existsSync(absFile)) continue;
|
|
57
|
+
const content = readSafe(absFile);
|
|
58
|
+
const lang = detectLanguage(relFile);
|
|
59
|
+
if (!lang) continue;
|
|
60
|
+
|
|
61
|
+
const fileExports = parseExports(content, lang, relFile);
|
|
62
|
+
for (const exp of fileExports) {
|
|
63
|
+
const annotation = findAnnotation(content, exp.line, lang);
|
|
64
|
+
const callers = findCallers({
|
|
65
|
+
name: exp.name,
|
|
66
|
+
kind: exp.kind,
|
|
67
|
+
lang,
|
|
68
|
+
ownerFile: relFile,
|
|
69
|
+
codebaseFiles,
|
|
70
|
+
root,
|
|
71
|
+
});
|
|
72
|
+
const production = callers.filter((c) => !isTestFile(c.file, tg));
|
|
73
|
+
const test = callers.filter((c) => isTestFile(c.file, tg));
|
|
74
|
+
exports.push({
|
|
75
|
+
name: exp.name,
|
|
76
|
+
file: relFile,
|
|
77
|
+
line: exp.line,
|
|
78
|
+
kind: exp.kind,
|
|
79
|
+
annotation,
|
|
80
|
+
production_callers: production,
|
|
81
|
+
test_callers: test,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const stats = {
|
|
87
|
+
files_checked: files.length,
|
|
88
|
+
exports_found: exports.length,
|
|
89
|
+
orphans_unannotated: exports.filter(
|
|
90
|
+
(e) => e.production_callers.length === 0 && e.annotation === null,
|
|
91
|
+
).length,
|
|
92
|
+
annotated: exports.filter((e) => e.annotation !== null).length,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return { exports, stats };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- Language detection ----------------------------------------------------
|
|
99
|
+
|
|
100
|
+
function detectLanguage(file) {
|
|
101
|
+
if (TSJS_EXT.test(file)) return 'tsjs';
|
|
102
|
+
if (PY_EXT.test(file)) return 'py';
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- Export parsing --------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
function parseExports(content, lang, file) {
|
|
109
|
+
const lines = content.split('\n');
|
|
110
|
+
const out = [];
|
|
111
|
+
|
|
112
|
+
if (lang === 'tsjs') {
|
|
113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
114
|
+
const line = lines[i];
|
|
115
|
+
|
|
116
|
+
// Skip type-only exports.
|
|
117
|
+
if (/^\s*export\s+(type|interface|enum)\s/.test(line)) continue;
|
|
118
|
+
|
|
119
|
+
let m;
|
|
120
|
+
// export (async) function NAME
|
|
121
|
+
if ((m = /^\s*export\s+(?:async\s+)?function\s+(\w+)/.exec(line))) {
|
|
122
|
+
out.push({ name: m[1], line: i + 1, kind: 'function' });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// export (abstract) class NAME
|
|
126
|
+
if ((m = /^\s*export\s+(?:abstract\s+)?class\s+(\w+)/.exec(line))) {
|
|
127
|
+
// Skip if this is "export default class NAME" (handled below).
|
|
128
|
+
if (/^\s*export\s+default\s/.test(line)) continue;
|
|
129
|
+
out.push({ name: m[1], line: i + 1, kind: 'class' });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// export const|let|var NAME =
|
|
133
|
+
if ((m = /^\s*export\s+(?:const|let|var)\s+(\w+)/.exec(line))) {
|
|
134
|
+
out.push({ name: m[1], line: i + 1, kind: 'const' });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// export default function NAME
|
|
138
|
+
if ((m = /^\s*export\s+default\s+(?:async\s+)?function\s+(\w+)/.exec(line))) {
|
|
139
|
+
out.push({ name: m[1], line: i + 1, kind: 'default' });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// export default class NAME
|
|
143
|
+
if ((m = /^\s*export\s+default\s+(?:abstract\s+)?class\s+(\w+)/.exec(line))) {
|
|
144
|
+
out.push({ name: m[1], line: i + 1, kind: 'default' });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// export default <anonymous expression> — name from filename
|
|
148
|
+
if (/^\s*export\s+default\s/.test(line)) {
|
|
149
|
+
const stem = path.basename(file).replace(/\.[^.]+$/, '');
|
|
150
|
+
out.push({ name: stem, line: i + 1, kind: 'default' });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// export { foo, bar as baz, qux } [from '...']
|
|
155
|
+
if ((m = /^\s*export\s+\{\s*([^}]+)\s*\}/.exec(line))) {
|
|
156
|
+
for (const item of m[1].split(',')) {
|
|
157
|
+
const parts = item.trim().split(/\s+as\s+/);
|
|
158
|
+
// Externally-visible name is the alias (right side of `as`) or the only token.
|
|
159
|
+
const externalName = (parts[1] ?? parts[0]).trim();
|
|
160
|
+
if (!externalName || !/^\w+$/.test(externalName)) continue;
|
|
161
|
+
out.push({ name: externalName, line: i + 1, kind: 'reexport' });
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (lang === 'py') {
|
|
167
|
+
for (let i = 0; i < lines.length; i++) {
|
|
168
|
+
const line = lines[i];
|
|
169
|
+
let m;
|
|
170
|
+
// Top-level def NAME (no leading whitespace)
|
|
171
|
+
if ((m = /^(?:async\s+)?def\s+(\w+)\s*\(/.exec(line))) {
|
|
172
|
+
if (m[1].startsWith('_')) continue;
|
|
173
|
+
out.push({ name: m[1], line: i + 1, kind: 'def' });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Top-level class NAME
|
|
177
|
+
if ((m = /^class\s+(\w+)/.exec(line))) {
|
|
178
|
+
if (m[1].startsWith('_')) continue;
|
|
179
|
+
out.push({ name: m[1], line: i + 1, kind: 'pyclass' });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---- Annotation parsing ----------------------------------------------------
|
|
189
|
+
|
|
190
|
+
const ANNOTATION_PATTERN = /(gated-pending|untraceable)\s*:\s*(.*)$/;
|
|
191
|
+
|
|
192
|
+
function findAnnotation(content, exportLine, lang) {
|
|
193
|
+
const lines = content.split('\n');
|
|
194
|
+
const exportIdx = exportLine - 1;
|
|
195
|
+
const lineCommentPrefix = lang === 'py' ? '#' : '//';
|
|
196
|
+
|
|
197
|
+
// Trailing comment on the export line itself.
|
|
198
|
+
const trailingMatch = matchInComment(lines[exportIdx], lineCommentPrefix);
|
|
199
|
+
if (trailingMatch) return trailingMatch;
|
|
200
|
+
|
|
201
|
+
// Walk preceding consecutive comment lines.
|
|
202
|
+
for (let i = exportIdx - 1; i >= 0; i--) {
|
|
203
|
+
const line = lines[i].trimEnd();
|
|
204
|
+
if (line === '') return null; // blank line breaks the comment block
|
|
205
|
+
const trimmed = line.trimStart();
|
|
206
|
+
if (!trimmed.startsWith(lineCommentPrefix)) return null;
|
|
207
|
+
const m = matchInComment(line, lineCommentPrefix);
|
|
208
|
+
if (m) return m;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function matchInComment(line, prefix) {
|
|
214
|
+
if (!line) return null;
|
|
215
|
+
const idx = line.indexOf(prefix);
|
|
216
|
+
if (idx < 0) return null;
|
|
217
|
+
const tail = line.slice(idx + prefix.length).trim();
|
|
218
|
+
const m = ANNOTATION_PATTERN.exec(tail);
|
|
219
|
+
if (!m) return null;
|
|
220
|
+
return { kind: m[1], value: m[2].trim(), raw: line.slice(idx).trim() };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---- Caller search ---------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
function findCallers({ name, kind, lang, ownerFile, codebaseFiles, root }) {
|
|
226
|
+
const callers = [];
|
|
227
|
+
const ownerAbs = path.resolve(root, ownerFile);
|
|
228
|
+
const ownerStem = path.basename(ownerFile).replace(/\.[^.]+$/, '');
|
|
229
|
+
|
|
230
|
+
// For "default" exports, we treat the name as the file stem; production callers
|
|
231
|
+
// are import-default lines (`import X from '...'`) plus subsequent invocations.
|
|
232
|
+
// The script reports any "import default from ownerFile" caller as a usage hit.
|
|
233
|
+
|
|
234
|
+
for (const candidate of codebaseFiles) {
|
|
235
|
+
if (path.resolve(candidate) === ownerAbs) continue;
|
|
236
|
+
const candLang = detectLanguage(candidate);
|
|
237
|
+
if (!candLang) continue;
|
|
238
|
+
if (lang !== candLang) continue; // don't mix TS callers with Python exports
|
|
239
|
+
|
|
240
|
+
const content = readSafe(candidate);
|
|
241
|
+
if (!content) continue;
|
|
242
|
+
|
|
243
|
+
const relCandidate = path.relative(root, candidate);
|
|
244
|
+
|
|
245
|
+
// FP mitigation: require the candidate to import the export name (with
|
|
246
|
+
// path provenance) before counting callers. Track local names so aliased
|
|
247
|
+
// imports (`import { foo as bar }; bar()`) match the alias usage.
|
|
248
|
+
let locals;
|
|
249
|
+
if (kind === 'default') {
|
|
250
|
+
const localName = findDefaultImportLocal(content, ownerAbs, candidate, root);
|
|
251
|
+
locals = localName ? [localName] : [];
|
|
252
|
+
} else {
|
|
253
|
+
locals = findImportLocals(content, name, candLang, ownerAbs, candidate, root);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (locals.length === 0) {
|
|
257
|
+
// Accept downstream re-exports as a usage signal: `export { name } from '<path-to-owner>'`.
|
|
258
|
+
if (candLang === 'tsjs' && hasReExportFrom(content, name, ownerAbs, candidate, root)) {
|
|
259
|
+
callers.push({ file: relCandidate, line: lineOfReExport(content, name), pattern: 're-export' });
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const hits = scanCallerPatternsForLocals(content, locals, candLang);
|
|
265
|
+
for (const h of hits) callers.push({ file: relCandidate, line: h.line, pattern: h.pattern });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return callers;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function findImportLocals(content, name, lang, ownerAbs, candidateAbs, root) {
|
|
272
|
+
// Returns array of local names used in the caller for the export `name`
|
|
273
|
+
// from `ownerAbs`. Empty if the caller doesn't import the export. Path-like
|
|
274
|
+
// specifiers (relative/absolute) must resolve to ownerAbs (FP mitigation);
|
|
275
|
+
// bare specifiers (`react`, `@org/pkg`, path aliases) trust the name.
|
|
276
|
+
// Namespace imports return synthetic `ns.name` locals for downstream scanning.
|
|
277
|
+
const locals = new Set();
|
|
278
|
+
|
|
279
|
+
if (lang === 'py') {
|
|
280
|
+
const fromImport = /^\s*from\s+([\w.]+)\s+import\s+([^\n#]+)/gm;
|
|
281
|
+
let m;
|
|
282
|
+
while ((m = fromImport.exec(content)) !== null) {
|
|
283
|
+
const mod = m[1];
|
|
284
|
+
if (mod.startsWith('.')) {
|
|
285
|
+
const resolved = resolvePyModule(path.dirname(candidateAbs), mod);
|
|
286
|
+
if (!resolved || !pathEqualish(resolved, ownerAbs)) continue;
|
|
287
|
+
}
|
|
288
|
+
for (const raw of m[2].split(',')) {
|
|
289
|
+
const item = raw.trim().replace(/[()\\]/g, '');
|
|
290
|
+
if (!item) continue;
|
|
291
|
+
const parts = item.split(/\s+as\s+/);
|
|
292
|
+
if (parts[0].trim() === name) locals.add((parts[1] ?? parts[0]).trim());
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// `import <module>` (no path resolution for v5.0); add namespaced access form.
|
|
296
|
+
const importMod = /^\s*import\s+([\w.]+)(?:\s+as\s+(\w+))?/gm;
|
|
297
|
+
while ((m = importMod.exec(content)) !== null) {
|
|
298
|
+
const tail = m[1].split('.').pop();
|
|
299
|
+
const alias = m[2] ?? tail;
|
|
300
|
+
// Heuristic: if `alias.name` appears anywhere, it's a usage — emit synthetic local.
|
|
301
|
+
const access = new RegExp(`\\b${escapeRe(alias)}\\.${escapeRe(name)}\\b`);
|
|
302
|
+
if (access.test(content)) locals.add(`${alias}.${name}`);
|
|
303
|
+
}
|
|
304
|
+
return [...locals];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// TS/JS:
|
|
308
|
+
// Match `import { ... } from '...'` and `import x, { ... } from '...'` (mixed default+named).
|
|
309
|
+
const importBlock =
|
|
310
|
+
/import\s+(?:\w+\s*,\s*)?(?:type\s*)?\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
311
|
+
let m;
|
|
312
|
+
while ((m = importBlock.exec(content)) !== null) {
|
|
313
|
+
const importPath = m[2];
|
|
314
|
+
if (!importTargetsOwner(importPath, candidateAbs, root, ownerAbs)) continue;
|
|
315
|
+
for (const item of m[1].split(',')) {
|
|
316
|
+
const parts = item.trim().split(/\s+as\s+/);
|
|
317
|
+
const importedName = parts[0].trim();
|
|
318
|
+
const localName = (parts[1] ?? parts[0]).trim();
|
|
319
|
+
if (importedName === name) locals.add(localName);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const ns = /import\s*\*\s*as\s+(\w+)\s+from\s*['"]([^'"]+)['"]/g;
|
|
323
|
+
while ((m = ns.exec(content)) !== null) {
|
|
324
|
+
const importPath = m[2];
|
|
325
|
+
if (!importTargetsOwner(importPath, candidateAbs, root, ownerAbs)) continue;
|
|
326
|
+
const nsName = m[1];
|
|
327
|
+
const access = new RegExp(`\\b${escapeRe(nsName)}\\.${escapeRe(name)}\\b`);
|
|
328
|
+
if (access.test(content)) locals.add(`${nsName}.${name}`);
|
|
329
|
+
}
|
|
330
|
+
return [...locals];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function importTargetsOwner(importPath, candidateAbs, root, ownerAbs) {
|
|
334
|
+
// Returns true when this import is plausibly bringing in the export from
|
|
335
|
+
// ownerAbs. Two cases return true:
|
|
336
|
+
// 1. Path-like specifier (relative or absolute) that resolves to ownerAbs.
|
|
337
|
+
// 2. Bare specifier (`react`, `@org/pkg`, path aliases like `@/`) — we
|
|
338
|
+
// cannot resolve it here, so we TRUST the name match. This trades a
|
|
339
|
+
// tiny FP risk (two unrelated bare modules with the same export name)
|
|
340
|
+
// for FP-orphan-free behavior on aliased projects (much more common).
|
|
341
|
+
// Path-like specifiers that DON'T resolve to ownerAbs return false — this
|
|
342
|
+
// is the case-19 fix (same name imported from a different relative module).
|
|
343
|
+
if (importPath.startsWith('.') || path.isAbsolute(importPath)) {
|
|
344
|
+
const resolved = resolveImport(path.dirname(candidateAbs), importPath);
|
|
345
|
+
return Boolean(resolved && pathEqualish(resolved, ownerAbs));
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resolvePyModule(fromDir, mod) {
|
|
351
|
+
// `.lib` -> ./lib.py or ./lib/__init__.py; `..lib` -> ../lib.py
|
|
352
|
+
let dots = 0;
|
|
353
|
+
while (dots < mod.length && mod[dots] === '.') dots++;
|
|
354
|
+
const rest = mod.slice(dots);
|
|
355
|
+
let target = fromDir;
|
|
356
|
+
for (let i = 1; i < dots; i++) target = path.dirname(target);
|
|
357
|
+
const segments = rest.split('.').filter(Boolean);
|
|
358
|
+
const base = path.join(target, ...segments);
|
|
359
|
+
for (const candidate of [base + '.py', path.join(base, '__init__.py')]) {
|
|
360
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function hasReExportFrom(content, name, ownerAbs, candidateAbs, root) {
|
|
366
|
+
// Catch `export { name } from '<path>'` where <path> resolves to ownerAbs.
|
|
367
|
+
const re = /export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
368
|
+
let m;
|
|
369
|
+
while ((m = re.exec(content)) !== null) {
|
|
370
|
+
const importPath = m[2];
|
|
371
|
+
if (!importTargetsOwner(importPath, candidateAbs, root, ownerAbs)) continue;
|
|
372
|
+
for (const item of m[1].split(',')) {
|
|
373
|
+
const parts = item.trim().split(/\s+as\s+/);
|
|
374
|
+
const original = parts[0].trim();
|
|
375
|
+
if (original === name) return true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function lineOfReExport(content, name) {
|
|
382
|
+
const lines = content.split('\n');
|
|
383
|
+
const re = /export\s*\{([^}]+)\}/;
|
|
384
|
+
for (let i = 0; i < lines.length; i++) {
|
|
385
|
+
const m = re.exec(lines[i]);
|
|
386
|
+
if (!m) continue;
|
|
387
|
+
if (m[1].split(',').some((it) => it.trim().split(/\s+as\s+/)[0].trim() === name)) {
|
|
388
|
+
return i + 1;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function findDefaultImportLocal(content, ownerAbs, candidatePath, root) {
|
|
395
|
+
// Find `import LOCAL from '<path>'` or `import LOCAL, { ... } from '<path>'`
|
|
396
|
+
// where path resolves to ownerAbs.
|
|
397
|
+
const re = /import\s+(\w+)(?:\s*,\s*\{[^}]*\})?\s+from\s*['"]([^'"]+)['"]/g;
|
|
398
|
+
let m;
|
|
399
|
+
const candidateAbs = path.resolve(root, candidatePath);
|
|
400
|
+
while ((m = re.exec(content)) !== null) {
|
|
401
|
+
const localName = m[1];
|
|
402
|
+
const importPath = m[2];
|
|
403
|
+
if (!importPath.startsWith('.') && !path.isAbsolute(importPath)) continue;
|
|
404
|
+
const resolved = resolveImport(path.dirname(candidateAbs), importPath);
|
|
405
|
+
if (resolved && pathEqualish(resolved, ownerAbs)) return localName;
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function resolveImport(fromDir, importPath) {
|
|
411
|
+
const exts = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '/index.ts', '/index.tsx', '/index.js', '/index.jsx'];
|
|
412
|
+
const base = path.resolve(fromDir, importPath);
|
|
413
|
+
for (const ext of exts) {
|
|
414
|
+
const candidate = base + ext;
|
|
415
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function pathEqualish(a, b) {
|
|
421
|
+
// Compare with extension stripped (TS allows extensionless imports).
|
|
422
|
+
const strip = (p) => p.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, '');
|
|
423
|
+
return strip(path.resolve(a)) === strip(path.resolve(b));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function scanCallerPatternsForLocals(content, locals, lang) {
|
|
427
|
+
const hits = [];
|
|
428
|
+
if (locals.length === 0) return hits;
|
|
429
|
+
const lines = content.split('\n');
|
|
430
|
+
const commentPrefix = lang === 'py' ? '#' : '//';
|
|
431
|
+
|
|
432
|
+
// Build patterns per local name. A local containing '.' is a namespace
|
|
433
|
+
// access (e.g. `ns.foo` from `import * as ns`); JSX/new patterns aren't
|
|
434
|
+
// typical for that form so we only emit invocation/mount/reference.
|
|
435
|
+
const patterns = [];
|
|
436
|
+
for (const local of locals) {
|
|
437
|
+
const escaped = escapeRe(local);
|
|
438
|
+
const isMember = local.includes('.');
|
|
439
|
+
if (lang === 'py') {
|
|
440
|
+
patterns.push({ kind: 'invocation', re: new RegExp(`\\b${escaped}\\s*\\(`) });
|
|
441
|
+
patterns.push({ kind: 'reference', re: new RegExp(`(?<!\\.)\\b${escaped}\\b`) });
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (!isMember) {
|
|
445
|
+
patterns.push({ kind: 'new', re: new RegExp(`\\bnew\\s+${escaped}\\s*\\(`) });
|
|
446
|
+
patterns.push({ kind: 'jsx', re: new RegExp(`<${escaped}(\\s|/|>)`) });
|
|
447
|
+
}
|
|
448
|
+
patterns.push({
|
|
449
|
+
kind: 'mount',
|
|
450
|
+
re: new RegExp(
|
|
451
|
+
`\\b(?:app|router)\\.(?:use|get|post|put|delete|patch|head|options|all)\\s*\\([^)]*\\b${escaped}\\b`,
|
|
452
|
+
),
|
|
453
|
+
});
|
|
454
|
+
patterns.push({ kind: 'invocation', re: new RegExp(`\\b${escaped}\\s*\\(`) });
|
|
455
|
+
// Reference fallback: any non-property occurrence of the local name.
|
|
456
|
+
// Covers member access (`meta.version`), pass-as-arg (`fn(handler)`),
|
|
457
|
+
// assignments, etc. Lower-priority than the structural patterns above.
|
|
458
|
+
if (isMember) {
|
|
459
|
+
patterns.push({ kind: 'reference', re: new RegExp(`\\b${escaped}\\b`) });
|
|
460
|
+
} else {
|
|
461
|
+
patterns.push({ kind: 'reference', re: new RegExp(`(?<!\\.)\\b${escaped}\\b`) });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Skip lines inside import statements so the import declaration itself
|
|
466
|
+
// (which contains the local name) doesn't register as a caller. Tracks
|
|
467
|
+
// multi-line imports too — `import {\n foo,\n bar,\n} from '...'` — by
|
|
468
|
+
// staying in skip mode until the `from '...'` or `import '...'` clause is
|
|
469
|
+
// seen. Dynamic imports (`import('./x')`) and meta access (`import.meta`)
|
|
470
|
+
// do NOT enter skip mode — both lack the whitespace after `import` that
|
|
471
|
+
// identifies a static import declaration.
|
|
472
|
+
// Static import must be `import` + whitespace + a token that ISN'T `(`.
|
|
473
|
+
// The negative lookbehind keeps `import ('./x')` (space before paren — still
|
|
474
|
+
// dynamic) out of skip mode.
|
|
475
|
+
const STATIC_IMPORT_START = /^\s*import\s+(?!\()/;
|
|
476
|
+
const STATIC_IMPORT_END = /\bfrom\s*['"][^'"]+['"]|^\s*import\s+['"][^'"]+['"]/;
|
|
477
|
+
let inImport = false;
|
|
478
|
+
for (let i = 0; i < lines.length; i++) {
|
|
479
|
+
const lineRaw = lines[i];
|
|
480
|
+
if (!inImport && STATIC_IMPORT_START.test(lineRaw)) inImport = true;
|
|
481
|
+
if (inImport) {
|
|
482
|
+
if (STATIC_IMPORT_END.test(lineRaw)) inImport = false;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const line = stripLineComment(lineRaw, commentPrefix);
|
|
486
|
+
for (const p of patterns) {
|
|
487
|
+
if (p.re.test(line)) {
|
|
488
|
+
hits.push({ line: i + 1, pattern: p.kind });
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return hits;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function stripLineComment(line, commentPrefix) {
|
|
497
|
+
const idx = findLineCommentStart(line, commentPrefix);
|
|
498
|
+
return idx >= 0 ? line.slice(0, idx) : line;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function findLineCommentStart(line, prefix) {
|
|
502
|
+
// Two-state scan over single/double/backtick quotes; returns the index where
|
|
503
|
+
// the line comment starts (outside any string literal), or -1 if none.
|
|
504
|
+
let inSingle = false;
|
|
505
|
+
let inDouble = false;
|
|
506
|
+
let inBacktick = false;
|
|
507
|
+
for (let i = 0; i < line.length; i++) {
|
|
508
|
+
const c = line[i];
|
|
509
|
+
const prev = line[i - 1];
|
|
510
|
+
if (c === '\\' && prev !== '\\') {
|
|
511
|
+
i++; // skip escaped char
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (!inDouble && !inBacktick && c === "'") inSingle = !inSingle;
|
|
515
|
+
else if (!inSingle && !inBacktick && c === '"') inDouble = !inDouble;
|
|
516
|
+
else if (!inSingle && !inDouble && c === '`') inBacktick = !inBacktick;
|
|
517
|
+
if (!inSingle && !inDouble && !inBacktick) {
|
|
518
|
+
if (line.startsWith(prefix, i)) return i;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return -1;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ---- Filesystem helpers ----------------------------------------------------
|
|
525
|
+
|
|
526
|
+
function* walkFiles(dir, depth = 0) {
|
|
527
|
+
if (depth > 24) return;
|
|
528
|
+
let entries;
|
|
529
|
+
try {
|
|
530
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
531
|
+
} catch {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
for (const e of entries) {
|
|
535
|
+
if (DEFAULT_IGNORE_DIRS.has(e.name)) continue;
|
|
536
|
+
if (e.name.startsWith('.') && e.name !== '.claude') continue;
|
|
537
|
+
const full = path.join(dir, e.name);
|
|
538
|
+
if (e.isDirectory()) yield* walkFiles(full, depth + 1);
|
|
539
|
+
else if (e.isFile()) yield full;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function readSafe(file) {
|
|
544
|
+
try {
|
|
545
|
+
return fs.readFileSync(file, 'utf8');
|
|
546
|
+
} catch {
|
|
547
|
+
return '';
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function isTestFile(file, globs) {
|
|
552
|
+
// Cheap glob match: exact-segment globs (e.g. **/*.test.*, **/__tests__/**)
|
|
553
|
+
for (const g of globs) {
|
|
554
|
+
if (matchGlob(g, file)) return true;
|
|
555
|
+
}
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function matchGlob(pattern, file) {
|
|
560
|
+
// Convert glob to regex: ** -> .*, * -> [^/]*, . -> \., everything else literal.
|
|
561
|
+
const re = new RegExp(
|
|
562
|
+
'^' +
|
|
563
|
+
pattern
|
|
564
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
565
|
+
.replace(/\*\*/g, '')
|
|
566
|
+
.replace(/\*/g, '[^/]*')
|
|
567
|
+
.replace(//g, '.*') +
|
|
568
|
+
'$',
|
|
569
|
+
);
|
|
570
|
+
return re.test(file);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function escapeRe(s) {
|
|
574
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ---- CLI -------------------------------------------------------------------
|
|
578
|
+
|
|
579
|
+
function parseArgs(argv) {
|
|
580
|
+
const out = { files: [], root: process.cwd(), testGlobs: undefined };
|
|
581
|
+
for (let i = 0; i < argv.length; i++) {
|
|
582
|
+
const a = argv[i];
|
|
583
|
+
if (a === '--files') {
|
|
584
|
+
out.files = (argv[++i] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
585
|
+
} else if (a === '--root') {
|
|
586
|
+
out.root = path.resolve(argv[++i]);
|
|
587
|
+
} else if (a === '--test-globs') {
|
|
588
|
+
out.testGlobs = (argv[++i] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
589
|
+
} else if (a === '--help' || a === '-h') {
|
|
590
|
+
out.help = true;
|
|
591
|
+
} else {
|
|
592
|
+
throw new Error(`Unknown arg: ${a}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return out;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function printHelp() {
|
|
599
|
+
process.stdout.write(
|
|
600
|
+
[
|
|
601
|
+
'Usage: check.mjs --files <a,b,c> [--root <dir>] [--test-globs <a,b>]',
|
|
602
|
+
'',
|
|
603
|
+
'Walks slice-diff exports and reports production / test callers + annotations.',
|
|
604
|
+
'Output: JSON to stdout. Manifest cross-check is the agent\'s responsibility.',
|
|
605
|
+
'',
|
|
606
|
+
].join('\n'),
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const isMain = (() => {
|
|
611
|
+
try {
|
|
612
|
+
return import.meta.url === `file://${fileURLToPath(import.meta.url)}` && process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
613
|
+
} catch {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
})();
|
|
617
|
+
|
|
618
|
+
if (isMain) {
|
|
619
|
+
let args;
|
|
620
|
+
try {
|
|
621
|
+
args = parseArgs(process.argv.slice(2));
|
|
622
|
+
} catch (e) {
|
|
623
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
624
|
+
process.exit(2);
|
|
625
|
+
}
|
|
626
|
+
if (args.help || args.files.length === 0) {
|
|
627
|
+
printHelp();
|
|
628
|
+
process.exit(args.help ? 0 : 2);
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const result = check(args);
|
|
632
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
633
|
+
process.exit(0);
|
|
634
|
+
} catch (e) {
|
|
635
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
636
|
+
process.exit(2);
|
|
637
|
+
}
|
|
638
|
+
}
|