@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.
- package/AGENTS.md +37 -27
- package/CHANGELOG.md +31 -0
- package/CLAUDE.md +37 -27
- package/README.md +15 -2
- package/assets/spec/rg13-1780435029246/test-questions.json +1 -0
- package/assets/spec/rg13-1780468345132/test-questions.json +1 -0
- package/package.json +3 -3
- package/packages/cli/dist/tool-registration.js +1 -0
- package/packages/cli/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/cli/tool-registration.ts +1 -0
- package/packages/tools/architecture/dist/index.js +539 -2
- package/packages/tools/architecture/dist/index.test.d.ts +1 -0
- package/packages/tools/architecture/dist/index.test.js +229 -0
- package/packages/tools/architecture/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/architecture/index.test.ts +329 -0
- package/packages/tools/architecture/index.ts +607 -5
- package/packages/tools/codegraph/dist/index.d.ts +3 -0
- package/packages/tools/codegraph/dist/index.js +157 -0
- package/packages/tools/codegraph/dist/lib/cg-instance.d.ts +29 -0
- package/packages/tools/codegraph/dist/lib/cg-instance.js +59 -0
- package/packages/tools/codegraph/dist/lib/cg-instance.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cg-instance.test.js +27 -0
- package/packages/tools/codegraph/dist/lib/cmd-explore.d.ts +5 -0
- package/packages/tools/codegraph/dist/lib/cmd-explore.js +95 -0
- package/packages/tools/codegraph/dist/lib/cmd-explore.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-explore.test.js +133 -0
- package/packages/tools/codegraph/dist/lib/cmd-flag-splice.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-flag-splice.test.js +83 -0
- package/packages/tools/codegraph/dist/lib/cmd-init.d.ts +5 -0
- package/packages/tools/codegraph/dist/lib/cmd-init.js +50 -0
- package/packages/tools/codegraph/dist/lib/cmd-init.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-init.test.js +51 -0
- package/packages/tools/codegraph/dist/lib/cmd-list-apis.d.ts +5 -0
- package/packages/tools/codegraph/dist/lib/cmd-list-apis.js +64 -0
- package/packages/tools/codegraph/dist/lib/cmd-list-apis.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-list-apis.test.js +69 -0
- package/packages/tools/codegraph/dist/lib/cmd-search.d.ts +5 -0
- package/packages/tools/codegraph/dist/lib/cmd-search.js +21 -0
- package/packages/tools/codegraph/dist/lib/cmd-search.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-search.test.js +30 -0
- package/packages/tools/codegraph/dist/lib/cmd-status.d.ts +4 -0
- package/packages/tools/codegraph/dist/lib/cmd-status.js +44 -0
- package/packages/tools/codegraph/dist/lib/cmd-status.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-status.test.js +72 -0
- package/packages/tools/codegraph/dist/lib/cmd-survey.d.ts +36 -0
- package/packages/tools/codegraph/dist/lib/cmd-survey.js +142 -0
- package/packages/tools/codegraph/dist/lib/cmd-survey.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-survey.test.js +136 -0
- package/packages/tools/codegraph/dist/lib/cmd-sync.d.ts +4 -0
- package/packages/tools/codegraph/dist/lib/cmd-sync.js +51 -0
- package/packages/tools/codegraph/dist/lib/cmd-sync.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-sync.test.js +30 -0
- package/packages/tools/codegraph/dist/lib/cmd-verify.d.ts +4 -0
- package/packages/tools/codegraph/dist/lib/cmd-verify.js +134 -0
- package/packages/tools/codegraph/dist/lib/cmd-verify.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/cmd-verify.test.js +139 -0
- package/packages/tools/codegraph/dist/lib/formatter.d.ts +67 -0
- package/packages/tools/codegraph/dist/lib/formatter.js +107 -0
- package/packages/tools/codegraph/dist/lib/formatter.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/formatter.test.js +41 -0
- package/packages/tools/codegraph/dist/lib/survey/grouper.d.ts +19 -0
- package/packages/tools/codegraph/dist/lib/survey/grouper.js +194 -0
- package/packages/tools/codegraph/dist/lib/survey/grouper.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/survey/grouper.test.js +62 -0
- package/packages/tools/codegraph/dist/lib/survey/scanner.d.ts +31 -0
- package/packages/tools/codegraph/dist/lib/survey/scanner.js +50 -0
- package/packages/tools/codegraph/dist/lib/verify/checker.d.ts +32 -0
- package/packages/tools/codegraph/dist/lib/verify/checker.js +146 -0
- package/packages/tools/codegraph/dist/lib/verify/checker.test.d.ts +1 -0
- package/packages/tools/codegraph/dist/lib/verify/checker.test.js +128 -0
- package/packages/tools/codegraph/dist/tsconfig.tsbuildinfo +1 -0
- package/packages/tools/codegraph/env.d.ts +56 -0
- package/packages/tools/codegraph/index.ts +173 -0
- package/packages/tools/codegraph/lib/cg-instance.test.ts +36 -0
- package/packages/tools/codegraph/lib/cg-instance.ts +66 -0
- package/packages/tools/codegraph/lib/cmd-explore.test.ts +195 -0
- package/packages/tools/codegraph/lib/cmd-explore.ts +129 -0
- package/packages/tools/codegraph/lib/cmd-flag-splice.test.ts +94 -0
- package/packages/tools/codegraph/lib/cmd-init.test.ts +68 -0
- package/packages/tools/codegraph/lib/cmd-init.ts +60 -0
- package/packages/tools/codegraph/lib/cmd-list-apis.test.ts +80 -0
- package/packages/tools/codegraph/lib/cmd-list-apis.ts +90 -0
- package/packages/tools/codegraph/lib/cmd-search.test.ts +37 -0
- package/packages/tools/codegraph/lib/cmd-search.ts +32 -0
- package/packages/tools/codegraph/lib/cmd-status.test.ts +86 -0
- package/packages/tools/codegraph/lib/cmd-status.ts +53 -0
- package/packages/tools/codegraph/lib/cmd-survey.test.ts +161 -0
- package/packages/tools/codegraph/lib/cmd-survey.ts +199 -0
- package/packages/tools/codegraph/lib/cmd-sync.test.ts +41 -0
- package/packages/tools/codegraph/lib/cmd-sync.ts +62 -0
- package/packages/tools/codegraph/lib/cmd-verify.test.ts +162 -0
- package/packages/tools/codegraph/lib/cmd-verify.ts +145 -0
- package/packages/tools/codegraph/lib/formatter.test.ts +47 -0
- package/packages/tools/codegraph/lib/formatter.ts +130 -0
- package/packages/tools/codegraph/lib/survey/grouper.test.ts +72 -0
- package/packages/tools/codegraph/lib/survey/grouper.ts +226 -0
- package/packages/tools/codegraph/lib/survey/scanner.ts +89 -0
- package/packages/tools/codegraph/lib/verify/checker.test.ts +140 -0
- package/packages/tools/codegraph/lib/verify/checker.ts +172 -0
- package/packages/tools/codegraph/package.json +23 -0
- package/packages/tools/codegraph/tsconfig.json +22 -0
- package/resources/project-architecture/atlas/atlas.history.log +32 -0
- package/resources/project-architecture/atlas/atlas.history.undo.json +356 -28
- package/resources/project-architecture/atlas/atlas.history.undo.stack.json +14350 -0
- package/resources/project-architecture/atlas/atlas.index.yaml +76 -12
- package/resources/project-architecture/atlas/features/codegraph.yaml +95 -0
- package/resources/project-architecture/atlas/features/eval-ci-gate.yaml +6 -1
- package/resources/project-architecture/atlas/features/eval-cli.yaml +16 -1
- package/resources/project-architecture/atlas/features/eval-executor.yaml +12 -2
- package/resources/project-architecture/atlas/features/eval-isolation.yaml +6 -1
- package/resources/project-architecture/atlas/features/eval-optimizer.yaml +17 -2
- package/resources/project-architecture/atlas/features/eval-question.yaml +12 -2
- package/resources/project-architecture/atlas/features/eval-reporter.yaml +6 -1
- package/resources/project-architecture/atlas/features/eval-scorer.yaml +12 -2
- package/resources/project-architecture/features/codegraph/cg-discovery.html +47 -0
- package/resources/project-architecture/features/codegraph/cg-lifecycle.html +48 -0
- package/resources/project-architecture/features/codegraph/cg-validation.html +47 -0
- package/resources/project-architecture/features/codegraph/index.html +58 -0
- package/resources/project-architecture/features/eval-ci-gate/workflow-trigger.html +6 -1
- package/resources/project-architecture/features/eval-cli/cli-handler.html +8 -1
- package/resources/project-architecture/features/eval-executor/exec-api-client.html +6 -1
- package/resources/project-architecture/features/eval-executor/trace-recorder.html +6 -1
- package/resources/project-architecture/features/eval-isolation/tool-dispatcher.html +6 -1
- package/resources/project-architecture/features/eval-optimizer/dedup-engine.html +6 -1
- package/resources/project-architecture/features/eval-optimizer/issue-extractor.html +7 -1
- package/resources/project-architecture/features/eval-question/question-loader.html +6 -1
- package/resources/project-architecture/features/eval-question/variant-generator.html +6 -1
- package/resources/project-architecture/features/eval-reporter/report-composer.html +6 -1
- package/resources/project-architecture/features/eval-scorer/judge-api-client.html +6 -1
- package/resources/project-architecture/features/eval-scorer/judge-prompt-builder.html +6 -1
- package/resources/project-architecture/index.html +200 -94
- package/skills/design/SKILL.md +33 -0
- package/skills/init-project-html/SKILL.md +12 -11
- 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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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(
|
|
621
|
+
(context.stderr || process.stderr).write(
|
|
622
|
+
`Error loading atlas CLI: ${error.message}\n`,
|
|
623
|
+
);
|
|
22
624
|
return 1;
|
|
23
625
|
}
|
|
24
626
|
}
|