@laitszkin/apollo-toolkit 4.0.11 → 4.1.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 (134) hide show
  1. package/AGENTS.md +37 -27
  2. package/CHANGELOG.md +31 -0
  3. package/CLAUDE.md +37 -27
  4. package/README.md +15 -2
  5. package/assets/spec/rg13-1780435029246/test-questions.json +1 -0
  6. package/assets/spec/rg13-1780468345132/test-questions.json +1 -0
  7. package/package.json +3 -3
  8. package/packages/cli/dist/tool-registration.js +1 -0
  9. package/packages/cli/dist/tsconfig.tsbuildinfo +1 -1
  10. package/packages/cli/tool-registration.ts +1 -0
  11. package/packages/tools/architecture/dist/index.js +539 -2
  12. package/packages/tools/architecture/dist/index.test.d.ts +1 -0
  13. package/packages/tools/architecture/dist/index.test.js +229 -0
  14. package/packages/tools/architecture/dist/tsconfig.tsbuildinfo +1 -1
  15. package/packages/tools/architecture/index.test.ts +329 -0
  16. package/packages/tools/architecture/index.ts +607 -5
  17. package/packages/tools/codegraph/dist/index.d.ts +3 -0
  18. package/packages/tools/codegraph/dist/index.js +157 -0
  19. package/packages/tools/codegraph/dist/lib/cg-instance.d.ts +29 -0
  20. package/packages/tools/codegraph/dist/lib/cg-instance.js +59 -0
  21. package/packages/tools/codegraph/dist/lib/cg-instance.test.d.ts +1 -0
  22. package/packages/tools/codegraph/dist/lib/cg-instance.test.js +27 -0
  23. package/packages/tools/codegraph/dist/lib/cmd-explore.d.ts +5 -0
  24. package/packages/tools/codegraph/dist/lib/cmd-explore.js +95 -0
  25. package/packages/tools/codegraph/dist/lib/cmd-explore.test.d.ts +1 -0
  26. package/packages/tools/codegraph/dist/lib/cmd-explore.test.js +133 -0
  27. package/packages/tools/codegraph/dist/lib/cmd-flag-splice.test.d.ts +1 -0
  28. package/packages/tools/codegraph/dist/lib/cmd-flag-splice.test.js +83 -0
  29. package/packages/tools/codegraph/dist/lib/cmd-init.d.ts +5 -0
  30. package/packages/tools/codegraph/dist/lib/cmd-init.js +50 -0
  31. package/packages/tools/codegraph/dist/lib/cmd-init.test.d.ts +1 -0
  32. package/packages/tools/codegraph/dist/lib/cmd-init.test.js +51 -0
  33. package/packages/tools/codegraph/dist/lib/cmd-list-apis.d.ts +5 -0
  34. package/packages/tools/codegraph/dist/lib/cmd-list-apis.js +64 -0
  35. package/packages/tools/codegraph/dist/lib/cmd-list-apis.test.d.ts +1 -0
  36. package/packages/tools/codegraph/dist/lib/cmd-list-apis.test.js +69 -0
  37. package/packages/tools/codegraph/dist/lib/cmd-search.d.ts +5 -0
  38. package/packages/tools/codegraph/dist/lib/cmd-search.js +21 -0
  39. package/packages/tools/codegraph/dist/lib/cmd-search.test.d.ts +1 -0
  40. package/packages/tools/codegraph/dist/lib/cmd-search.test.js +30 -0
  41. package/packages/tools/codegraph/dist/lib/cmd-status.d.ts +4 -0
  42. package/packages/tools/codegraph/dist/lib/cmd-status.js +44 -0
  43. package/packages/tools/codegraph/dist/lib/cmd-status.test.d.ts +1 -0
  44. package/packages/tools/codegraph/dist/lib/cmd-status.test.js +72 -0
  45. package/packages/tools/codegraph/dist/lib/cmd-survey.d.ts +36 -0
  46. package/packages/tools/codegraph/dist/lib/cmd-survey.js +142 -0
  47. package/packages/tools/codegraph/dist/lib/cmd-survey.test.d.ts +1 -0
  48. package/packages/tools/codegraph/dist/lib/cmd-survey.test.js +136 -0
  49. package/packages/tools/codegraph/dist/lib/cmd-sync.d.ts +4 -0
  50. package/packages/tools/codegraph/dist/lib/cmd-sync.js +51 -0
  51. package/packages/tools/codegraph/dist/lib/cmd-sync.test.d.ts +1 -0
  52. package/packages/tools/codegraph/dist/lib/cmd-sync.test.js +30 -0
  53. package/packages/tools/codegraph/dist/lib/cmd-verify.d.ts +4 -0
  54. package/packages/tools/codegraph/dist/lib/cmd-verify.js +134 -0
  55. package/packages/tools/codegraph/dist/lib/cmd-verify.test.d.ts +1 -0
  56. package/packages/tools/codegraph/dist/lib/cmd-verify.test.js +139 -0
  57. package/packages/tools/codegraph/dist/lib/formatter.d.ts +67 -0
  58. package/packages/tools/codegraph/dist/lib/formatter.js +107 -0
  59. package/packages/tools/codegraph/dist/lib/formatter.test.d.ts +1 -0
  60. package/packages/tools/codegraph/dist/lib/formatter.test.js +41 -0
  61. package/packages/tools/codegraph/dist/lib/survey/grouper.d.ts +19 -0
  62. package/packages/tools/codegraph/dist/lib/survey/grouper.js +194 -0
  63. package/packages/tools/codegraph/dist/lib/survey/grouper.test.d.ts +1 -0
  64. package/packages/tools/codegraph/dist/lib/survey/grouper.test.js +62 -0
  65. package/packages/tools/codegraph/dist/lib/survey/scanner.d.ts +31 -0
  66. package/packages/tools/codegraph/dist/lib/survey/scanner.js +50 -0
  67. package/packages/tools/codegraph/dist/lib/verify/checker.d.ts +32 -0
  68. package/packages/tools/codegraph/dist/lib/verify/checker.js +146 -0
  69. package/packages/tools/codegraph/dist/lib/verify/checker.test.d.ts +1 -0
  70. package/packages/tools/codegraph/dist/lib/verify/checker.test.js +128 -0
  71. package/packages/tools/codegraph/dist/tsconfig.tsbuildinfo +1 -0
  72. package/packages/tools/codegraph/env.d.ts +56 -0
  73. package/packages/tools/codegraph/index.ts +173 -0
  74. package/packages/tools/codegraph/lib/cg-instance.test.ts +36 -0
  75. package/packages/tools/codegraph/lib/cg-instance.ts +66 -0
  76. package/packages/tools/codegraph/lib/cmd-explore.test.ts +195 -0
  77. package/packages/tools/codegraph/lib/cmd-explore.ts +129 -0
  78. package/packages/tools/codegraph/lib/cmd-flag-splice.test.ts +94 -0
  79. package/packages/tools/codegraph/lib/cmd-init.test.ts +68 -0
  80. package/packages/tools/codegraph/lib/cmd-init.ts +60 -0
  81. package/packages/tools/codegraph/lib/cmd-list-apis.test.ts +80 -0
  82. package/packages/tools/codegraph/lib/cmd-list-apis.ts +90 -0
  83. package/packages/tools/codegraph/lib/cmd-search.test.ts +37 -0
  84. package/packages/tools/codegraph/lib/cmd-search.ts +32 -0
  85. package/packages/tools/codegraph/lib/cmd-status.test.ts +86 -0
  86. package/packages/tools/codegraph/lib/cmd-status.ts +53 -0
  87. package/packages/tools/codegraph/lib/cmd-survey.test.ts +161 -0
  88. package/packages/tools/codegraph/lib/cmd-survey.ts +199 -0
  89. package/packages/tools/codegraph/lib/cmd-sync.test.ts +41 -0
  90. package/packages/tools/codegraph/lib/cmd-sync.ts +62 -0
  91. package/packages/tools/codegraph/lib/cmd-verify.test.ts +162 -0
  92. package/packages/tools/codegraph/lib/cmd-verify.ts +145 -0
  93. package/packages/tools/codegraph/lib/formatter.test.ts +47 -0
  94. package/packages/tools/codegraph/lib/formatter.ts +130 -0
  95. package/packages/tools/codegraph/lib/survey/grouper.test.ts +72 -0
  96. package/packages/tools/codegraph/lib/survey/grouper.ts +226 -0
  97. package/packages/tools/codegraph/lib/survey/scanner.ts +89 -0
  98. package/packages/tools/codegraph/lib/verify/checker.test.ts +140 -0
  99. package/packages/tools/codegraph/lib/verify/checker.ts +172 -0
  100. package/packages/tools/codegraph/package.json +23 -0
  101. package/packages/tools/codegraph/tsconfig.json +22 -0
  102. package/resources/project-architecture/atlas/atlas.history.log +32 -0
  103. package/resources/project-architecture/atlas/atlas.history.undo.json +356 -28
  104. package/resources/project-architecture/atlas/atlas.history.undo.stack.json +14350 -0
  105. package/resources/project-architecture/atlas/atlas.index.yaml +76 -12
  106. package/resources/project-architecture/atlas/features/codegraph.yaml +95 -0
  107. package/resources/project-architecture/atlas/features/eval-ci-gate.yaml +6 -1
  108. package/resources/project-architecture/atlas/features/eval-cli.yaml +16 -1
  109. package/resources/project-architecture/atlas/features/eval-executor.yaml +12 -2
  110. package/resources/project-architecture/atlas/features/eval-isolation.yaml +6 -1
  111. package/resources/project-architecture/atlas/features/eval-optimizer.yaml +17 -2
  112. package/resources/project-architecture/atlas/features/eval-question.yaml +12 -2
  113. package/resources/project-architecture/atlas/features/eval-reporter.yaml +6 -1
  114. package/resources/project-architecture/atlas/features/eval-scorer.yaml +12 -2
  115. package/resources/project-architecture/features/codegraph/cg-discovery.html +47 -0
  116. package/resources/project-architecture/features/codegraph/cg-lifecycle.html +48 -0
  117. package/resources/project-architecture/features/codegraph/cg-validation.html +47 -0
  118. package/resources/project-architecture/features/codegraph/index.html +58 -0
  119. package/resources/project-architecture/features/eval-ci-gate/workflow-trigger.html +6 -1
  120. package/resources/project-architecture/features/eval-cli/cli-handler.html +8 -1
  121. package/resources/project-architecture/features/eval-executor/exec-api-client.html +6 -1
  122. package/resources/project-architecture/features/eval-executor/trace-recorder.html +6 -1
  123. package/resources/project-architecture/features/eval-isolation/tool-dispatcher.html +6 -1
  124. package/resources/project-architecture/features/eval-optimizer/dedup-engine.html +6 -1
  125. package/resources/project-architecture/features/eval-optimizer/issue-extractor.html +7 -1
  126. package/resources/project-architecture/features/eval-question/question-loader.html +6 -1
  127. package/resources/project-architecture/features/eval-question/variant-generator.html +6 -1
  128. package/resources/project-architecture/features/eval-reporter/report-composer.html +6 -1
  129. package/resources/project-architecture/features/eval-scorer/judge-api-client.html +6 -1
  130. package/resources/project-architecture/features/eval-scorer/judge-prompt-builder.html +6 -1
  131. package/resources/project-architecture/index.html +200 -94
  132. package/skills/design/SKILL.md +33 -0
  133. package/skills/init-project-html/SKILL.md +12 -11
  134. package/tsconfig.json +1 -0
@@ -1,13 +1,613 @@
1
1
  import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
2
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
3
5
  import type { ToolDefinition, ToolContext } from '@laitszkin/tool-registry';
6
+ import yaml from 'js-yaml';
4
7
 
5
- export async function architectureHandler(args: string[], context: ToolContext): Promise<number> {
6
- const sourceRoot = context.sourceRoot || path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
8
+ // ── Apply & Template helpers (mirrors cli.js internals for the new verbs) ─────
9
+
10
+ function findFeature(state: any, slug: string): any {
11
+ return (state.features || []).find((f: any) => f.slug === slug);
12
+ }
13
+
14
+ function findSubmodule(feature: any, slug: string): any {
15
+ return ((feature && feature.submodules) || []).find((s: any) => s.slug === slug);
16
+ }
17
+
18
+ function ensureFeature(state: any, slug: string, init?: Record<string, unknown>): any {
19
+ let feature = findFeature(state, slug);
20
+ if (!feature) {
21
+ feature = { slug, title: slug, story: '', dependsOn: [], submodules: [], edges: [], ...init };
22
+ state.features = state.features || [];
23
+ state.features.push(feature);
24
+ } else if (init) {
25
+ Object.assign(feature, init);
26
+ }
27
+ return feature;
28
+ }
29
+
30
+ function removeFeature(state: any, slug: string): boolean {
31
+ if (!state.features) return false;
32
+ const before = state.features.length;
33
+ state.features = state.features.filter((f: any) => f.slug !== slug);
34
+ state.edges = (state.edges || []).filter(
35
+ (e: any) => !endpointReferences(e.from, slug) && !endpointReferences(e.to, slug),
36
+ );
37
+ return state.features.length < before;
38
+ }
39
+
40
+ function endpointReferences(endpoint: any, slug: string): boolean {
41
+ if (!endpoint || typeof endpoint === 'string') return false;
42
+ return endpoint.feature === slug;
43
+ }
44
+
45
+ function ensureSubmodule(feature: any, slug: string, init?: Record<string, unknown>): any {
46
+ let sub = findSubmodule(feature, slug);
47
+ if (!sub) {
48
+ sub = { slug, kind: 'service', role: '', functions: [], variables: [], dataflow: [], errors: [], ...init };
49
+ feature.submodules = feature.submodules || [];
50
+ feature.submodules.push(sub);
51
+ } else if (init) {
52
+ Object.assign(sub, init);
53
+ }
54
+ return sub;
55
+ }
56
+
57
+ function removeSubmodule(feature: any, slug: string, merged?: any): boolean {
58
+ if (!feature.submodules) return false;
59
+ const before = feature.submodules.length;
60
+ feature.submodules = feature.submodules.filter((s: any) => s.slug !== slug);
61
+ feature.edges = (feature.edges || []).filter((e: any) => {
62
+ const f = typeof e.from === 'string' ? e.from : e.from?.submodule;
63
+ const t = typeof e.to === 'string' ? e.to : e.to?.submodule;
64
+ return f !== slug && t !== slug;
65
+ });
66
+ if (merged) {
67
+ merged.edges = (merged.edges || []).filter((e: any) => {
68
+ const fromEp = typeof e.from === 'object' && e.from;
69
+ const toEp = typeof e.to === 'object' && e.to;
70
+ const fromMatch = fromEp && fromEp.feature === feature.slug && fromEp.submodule === slug;
71
+ const toMatch = toEp && toEp.feature === feature.slug && toEp.submodule === slug;
72
+ return !fromMatch && !toMatch;
73
+ });
74
+ }
75
+ return feature.submodules.length < before;
76
+ }
77
+
78
+ function parseEndpoint(value: string): { feature: string; submodule?: string } {
79
+ const parts = value.split('/').filter(Boolean);
80
+ if (parts.length === 0) throw new Error(`Invalid endpoint: "${value}"`);
81
+ return parts.length > 1
82
+ ? { feature: parts[0], submodule: parts[1] }
83
+ : { feature: parts[0] };
84
+ }
85
+
86
+ function isIntraFeatureEdge(from: any, to: any): boolean {
87
+ return from?.feature && to?.feature && from.feature === to.feature && from.submodule && to.submodule;
88
+ }
89
+
90
+ function endpointEquals(a: any, b: any): boolean {
91
+ if (typeof a === 'string' || typeof b === 'string') return false;
92
+ if (!a || !b) return false;
93
+ return a.feature === b.feature && (a.submodule ?? null) === (b.submodule ?? null);
94
+ }
95
+
96
+ function toSlug(text: string): string {
97
+ return text
98
+ .toLowerCase()
99
+ .replace(/[^a-z0-9À-ɏ一-鿿]+/g, '-')
100
+ .replace(/-+/g, '-')
101
+ .replace(/^-|-$/g, '')
102
+ .substring(0, 64) || 'feature';
103
+ }
104
+
105
+ function parseSpecMetadata(filePath: string): { title: string; goal: string } {
106
+ const content = fs.readFileSync(filePath, 'utf8');
107
+ const lines = content.split('\n');
108
+
109
+ let title = '';
110
+ let inGoal = false;
111
+ const goalLines: string[] = [];
112
+
113
+ for (const line of lines) {
114
+ if (!title && line.startsWith('# ') && !line.startsWith('## ')) {
115
+ title = line.replace(/^#\s+/, '').replace(/^Spec:\s*/i, '').trim();
116
+ }
117
+ if (line.startsWith('## Goal')) {
118
+ inGoal = true;
119
+ continue;
120
+ }
121
+ if (inGoal) {
122
+ if (line.startsWith('## ')) break;
123
+ goalLines.push(line);
124
+ }
125
+ }
126
+
127
+ const goal = goalLines
128
+ .map((l) => l.trim())
129
+ .filter(Boolean)
130
+ .join(' ')
131
+ .replace(/\s+/g, ' ')
132
+ .trim();
133
+
134
+ return { title, goal };
135
+ }
136
+
137
+ function yamlStr(value: string): string {
138
+ if (/["\n]/.test(value)) {
139
+ return `'${value.replace(/'/g, "''")}'`;
140
+ }
141
+ return `"${value}"`;
142
+ }
143
+
144
+ // ── apply ────────────────────────────────────────────────────────────────────
145
+
146
+ async function handleApply(applyArgs: string[], context: ToolContext): Promise<number> {
147
+ const stdout = context.stdout || process.stdout;
148
+ const stderr = context.stderr || process.stderr;
149
+ const sourceRoot =
150
+ context.sourceRoot ||
151
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
152
+
153
+ const yamlArg = applyArgs[0];
154
+ if (!yamlArg || yamlArg.startsWith('--')) {
155
+ stderr.write(
156
+ 'Usage: apltk architecture apply <yaml-file> [--spec <dir>] [--project <root>] [--no-render]\n',
157
+ );
158
+ return 1;
159
+ }
160
+
161
+ // Simple flag parser for trailing flags (--spec, --project, --no-render)
162
+ const rest = applyArgs.slice(1);
163
+ const flags: Record<string, any> = {};
164
+ for (let i = 0; i < rest.length; i++) {
165
+ const a = rest[i];
166
+ if (a === '--no-render') flags['no-render'] = true;
167
+ else if (a === '--spec' && i + 1 < rest.length) flags.spec = rest[++i];
168
+ else if (a === '--project' && i + 1 < rest.length) flags.project = rest[++i];
169
+ }
170
+
171
+ // Parse YAML
172
+ let batch: any;
173
+ try {
174
+ const yamlPath = path.resolve(yamlArg);
175
+ const raw = fs.readFileSync(yamlPath, 'utf8');
176
+ batch = yaml.load(raw);
177
+ } catch (e: any) {
178
+ const location = e.mark ? ` at line ${e.mark.line + 1}` : '';
179
+ stderr.write(`Error parsing apply YAML (${yamlArg})${location}: ${e.message}\n`);
180
+ return 1;
181
+ }
182
+
183
+ if (!batch || typeof batch !== 'object') {
184
+ stderr.write('Invalid apply YAML: expected an object with "features" / "edges" keys.\n');
185
+ return 1;
186
+ }
187
+
188
+ // Import atlas modules (shared with the existing JS CLI)
189
+ const cliPath = path.join(
190
+ sourceRoot,
191
+ 'skills',
192
+ 'init-project-html',
193
+ 'lib',
194
+ 'atlas',
195
+ 'cli.js',
196
+ );
197
+ const statePath = path.join(
198
+ sourceRoot,
199
+ 'skills',
200
+ 'init-project-html',
201
+ 'lib',
202
+ 'atlas',
203
+ 'state.js',
204
+ );
205
+
206
+ const [cliMod, stateMod] = await Promise.all([
207
+ import(pathToFileURL(cliPath).href),
208
+ import(pathToFileURL(statePath).href),
209
+ ]);
210
+ const cli: any = cliMod.default;
211
+ const stateLib: any = stateMod.default;
212
+
213
+ // Resolve project root
214
+ let projectRoot: string;
215
+ try {
216
+ projectRoot = cli.resolveProjectRoot(flags);
217
+ } catch (e: any) {
218
+ stderr.write(`${e.message}\n`);
219
+ return 1;
220
+ }
221
+
222
+ const isSpec = Boolean(flags.spec);
223
+ const atlasDir = cli.baseAtlasDir(projectRoot);
224
+ let merged: any;
225
+ let overlayDir: string | null = null;
226
+ let preOverlayBase: any;
227
+
228
+ if (isSpec) {
229
+ const sov = cli.specOverlayDir(projectRoot, flags.spec);
230
+ overlayDir = sov.overlayDir;
231
+ preOverlayBase = stateLib.load(atlasDir); // snapshot before overlay merge
232
+ const overlay = stateLib.loadOverlay(overlayDir);
233
+ merged = stateLib.mergeOverlay(preOverlayBase, overlay);
234
+ } else {
235
+ merged = JSON.parse(JSON.stringify(stateLib.load(atlasDir)));
236
+ }
237
+
238
+ // ── Process mutations on the in-memory merged state ──
239
+ // All processing happens on the deep-clone; disk is not touched until
240
+ // every step succeeds. If anything throws, the batch is aborted and
241
+ // nothing is persisted.
242
+
243
+ try {
244
+ // 1) Features (add / modify / remove)
245
+ for (const feat of batch.features || []) {
246
+ const slug: string = feat.slug;
247
+ if (!slug) throw new Error('"features" entry missing required "slug" field');
248
+
249
+ switch (feat.action) {
250
+ case 'add': {
251
+ const init: Record<string, unknown> = {};
252
+ if (feat.title !== undefined) init.title = String(feat.title);
253
+ if (feat.story !== undefined) init.story = String(feat.story);
254
+ if (feat.dependsOn !== undefined)
255
+ init.dependsOn = Array.isArray(feat.dependsOn) ? feat.dependsOn : [feat.dependsOn];
256
+ ensureFeature(merged, slug, init);
257
+ break;
258
+ }
259
+ case 'modify': {
260
+ const existing = findFeature(merged, slug);
261
+ if (!existing) throw new Error(`feature "${slug}" not found for action "modify"`);
262
+ if (feat.title !== undefined) existing.title = String(feat.title);
263
+ if (feat.story !== undefined) existing.story = String(feat.story);
264
+ if (feat.dependsOn !== undefined)
265
+ existing.dependsOn = Array.isArray(feat.dependsOn) ? feat.dependsOn : [feat.dependsOn];
266
+ break;
267
+ }
268
+ case 'remove':
269
+ removeFeature(merged, slug);
270
+ break;
271
+ default:
272
+ throw new Error(`feature "${slug}": unknown action "${feat.action}"`);
273
+ }
274
+ }
275
+
276
+ // 2) Submodules (add / remove) — skip features that were removed
277
+ for (const feat of batch.features || []) {
278
+ if (feat.action === 'remove') continue;
279
+ const parent = findFeature(merged, feat.slug);
280
+ if (!parent) throw new Error(`feature "${feat.slug}" not found for submodule operations`);
281
+ for (const sub of feat.submodules || []) {
282
+ switch (sub.action) {
283
+ case 'add': {
284
+ const init: Record<string, unknown> = {};
285
+ if (sub.kind !== undefined) init.kind = String(sub.kind);
286
+ if (sub.role !== undefined) init.role = String(sub.role);
287
+ ensureSubmodule(parent, sub.slug, init);
288
+ break;
289
+ }
290
+ case 'remove':
291
+ removeSubmodule(parent, sub.slug, merged);
292
+ break;
293
+ default:
294
+ throw new Error(
295
+ `submodule "${feat.slug}/${sub.slug}": unknown action "${sub.action}"`,
296
+ );
297
+ }
298
+ }
299
+ }
300
+
301
+ // 3) Functions (add / remove) — skip removed features & submodules
302
+ for (const feat of batch.features || []) {
303
+ if (feat.action === 'remove') continue;
304
+ const parent = findFeature(merged, feat.slug);
305
+ if (!parent) continue;
306
+ for (const sub of feat.submodules || []) {
307
+ if (sub.action === 'remove') continue;
308
+ const subMod = findSubmodule(parent, sub.slug);
309
+ if (!subMod)
310
+ throw new Error(`submodule "${feat.slug}/${sub.slug}" not found for function operations`);
311
+ for (const fn of sub.functions || []) {
312
+ switch (fn.action) {
313
+ case 'add': {
314
+ subMod.functions = (subMod.functions || []).filter(
315
+ (f: any) => f.name !== fn.name,
316
+ );
317
+ const newFn: Record<string, unknown> = { name: fn.name };
318
+ if (fn.in !== undefined) newFn.in = String(fn.in);
319
+ if (fn.out !== undefined) newFn.out = String(fn.out);
320
+ if (fn.side !== undefined) newFn.side = String(fn.side);
321
+ if (fn.purpose !== undefined) newFn.purpose = String(fn.purpose);
322
+ subMod.functions.push(newFn);
323
+ break;
324
+ }
325
+ case 'remove':
326
+ subMod.functions = (subMod.functions || []).filter(
327
+ (f: any) => f.name !== fn.name,
328
+ );
329
+ break;
330
+ default:
331
+ throw new Error(
332
+ `function "${feat.slug}/${sub.slug}/${fn.name}": unknown action "${fn.action}"`,
333
+ );
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ // 4) Edges (add / remove)
340
+ for (const edge of batch.edges || []) {
341
+ let from: { feature: string; submodule?: string };
342
+ let to: { feature: string; submodule?: string };
343
+ try {
344
+ from = parseEndpoint(edge.from);
345
+ to = parseEndpoint(edge.to);
346
+ } catch (er: any) {
347
+ throw new Error(`edge: ${er.message}`);
348
+ }
349
+
350
+ switch (edge.action) {
351
+ case 'add': {
352
+ // Referential integrity validation
353
+ const fromFeature = findFeature(merged, from.feature);
354
+ if (!fromFeature) {
355
+ throw new Error(
356
+ `edge "${edge.from} → ${edge.to}": source feature "${from.feature}" not found`,
357
+ );
358
+ }
359
+ if (from.submodule) {
360
+ const fromSub = findSubmodule(fromFeature, from.submodule);
361
+ if (!fromSub) {
362
+ throw new Error(
363
+ `edge "${edge.from} → ${edge.to}": source submodule "${from.submodule}" not found in feature "${from.feature}"`,
364
+ );
365
+ }
366
+ }
367
+ const toFeature = findFeature(merged, to.feature);
368
+ if (!toFeature) {
369
+ throw new Error(
370
+ `edge "${edge.from} → ${edge.to}": target feature "${to.feature}" not found`,
371
+ );
372
+ }
373
+ if (to.submodule) {
374
+ const toSub = findSubmodule(toFeature, to.submodule);
375
+ if (!toSub) {
376
+ throw new Error(
377
+ `edge "${edge.from} → ${edge.to}": target submodule "${to.submodule}" not found in feature "${to.feature}"`,
378
+ );
379
+ }
380
+ }
381
+
382
+ const eid = edge.id || `e-${Math.random().toString(36).slice(2, 8)}`;
383
+ const kind = edge.kind || 'call';
384
+ const label = edge.label !== undefined ? String(edge.label) : '';
385
+
386
+ if (isIntraFeatureEdge(from, to)) {
387
+ const feature = findFeature(merged, from.feature);
388
+ feature.edges = (feature.edges || []).filter((ex: any) => ex.id !== eid);
389
+ feature.edges.push({
390
+ id: eid,
391
+ from: from.submodule,
392
+ to: to.submodule,
393
+ kind,
394
+ label,
395
+ });
396
+ } else {
397
+ merged.edges = (merged.edges || []).filter((ex: any) => ex.id !== eid);
398
+ merged.edges.push({ id: eid, from, to, kind, label });
399
+ }
400
+ break;
401
+ }
402
+ case 'remove': {
403
+ const byId = edge.id ? (ex: any) => ex.id === edge.id : null;
404
+ if (isIntraFeatureEdge(from, to)) {
405
+ const feature = findFeature(merged, from.feature);
406
+ if (feature) {
407
+ feature.edges = (feature.edges || []).filter((ex: any) => {
408
+ if (byId && ex.id === edge.id) return false;
409
+ const ef = typeof ex.from === 'string' ? ex.from : ex.from?.submodule;
410
+ const et = typeof ex.to === 'string' ? ex.to : ex.to?.submodule;
411
+ return !(ef === from.submodule && et === to.submodule);
412
+ });
413
+ }
414
+ } else {
415
+ merged.edges = (merged.edges || []).filter((ex: any) => {
416
+ if (byId && ex.id === edge.id) return false;
417
+ return !(endpointEquals(ex.from, from) && endpointEquals(ex.to, to));
418
+ });
419
+ }
420
+ break;
421
+ }
422
+ default:
423
+ throw new Error(`edge "${edge.from} → ${edge.to}": unknown action "${edge.action}"`);
424
+ }
425
+ }
426
+ } catch (e: any) {
427
+ stderr.write(`Batch aborted: ${e.message}\n`);
428
+ return 1;
429
+ }
430
+
431
+ // ── All mutations succeeded — persist ──
432
+ const saveDir = isSpec ? overlayDir! : atlasDir;
433
+
434
+ // Write undo snapshot **before** committing, so undo goes back to the pre-apply state.
435
+ if (isSpec) {
436
+ const freshBase = stateLib.load(atlasDir);
437
+ stateLib.writeUndoSnapshot(saveDir, {
438
+ base: freshBase,
439
+ overlay: stateLib.loadOverlay(overlayDir!),
440
+ });
441
+ stateLib.saveOverlay(saveDir, stateLib.deriveOverlay(freshBase, merged));
442
+ stateLib.appendHistory(saveDir, {
443
+ action: 'apply',
444
+ args: { yaml: yamlArg },
445
+ mode: 'spec',
446
+ });
447
+ } else {
448
+ stateLib.writeUndoSnapshot(saveDir, { base: stateLib.load(atlasDir) });
449
+ stateLib.save(saveDir, merged);
450
+ stateLib.appendHistory(saveDir, {
451
+ action: 'apply',
452
+ args: { yaml: yamlArg },
453
+ mode: 'base',
454
+ });
455
+ }
456
+
457
+ stdout.write(
458
+ `atlas: apply applied — ${(batch.features || []).length} feature(s), ${(batch.edges || []).length} edge(s)\n`,
459
+ );
460
+
461
+ // Auto-render
462
+ if (!flags['no-render']) {
463
+ const renderFlags = isSpec ? { spec: flags.spec } : {};
464
+ await cli.runRender({
465
+ projectRoot,
466
+ flags: renderFlags,
467
+ preloadedMerged: merged,
468
+ preloadedBase: isSpec ? preOverlayBase : undefined,
469
+ });
470
+ }
471
+
472
+ return 0;
473
+ }
474
+
475
+ // ── template ─────────────────────────────────────────────────────────────────
476
+
477
+ async function handleTemplate(templateArgs: string[], context: ToolContext): Promise<number> {
478
+ const stdout = context.stdout || process.stdout;
479
+ const stderr = context.stderr || process.stderr;
480
+
481
+ // Parse --spec <dir> --output <dir>
482
+ let specDir: string | undefined;
483
+ let outputDir: string | undefined;
484
+ for (let i = 0; i < templateArgs.length; i++) {
485
+ const a = templateArgs[i];
486
+ if (a === '--spec' && i + 1 < templateArgs.length) specDir = templateArgs[++i];
487
+ else if (a === '--output' && i + 1 < templateArgs.length) outputDir = templateArgs[++i];
488
+ }
489
+
490
+ if (!specDir || !outputDir) {
491
+ stderr.write('Usage: apltk architecture template --spec <spec-dir> --output <output-dir>\n');
492
+ return 1;
493
+ }
494
+
495
+ const specPath = path.resolve(specDir, 'SPEC.md');
496
+ const outputDirPath = path.resolve(outputDir);
497
+ const outputPath = path.join(outputDirPath, 'proposal.yaml');
498
+
499
+ // Extract spec metadata (title, goal) from SPEC.md
500
+ let featureSlug = 'feature';
501
+ let featureTitle = 'Feature';
502
+ let goal = '';
503
+
504
+ if (fs.existsSync(specPath)) {
505
+ const meta = parseSpecMetadata(specPath);
506
+ if (meta.title) {
507
+ featureTitle = meta.title;
508
+ featureSlug = toSlug(featureTitle);
509
+ }
510
+ if (meta.goal) {
511
+ goal = meta.goal;
512
+ }
513
+ } else {
514
+ const resolvedSpecDir = path.resolve(specDir);
515
+ if (!fs.existsSync(resolvedSpecDir)) {
516
+ stderr.write(`Spec directory not found: ${resolvedSpecDir}\n`);
517
+ } else {
518
+ const mdFiles = fs.readdirSync(resolvedSpecDir).filter((f: string) => f.endsWith('.md'));
519
+ if (mdFiles.length > 0) {
520
+ stderr.write(`Spec directory found but no SPEC.md. Found: ${mdFiles.join(', ')}\n`);
521
+ } else {
522
+ stderr.write(`Spec directory found but no SPEC.md. No .md files found.\n`);
523
+ }
524
+ }
525
+ return 1;
526
+ }
527
+
528
+ // Build proposal.yaml content
529
+ const lines: string[] = [
530
+ '# proposal.yaml — generated by `apltk architecture template`',
531
+ '# Fill in the sections below to describe the architecture proposal.',
532
+ '',
533
+ 'features:',
534
+ ` - slug: ${featureSlug}`,
535
+ ` title: ${yamlStr(featureTitle)}`,
536
+ ' action: add',
537
+ ];
538
+ if (goal) {
539
+ lines.push(` story: ${yamlStr(goal)}`);
540
+ }
541
+ lines.push(
542
+ ' submodules: [] # LLM: fill in submodule entries',
543
+ '',
544
+ '# Cross-feature edges (leave empty for single-feature proposals)',
545
+ 'edges: [] # LLM: fill in edge entries',
546
+ '',
547
+ );
548
+
549
+ try {
550
+ fs.mkdirSync(outputDirPath, { recursive: true });
551
+ fs.writeFileSync(outputPath, lines.join('\n'), 'utf8');
552
+ stdout.write(`${outputPath}\n`);
553
+ } catch (e: any) {
554
+ stderr.write(`Error writing proposal.yaml: ${e.message}\n`);
555
+ return 1;
556
+ }
557
+
558
+ // Try to enrich with CodeGraph API listing
559
+ try {
560
+ const cgRequire = createRequire(import.meta.url);
561
+ const { CodeGraph } = cgRequire('@colbymchenry/codegraph');
562
+ const projectRoot = process.cwd();
563
+ if (CodeGraph.isInitialized(projectRoot)) {
564
+ const cg = await CodeGraph.open(projectRoot, { sync: false, readOnly: true });
565
+ const nodes = cg.getNodesByKind('function');
566
+ const apiLines: string[] = [
567
+ '',
568
+ '# CodeGraph API index found — detected APIs (up to 50):',
569
+ ];
570
+ for (let i = 0; i < Math.min(nodes.length, 50); i++) {
571
+ const n = nodes[i];
572
+ apiLines.push(
573
+ `# ${n.name} (${n.isExported ? 'exported' : 'internal'}) ${n.filePath}:${n.startLine}`,
574
+ );
575
+ }
576
+ apiLines.push('#');
577
+ const existing = fs.readFileSync(outputPath, 'utf8');
578
+ fs.writeFileSync(outputPath, apiLines.join('\n') + '\n' + existing);
579
+ cg.close();
580
+ }
581
+ } catch {
582
+ // CodeGraph not installed or errored — skip silently
583
+ }
584
+
585
+ return 0;
586
+ }
587
+
588
+ // ── Handler entrypoint ───────────────────────────────────────────────────────
589
+
590
+ export async function architectureHandler(
591
+ args: string[],
592
+ context: ToolContext,
593
+ ): Promise<number> {
594
+ // Intercept apply / template before passing through to the JS CLI
595
+ const first = args[0] || '';
596
+ if (first === 'apply') return handleApply(args.slice(1), context);
597
+ if (first === 'template') return handleTemplate(args.slice(1), context);
7
598
 
8
599
  // Delegate to the existing atlas CLI (still in JS)
9
- // FIXED: added 'skills/' prefix that was missing in the original lib/tools/architecture.ts path.join call
10
- const cliPath = path.join(sourceRoot, 'skills', 'init-project-html', 'lib', 'atlas', 'cli.js');
600
+ const sourceRoot =
601
+ context.sourceRoot ||
602
+ path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
603
+ const cliPath = path.join(
604
+ sourceRoot,
605
+ 'skills',
606
+ 'init-project-html',
607
+ 'lib',
608
+ 'atlas',
609
+ 'cli.js',
610
+ );
11
611
 
12
612
  try {
13
613
  // Use file URL for ESM import compatibility on Windows — import() requires forward slashes.
@@ -18,7 +618,9 @@ export async function architectureHandler(args: string[], context: ToolContext):
18
618
  stderr: context.stderr || process.stderr,
19
619
  });
20
620
  } catch (error: any) {
21
- (context.stderr || process.stderr).write(`Error loading atlas CLI: ${error.message}\n`);
621
+ (context.stderr || process.stderr).write(
622
+ `Error loading atlas CLI: ${error.message}\n`,
623
+ );
22
624
  return 1;
23
625
  }
24
626
  }
@@ -0,0 +1,3 @@
1
+ import type { ToolDefinition, ToolContext } from '@laitszkin/tool-registry';
2
+ export declare function codegraphHandler(args: string[], context: ToolContext): Promise<number>;
3
+ export declare const tool: ToolDefinition;