@renzrollon/interlock 1.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.
Files changed (125) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +58 -0
  3. package/CHANGELOG.md +380 -0
  4. package/LICENSE +21 -0
  5. package/README.md +358 -0
  6. package/agents/ping.md +8 -0
  7. package/agents/worker.md +8 -0
  8. package/bin/interlock +2655 -0
  9. package/bin/interlock-graph +6 -0
  10. package/bin/interlock-run +508 -0
  11. package/bin/interlock-ship-acp +39 -0
  12. package/hooks/_shared.mjs +162 -0
  13. package/hooks/guard-commit.mjs +77 -0
  14. package/hooks/guard-tasks.mjs +107 -0
  15. package/hooks/guard-tests.mjs +63 -0
  16. package/hooks/preflight.mjs +107 -0
  17. package/lib/artifacts.mjs +316 -0
  18. package/lib/autonomy.mjs +149 -0
  19. package/lib/conformance.mjs +132 -0
  20. package/lib/doctor.mjs +1176 -0
  21. package/lib/drift.mjs +409 -0
  22. package/lib/eval-history.mjs +304 -0
  23. package/lib/evals-calibrate.mjs +317 -0
  24. package/lib/evals-capture.mjs +437 -0
  25. package/lib/evals-promote.mjs +257 -0
  26. package/lib/evals-triage.mjs +216 -0
  27. package/lib/findings.mjs +340 -0
  28. package/lib/graph/cli.mjs +266 -0
  29. package/lib/graph/extract/config-json.mjs +74 -0
  30. package/lib/graph/extract/create-extractor.mjs +35 -0
  31. package/lib/graph/extract/memory.mjs +99 -0
  32. package/lib/graph/extract/openspec.mjs +80 -0
  33. package/lib/graph/extract/python.mjs +137 -0
  34. package/lib/graph/extract/regex-fallback.mjs +187 -0
  35. package/lib/graph/extract/shell.mjs +71 -0
  36. package/lib/graph/extract/typescript.mjs +217 -0
  37. package/lib/graph/extract/walk.mjs +272 -0
  38. package/lib/graph/format.mjs +51 -0
  39. package/lib/graph/graph/build.mjs +374 -0
  40. package/lib/graph/graph/query.mjs +249 -0
  41. package/lib/graph/graph/report.mjs +122 -0
  42. package/lib/graph/graph/update.mjs +47 -0
  43. package/lib/graph/paths.mjs +66 -0
  44. package/lib/graph/retrieve/context.mjs +86 -0
  45. package/lib/graph/retrieve/docs-index.mjs +116 -0
  46. package/lib/graph/retrieve/markdown.mjs +246 -0
  47. package/lib/host/acp.mjs +551 -0
  48. package/lib/host/claude-cli.mjs +285 -0
  49. package/lib/host/codex.mjs +231 -0
  50. package/lib/host/model-map.mjs +129 -0
  51. package/lib/host/qwen.mjs +176 -0
  52. package/lib/host/registry.mjs +213 -0
  53. package/lib/host.mjs +393 -0
  54. package/lib/ledger.mjs +444 -0
  55. package/lib/limits.mjs +525 -0
  56. package/lib/locators.mjs +99 -0
  57. package/lib/merge-lanes.mjs +148 -0
  58. package/lib/metrics.mjs +99 -0
  59. package/lib/notify.mjs +229 -0
  60. package/lib/outcomes.mjs +560 -0
  61. package/lib/plan-fingerprint.mjs +570 -0
  62. package/lib/project-slug.mjs +18 -0
  63. package/lib/prompts/commit.mjs +26 -0
  64. package/lib/prompts/dimensions.mjs +70 -0
  65. package/lib/prompts/handoff.mjs +86 -0
  66. package/lib/prompts/implementer.mjs +150 -0
  67. package/lib/prompts/index.mjs +250 -0
  68. package/lib/prompts/planner.mjs +70 -0
  69. package/lib/prompts/remediate.mjs +137 -0
  70. package/lib/prompts/replan.mjs +23 -0
  71. package/lib/prompts/review.mjs +107 -0
  72. package/lib/prompts/schemas.mjs +206 -0
  73. package/lib/prompts/stage.mjs +48 -0
  74. package/lib/prompts/verify.mjs +88 -0
  75. package/lib/ready.mjs +812 -0
  76. package/lib/receipt.mjs +434 -0
  77. package/lib/remediate.mjs +275 -0
  78. package/lib/report-html.mjs +623 -0
  79. package/lib/report.mjs +1124 -0
  80. package/lib/review-core.mjs +713 -0
  81. package/lib/risk.mjs +370 -0
  82. package/lib/run-log.mjs +1004 -0
  83. package/lib/run-paths.mjs +239 -0
  84. package/lib/run.mjs +2921 -0
  85. package/lib/ship-stage.mjs +223 -0
  86. package/lib/spill.mjs +150 -0
  87. package/lib/surface.mjs +195 -0
  88. package/lib/verify.mjs +1074 -0
  89. package/lib/waves.mjs +2661 -0
  90. package/package.json +40 -0
  91. package/shared/CONTEXT-HYGIENE.md +84 -0
  92. package/shared/DECISION-LEDGER.md +141 -0
  93. package/shared/EXPLORE-BRIEF.md +96 -0
  94. package/shared/INVARIANT-SWEEP.md +57 -0
  95. package/shared/SESSION-SHAPES.md +261 -0
  96. package/shared/SKILL-CANDIDATE-BRIEF.md +106 -0
  97. package/shared/TEST-PROFILE.md +204 -0
  98. package/shared/TOOL-ECONOMY.md +155 -0
  99. package/skills/bootstrap/SKILL.md +226 -0
  100. package/skills/commit/SKILL.md +99 -0
  101. package/skills/dispatch/SKILL.md +78 -0
  102. package/skills/docs-digest/SKILL.md +109 -0
  103. package/skills/evals/SKILL.md +160 -0
  104. package/skills/explain-code/SKILL.md +42 -0
  105. package/skills/explain-code/commit-teachin.md +136 -0
  106. package/skills/explain-code/module-walkthrough.md +358 -0
  107. package/skills/explore/SKILL.md +159 -0
  108. package/skills/fix-tests/SKILL.md +120 -0
  109. package/skills/graph/SKILL.md +136 -0
  110. package/skills/manual-test-plan/SKILL.md +121 -0
  111. package/skills/mr/SKILL.md +170 -0
  112. package/skills/report/SKILL.md +121 -0
  113. package/skills/review-artifacts/SKILL.md +127 -0
  114. package/skills/review-code/REVIEW.template.md +49 -0
  115. package/skills/review-code/SKILL.md +156 -0
  116. package/skills/review-code/dimensions/architecture.md +10 -0
  117. package/skills/review-code/dimensions/devops.md +12 -0
  118. package/skills/review-code/dimensions/language.md +12 -0
  119. package/skills/review-code/dimensions/qa.md +12 -0
  120. package/skills/review-code/dimensions/security.md +12 -0
  121. package/skills/review-code/dimensions/technical-lead.md +19 -0
  122. package/skills/ship/SKILL.md +67 -0
  123. package/skills/spec/SKILL.md +264 -0
  124. package/skills/spec/continuity.md +66 -0
  125. package/workflows/ship.js +565 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Regex-based JS/TS extractor. Used when typescript package is unavailable.
3
+ * Extracts imports, exports, and best-effort same-file references.
4
+ */
5
+
6
+ const IMPORT_RE =
7
+ /(?:import\s+(?:type\s+)?(?:([\w*{}\s,]+)\s+from\s+)?|export\s+(?:type\s+)?(?:[\w*{}\s,]+\s+from\s+)?|require\s*\(\s*)['"]([^'"]+)['"]/g;
8
+
9
+ const NAMED_IMPORT_RE = /(?:import|export)\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
10
+ const DEFAULT_IMPORT_RE =
11
+ /import\s+(?:type\s+)?([A-Za-z_$][\w$]*)\s*(?:,\s*\{[^}]*\})?\s*from\s+['"]([^'"]+)['"]/g;
12
+ const NS_IMPORT_RE = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]([^'"]+)['"]/g;
13
+ const REQUIRE_RE = /(?:const|let|var)\s+(?:\{([^}]+)\}|([A-Za-z_$][\w$]*))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
14
+
15
+ const EXPORT_NAMED_RE =
16
+ /export\s+(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\s+([A-Za-z_$][\w$]*)/g;
17
+ const EXPORT_LIST_RE = /export\s+(?:type\s+)?\{([^}]+)\}/g;
18
+ const EXPORT_DEFAULT_RE = /export\s+default\s+(?:async\s+)?(?:function|class)?\s*([A-Za-z_$][\w$]*)?/g;
19
+
20
+ function lineOf(text, index) {
21
+ return text.slice(0, index).split('\n').length;
22
+ }
23
+
24
+ function parseSpecifiers(raw) {
25
+ return raw
26
+ .split(',')
27
+ .map((s) => s.trim())
28
+ .filter(Boolean)
29
+ .map((s) => {
30
+ const parts = s.split(/\s+as\s+/i);
31
+ const local = (parts[1] || parts[0]).trim();
32
+ const imported = parts[0].replace(/^type\s+/, '').trim();
33
+ return { imported, local };
34
+ })
35
+ .filter((s) => s.local && s.local !== 'type');
36
+ }
37
+
38
+ /**
39
+ * @param {string} filePath relative posix path
40
+ * @param {string} source
41
+ */
42
+ export function extractWithRegex(filePath, source) {
43
+ const imports = [];
44
+ const exports = [];
45
+ const references = [];
46
+ const importedLocals = new Map(); // localName -> { from, line }
47
+
48
+ let m;
49
+ NAMED_IMPORT_RE.lastIndex = 0;
50
+ while ((m = NAMED_IMPORT_RE.exec(source))) {
51
+ const line = lineOf(source, m.index);
52
+ const from = m[2];
53
+ for (const spec of parseSpecifiers(m[1])) {
54
+ imports.push({
55
+ from,
56
+ imported: spec.imported,
57
+ local: spec.local,
58
+ line,
59
+ confidence: 'EXTRACTED',
60
+ });
61
+ importedLocals.set(spec.local, { from, line });
62
+ }
63
+ }
64
+
65
+ DEFAULT_IMPORT_RE.lastIndex = 0;
66
+ while ((m = DEFAULT_IMPORT_RE.exec(source))) {
67
+ const line = lineOf(source, m.index);
68
+ const local = m[1];
69
+ const from = m[2];
70
+ imports.push({ from, imported: 'default', local, line, confidence: 'EXTRACTED' });
71
+ importedLocals.set(local, { from, line });
72
+ }
73
+
74
+ NS_IMPORT_RE.lastIndex = 0;
75
+ while ((m = NS_IMPORT_RE.exec(source))) {
76
+ const line = lineOf(source, m.index);
77
+ imports.push({
78
+ from: m[2],
79
+ imported: '*',
80
+ local: m[1],
81
+ line,
82
+ confidence: 'EXTRACTED',
83
+ });
84
+ importedLocals.set(m[1], { from: m[2], line });
85
+ }
86
+
87
+ REQUIRE_RE.lastIndex = 0;
88
+ while ((m = REQUIRE_RE.exec(source))) {
89
+ const line = lineOf(source, m.index);
90
+ const from = m[3];
91
+ if (m[1]) {
92
+ for (const spec of parseSpecifiers(m[1])) {
93
+ imports.push({
94
+ from,
95
+ imported: spec.imported,
96
+ local: spec.local,
97
+ line,
98
+ confidence: 'EXTRACTED',
99
+ });
100
+ importedLocals.set(spec.local, { from, line });
101
+ }
102
+ } else if (m[2]) {
103
+ imports.push({
104
+ from,
105
+ imported: 'default',
106
+ local: m[2],
107
+ line,
108
+ confidence: 'EXTRACTED',
109
+ });
110
+ importedLocals.set(m[2], { from, line });
111
+ }
112
+ }
113
+
114
+ // Bare side-effect imports / export-from without binding already covered partially;
115
+ // catch remaining import 'x' and export ... from 'x'
116
+ IMPORT_RE.lastIndex = 0;
117
+ const seenFrom = new Set(imports.map((i) => `${i.from}@${i.line}`));
118
+ while ((m = IMPORT_RE.exec(source))) {
119
+ const from = m[2];
120
+ const line = lineOf(source, m.index);
121
+ const key = `${from}@${line}`;
122
+ if (seenFrom.has(key)) continue;
123
+ // Only add if this looks like a side-effect import (no binding captured in m[1] meaningfully)
124
+ if (!m[1] || !m[1].trim()) {
125
+ imports.push({ from, imported: null, local: null, line, confidence: 'EXTRACTED' });
126
+ seenFrom.add(key);
127
+ }
128
+ }
129
+
130
+ EXPORT_NAMED_RE.lastIndex = 0;
131
+ while ((m = EXPORT_NAMED_RE.exec(source))) {
132
+ exports.push({
133
+ name: m[1],
134
+ kind: 'named',
135
+ line: lineOf(source, m.index),
136
+ confidence: 'EXTRACTED',
137
+ });
138
+ }
139
+
140
+ EXPORT_LIST_RE.lastIndex = 0;
141
+ while ((m = EXPORT_LIST_RE.exec(source))) {
142
+ // skip re-exports that have `from` — handled as imports + export list
143
+ const after = source.slice(m.index, m.index + m[0].length + 40);
144
+ if (/\bfrom\b/.test(after.slice(m[0].length))) {
145
+ // still record exported names
146
+ }
147
+ for (const spec of parseSpecifiers(m[1])) {
148
+ exports.push({
149
+ name: spec.local,
150
+ kind: 'named',
151
+ line: lineOf(source, m.index),
152
+ confidence: 'EXTRACTED',
153
+ });
154
+ }
155
+ }
156
+
157
+ EXPORT_DEFAULT_RE.lastIndex = 0;
158
+ while ((m = EXPORT_DEFAULT_RE.exec(source))) {
159
+ exports.push({
160
+ name: m[1] || 'default',
161
+ kind: 'default',
162
+ line: lineOf(source, m.index),
163
+ confidence: 'EXTRACTED',
164
+ });
165
+ }
166
+
167
+ // Cross-file references only for imported locals (identifier use after import)
168
+ for (const [local, meta] of importedLocals) {
169
+ const useRe = new RegExp(`\\b${local}\\b`, 'g');
170
+ let um;
171
+ let count = 0;
172
+ while ((um = useRe.exec(source))) {
173
+ const line = lineOf(source, um.index);
174
+ if (line === meta.line) continue; // the import site itself
175
+ references.push({
176
+ name: local,
177
+ from: meta.from,
178
+ line,
179
+ confidence: 'EXTRACTED',
180
+ });
181
+ count += 1;
182
+ if (count >= 8) break; // cap per local
183
+ }
184
+ }
185
+
186
+ return { filePath, imports, exports, references };
187
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Regex-based shell script extractor (source/. includes, .sh calls, functions).
3
+ */
4
+
5
+ const SOURCE_RE =
6
+ /(?:^|[\n;&|])\s*(?:source|\.)[ \t]+(?:["']([^"']+)["']|(\S+))/gm;
7
+ const SH_PATH_RE = /(?:^|[\n;&|\s`"'])((?:\$[\w{]+\/)?(?:\.\/)?[\w./-]+\.sh)\b/gm;
8
+ const FN_RE = /^(?:function[ \t]+)?([A-Za-z_][\w]*)[ \t]*\(\)[ \t]*\{/gm;
9
+
10
+ function lineOf(text, index) {
11
+ return text.slice(0, index).split('\n').length;
12
+ }
13
+
14
+ function normalizeShellPath(raw) {
15
+ let p = String(raw || '').trim();
16
+ if (!p) return null;
17
+ // Drop variable prefixes like $SCRIPT_DIR/ or ${APP_DIR}/ — keep trailing path
18
+ p = p.replace(/^\$\{?[\w]+\}?\//, '');
19
+ p = p.replace(/^\.\//, '');
20
+ if (p.includes('$') || p.includes('`')) return null;
21
+ return p;
22
+ }
23
+
24
+ /**
25
+ * @param {string} filePath relative posix path
26
+ * @param {string} source
27
+ */
28
+ export function extractShell(filePath, source) {
29
+ const imports = [];
30
+ const exports = [];
31
+ const references = [];
32
+ const seen = new Set();
33
+
34
+ let m;
35
+ SOURCE_RE.lastIndex = 0;
36
+ while ((m = SOURCE_RE.exec(source))) {
37
+ const raw = m[1] || m[2];
38
+ const from = normalizeShellPath(raw);
39
+ if (!from) continue;
40
+ const line = lineOf(source, m.index);
41
+ const key = `src:${from}@${line}`;
42
+ if (seen.has(key)) continue;
43
+ seen.add(key);
44
+ imports.push({ from, imported: null, local: null, line, confidence: 'EXTRACTED' });
45
+ }
46
+
47
+ SH_PATH_RE.lastIndex = 0;
48
+ while ((m = SH_PATH_RE.exec(source))) {
49
+ const from = normalizeShellPath(m[1]);
50
+ if (!from || from === filePath) continue;
51
+ // Skip self basename echoes in comments loosely handled by seen
52
+ const line = lineOf(source, m.index);
53
+ const key = `sh:${from}@${line}`;
54
+ if (seen.has(key)) continue;
55
+ seen.add(key);
56
+ // Don't double-count pure source lines already captured
57
+ imports.push({ from, imported: null, local: null, line, confidence: 'INFERRED' });
58
+ }
59
+
60
+ FN_RE.lastIndex = 0;
61
+ while ((m = FN_RE.exec(source))) {
62
+ exports.push({
63
+ name: m[1],
64
+ kind: 'function',
65
+ line: lineOf(source, m.index),
66
+ confidence: 'EXTRACTED',
67
+ });
68
+ }
69
+
70
+ return { filePath, imports, exports, references };
71
+ }
@@ -0,0 +1,217 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { extractWithRegex } from './regex-fallback.mjs';
4
+
5
+ /**
6
+ * Try to load typescript from the target project (or cwd), then fall back to regex.
7
+ * @returns {{ mode: 'typescript'|'regex', extract: Function }}
8
+ */
9
+ export function createJsExtractor(root) {
10
+ const ts = tryLoadTypescript(root);
11
+ if (!ts) {
12
+ return {
13
+ mode: 'regex',
14
+ extract(filePath, source) {
15
+ return extractWithRegex(filePath, source);
16
+ },
17
+ };
18
+ }
19
+
20
+ return {
21
+ mode: 'typescript',
22
+ extract(filePath, source) {
23
+ try {
24
+ return extractWithTypescript(ts, filePath, source);
25
+ } catch {
26
+ return extractWithRegex(filePath, source);
27
+ }
28
+ },
29
+ };
30
+ }
31
+
32
+ /** @deprecated use createExtractor from ./create-extractor.mjs */
33
+ export function createExtractor(root) {
34
+ return createJsExtractor(root);
35
+ }
36
+
37
+ function tryLoadTypescript(root) {
38
+ const candidates = [
39
+ path.join(root, 'package.json'),
40
+ path.join(root, 'node_modules', 'typescript', 'package.json'),
41
+ ];
42
+ for (const c of candidates) {
43
+ try {
44
+ return createRequire(c)('typescript');
45
+ } catch {
46
+ // try next
47
+ }
48
+ }
49
+ try {
50
+ return createRequire(import.meta.url)('typescript');
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ function lineOf(ts, sourceFile, pos) {
57
+ return sourceFile.getLineAndCharacterOfPosition(pos).line + 1;
58
+ }
59
+
60
+ function extractWithTypescript(ts, filePath, source) {
61
+ const scriptKind = filePath.endsWith('x')
62
+ ? filePath.endsWith('.tsx')
63
+ ? ts.ScriptKind.TSX
64
+ : ts.ScriptKind.JSX
65
+ : filePath.endsWith('.ts')
66
+ ? ts.ScriptKind.TS
67
+ : ts.ScriptKind.JS;
68
+
69
+ const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, scriptKind);
70
+ const imports = [];
71
+ const exports = [];
72
+ const references = [];
73
+ const importedLocals = new Map();
74
+
75
+ for (const stmt of sf.statements) {
76
+ if (ts.isImportDeclaration(stmt) && stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier)) {
77
+ const from = stmt.moduleSpecifier.text;
78
+ const line = lineOf(ts, sf, stmt.getStart(sf));
79
+ const clause = stmt.importClause;
80
+ if (!clause) {
81
+ imports.push({ from, imported: null, local: null, line, confidence: 'EXTRACTED' });
82
+ continue;
83
+ }
84
+ if (clause.name) {
85
+ const local = clause.name.text;
86
+ imports.push({ from, imported: 'default', local, line, confidence: 'EXTRACTED' });
87
+ importedLocals.set(local, { from, line });
88
+ }
89
+ if (clause.namedBindings) {
90
+ if (ts.isNamespaceImport(clause.namedBindings)) {
91
+ const local = clause.namedBindings.name.text;
92
+ imports.push({ from, imported: '*', local, line, confidence: 'EXTRACTED' });
93
+ importedLocals.set(local, { from, line });
94
+ } else if (ts.isNamedImports(clause.namedBindings)) {
95
+ for (const el of clause.namedBindings.elements) {
96
+ const imported = el.propertyName ? el.propertyName.text : el.name.text;
97
+ const local = el.name.text;
98
+ imports.push({ from, imported, local, line, confidence: 'EXTRACTED' });
99
+ importedLocals.set(local, { from, line });
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ if (ts.isExportDeclaration(stmt)) {
106
+ const line = lineOf(ts, sf, stmt.getStart(sf));
107
+ if (stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier)) {
108
+ const from = stmt.moduleSpecifier.text;
109
+ if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
110
+ for (const el of stmt.exportClause.elements) {
111
+ const name = el.name.text;
112
+ imports.push({
113
+ from,
114
+ imported: el.propertyName ? el.propertyName.text : name,
115
+ local: name,
116
+ line,
117
+ confidence: 'EXTRACTED',
118
+ });
119
+ exports.push({ name, kind: 'named', line, confidence: 'EXTRACTED' });
120
+ }
121
+ } else {
122
+ imports.push({ from, imported: '*', local: null, line, confidence: 'EXTRACTED' });
123
+ }
124
+ } else if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
125
+ for (const el of stmt.exportClause.elements) {
126
+ exports.push({
127
+ name: el.name.text,
128
+ kind: 'named',
129
+ line,
130
+ confidence: 'EXTRACTED',
131
+ });
132
+ }
133
+ }
134
+ }
135
+
136
+ if (ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt) || ts.isEnumDeclaration(stmt)) {
137
+ if (stmt.name && hasExportModifier(ts, stmt)) {
138
+ exports.push({
139
+ name: stmt.name.text,
140
+ kind: 'named',
141
+ line: lineOf(ts, sf, stmt.getStart(sf)),
142
+ confidence: 'EXTRACTED',
143
+ });
144
+ }
145
+ }
146
+
147
+ if (ts.isVariableStatement(stmt) && hasExportModifier(ts, stmt)) {
148
+ for (const decl of stmt.declarationList.declarations) {
149
+ if (ts.isIdentifier(decl.name)) {
150
+ exports.push({
151
+ name: decl.name.text,
152
+ kind: 'named',
153
+ line: lineOf(ts, sf, decl.getStart(sf)),
154
+ confidence: 'EXTRACTED',
155
+ });
156
+ }
157
+ }
158
+ }
159
+
160
+ if (
161
+ (ts.isTypeAliasDeclaration(stmt) || ts.isInterfaceDeclaration(stmt)) &&
162
+ hasExportModifier(ts, stmt)
163
+ ) {
164
+ exports.push({
165
+ name: stmt.name.text,
166
+ kind: 'named',
167
+ line: lineOf(ts, sf, stmt.getStart(sf)),
168
+ confidence: 'EXTRACTED',
169
+ });
170
+ }
171
+
172
+ if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
173
+ let name = 'default';
174
+ if (ts.isIdentifier(stmt.expression)) name = stmt.expression.text;
175
+ exports.push({
176
+ name,
177
+ kind: 'default',
178
+ line: lineOf(ts, sf, stmt.getStart(sf)),
179
+ confidence: 'EXTRACTED',
180
+ });
181
+ }
182
+ }
183
+
184
+ // Walk identifiers for imported local references
185
+ function visit(node) {
186
+ if (ts.isIdentifier(node)) {
187
+ const name = node.text;
188
+ const meta = importedLocals.get(name);
189
+ if (meta) {
190
+ const line = lineOf(ts, sf, node.getStart(sf));
191
+ if (line !== meta.line) {
192
+ references.push({ name, from: meta.from, line, confidence: 'EXTRACTED' });
193
+ }
194
+ }
195
+ }
196
+ ts.forEachChild(node, visit);
197
+ }
198
+ visit(sf);
199
+
200
+ // Cap references per local
201
+ const capped = [];
202
+ const counts = new Map();
203
+ for (const ref of references) {
204
+ const n = counts.get(ref.name) || 0;
205
+ if (n >= 8) continue;
206
+ counts.set(ref.name, n + 1);
207
+ capped.push(ref);
208
+ }
209
+
210
+ return { filePath, imports, exports, references: capped };
211
+ }
212
+
213
+ function hasExportModifier(ts, node) {
214
+ return !!node.modifiers?.some(
215
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword || m.kind === ts.SyntaxKind.DefaultKeyword,
216
+ );
217
+ }