busy-cli 0.1.2
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/README.md +129 -0
- package/dist/builders/context.d.ts +50 -0
- package/dist/builders/context.d.ts.map +1 -0
- package/dist/builders/context.js +190 -0
- package/dist/cache/index.d.ts +100 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +270 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +463 -0
- package/dist/commands/package.d.ts +96 -0
- package/dist/commands/package.d.ts.map +1 -0
- package/dist/commands/package.js +285 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/loader.d.ts +6 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +361 -0
- package/dist/merge.d.ts +16 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +102 -0
- package/dist/package/manifest.d.ts +59 -0
- package/dist/package/manifest.d.ts.map +1 -0
- package/dist/package/manifest.js +265 -0
- package/dist/parser.d.ts +28 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +220 -0
- package/dist/parsers/frontmatter.d.ts +14 -0
- package/dist/parsers/frontmatter.d.ts.map +1 -0
- package/dist/parsers/frontmatter.js +110 -0
- package/dist/parsers/imports.d.ts +48 -0
- package/dist/parsers/imports.d.ts.map +1 -0
- package/dist/parsers/imports.js +147 -0
- package/dist/parsers/links.d.ts +12 -0
- package/dist/parsers/links.d.ts.map +1 -0
- package/dist/parsers/links.js +79 -0
- package/dist/parsers/localdefs.d.ts +6 -0
- package/dist/parsers/localdefs.d.ts.map +1 -0
- package/dist/parsers/localdefs.js +132 -0
- package/dist/parsers/operations.d.ts +32 -0
- package/dist/parsers/operations.d.ts.map +1 -0
- package/dist/parsers/operations.js +313 -0
- package/dist/parsers/sections.d.ts +15 -0
- package/dist/parsers/sections.d.ts.map +1 -0
- package/dist/parsers/sections.js +173 -0
- package/dist/parsers/tools.d.ts +30 -0
- package/dist/parsers/tools.d.ts.map +1 -0
- package/dist/parsers/tools.js +178 -0
- package/dist/parsers/triggers.d.ts +35 -0
- package/dist/parsers/triggers.d.ts.map +1 -0
- package/dist/parsers/triggers.js +219 -0
- package/dist/providers/base.d.ts +60 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +34 -0
- package/dist/providers/github.d.ts +18 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +109 -0
- package/dist/providers/gitlab.d.ts +18 -0
- package/dist/providers/gitlab.d.ts.map +1 -0
- package/dist/providers/gitlab.js +101 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +17 -0
- package/dist/providers/local.d.ts +31 -0
- package/dist/providers/local.d.ts.map +1 -0
- package/dist/providers/local.js +116 -0
- package/dist/providers/url.d.ts +16 -0
- package/dist/providers/url.d.ts.map +1 -0
- package/dist/providers/url.js +45 -0
- package/dist/registry/index.d.ts +99 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +320 -0
- package/dist/types/schema.d.ts +3259 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +258 -0
- package/dist/utils/logger.d.ts +19 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +23 -0
- package/dist/utils/slugify.d.ts +14 -0
- package/dist/utils/slugify.d.ts.map +1 -0
- package/dist/utils/slugify.js +28 -0
- package/package.json +61 -0
- package/src/__tests__/cache.test.ts +393 -0
- package/src/__tests__/cli-package.test.ts +667 -0
- package/src/__tests__/fixtures/automated-workflow.busy.md +84 -0
- package/src/__tests__/fixtures/concept.busy.md +30 -0
- package/src/__tests__/fixtures/document.busy.md +44 -0
- package/src/__tests__/fixtures/simple-operation.busy.md +45 -0
- package/src/__tests__/fixtures/tool-document.busy.md +71 -0
- package/src/__tests__/fixtures/tool.busy.md +54 -0
- package/src/__tests__/imports.test.ts +244 -0
- package/src/__tests__/integration.test.ts +432 -0
- package/src/__tests__/operations.test.ts +408 -0
- package/src/__tests__/package-manifest.test.ts +455 -0
- package/src/__tests__/providers.test.ts +672 -0
- package/src/__tests__/registry.test.ts +402 -0
- package/src/__tests__/schema.test.ts +467 -0
- package/src/__tests__/tools.test.ts +376 -0
- package/src/__tests__/triggers.test.ts +312 -0
- package/src/builders/context.ts +294 -0
- package/src/cache/index.ts +312 -0
- package/src/cli/index.ts +514 -0
- package/src/commands/package.ts +392 -0
- package/src/index.ts +46 -0
- package/src/loader.ts +474 -0
- package/src/merge.ts +126 -0
- package/src/package/manifest.ts +349 -0
- package/src/parser.ts +278 -0
- package/src/parsers/frontmatter.ts +135 -0
- package/src/parsers/imports.ts +196 -0
- package/src/parsers/links.ts +108 -0
- package/src/parsers/localdefs.ts +166 -0
- package/src/parsers/operations.ts +404 -0
- package/src/parsers/sections.ts +230 -0
- package/src/parsers/tools.ts +215 -0
- package/src/parsers/triggers.ts +252 -0
- package/src/providers/base.ts +77 -0
- package/src/providers/github.ts +129 -0
- package/src/providers/gitlab.ts +121 -0
- package/src/providers/index.ts +25 -0
- package/src/providers/local.ts +129 -0
- package/src/providers/url.ts +56 -0
- package/src/registry/index.ts +408 -0
- package/src/types/schema.ts +369 -0
- package/src/utils/logger.ts +25 -0
- package/src/utils/slugify.ts +31 -0
- package/tsconfig.json +21 -0
package/src/loader.ts
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import fg from 'fast-glob';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
Repo,
|
|
6
|
+
BusyDocument,
|
|
7
|
+
Playbook,
|
|
8
|
+
ConceptBase,
|
|
9
|
+
LocalDef,
|
|
10
|
+
Operation,
|
|
11
|
+
ImportDef,
|
|
12
|
+
Edge,
|
|
13
|
+
Section,
|
|
14
|
+
File,
|
|
15
|
+
} from './types/schema.js';
|
|
16
|
+
import { parseFrontMatter } from './parsers/frontmatter.js';
|
|
17
|
+
import { parseSections, getAllSections, findSection } from './parsers/sections.js';
|
|
18
|
+
import { extractLocalDefs } from './parsers/localdefs.js';
|
|
19
|
+
import { extractOperations } from './parsers/operations.js';
|
|
20
|
+
import { extractImports, legacyResolveImportTarget } from './parsers/imports.js';
|
|
21
|
+
import { extractLinksFromSection } from './parsers/links.js';
|
|
22
|
+
import { debug, warn } from './utils/logger.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load and index a workspace from glob patterns
|
|
26
|
+
*/
|
|
27
|
+
export async function loadRepo(globs: string[]): Promise<Repo> {
|
|
28
|
+
debug.parser('Loading repo from globs: %o', globs);
|
|
29
|
+
|
|
30
|
+
// Find all markdown files
|
|
31
|
+
const filePaths = await fg(globs, {
|
|
32
|
+
absolute: true,
|
|
33
|
+
onlyFiles: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
debug.parser('Found %d files', filePaths.length);
|
|
37
|
+
|
|
38
|
+
// Sort files for determinism
|
|
39
|
+
filePaths.sort();
|
|
40
|
+
|
|
41
|
+
// Build file map for resolution
|
|
42
|
+
const fileMap = new Map<string, { docId: string; path: string }>();
|
|
43
|
+
|
|
44
|
+
// First pass: parse frontmatter to build file map
|
|
45
|
+
const fileContents = new Map<string, string>();
|
|
46
|
+
|
|
47
|
+
for (const filePath of filePaths) {
|
|
48
|
+
const content = await readFile(filePath, 'utf-8');
|
|
49
|
+
fileContents.set(filePath, content);
|
|
50
|
+
|
|
51
|
+
const { docId } = parseFrontMatter(content, filePath);
|
|
52
|
+
|
|
53
|
+
// Store multiple variants for resolution:
|
|
54
|
+
// - Full basename: "document.busy.md"
|
|
55
|
+
// - Without .busy: "document.md" (for imports that reference old extension)
|
|
56
|
+
// - Just name: "document"
|
|
57
|
+
const basename = path.basename(filePath);
|
|
58
|
+
const withoutBusy = basename.replace('.busy.md', '.md');
|
|
59
|
+
const nameOnly = basename.replace(/\.busy\.md$/, '').replace(/\.md$/, '');
|
|
60
|
+
|
|
61
|
+
fileMap.set(basename, { docId, path: filePath });
|
|
62
|
+
if (withoutBusy !== basename) {
|
|
63
|
+
fileMap.set(withoutBusy, { docId, path: filePath });
|
|
64
|
+
}
|
|
65
|
+
fileMap.set(nameOnly, { docId, path: filePath });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Second pass: parse documents
|
|
69
|
+
const files: File[] = []; // Lightweight file representations
|
|
70
|
+
const docs: (BusyDocument | Playbook)[] = []; // Full concept definitions
|
|
71
|
+
const allLocaldefs = new Map<string, LocalDef>();
|
|
72
|
+
const allOperations = new Map<string, Operation>();
|
|
73
|
+
const allImports: ImportDef[] = [];
|
|
74
|
+
const allEdges: Edge[] = [];
|
|
75
|
+
const allSections = new Map<string, Section>();
|
|
76
|
+
|
|
77
|
+
// Store document parts temporarily before building final docs
|
|
78
|
+
const docParts = new Map<string, {
|
|
79
|
+
filePath: string;
|
|
80
|
+
content: string;
|
|
81
|
+
frontmatter: any;
|
|
82
|
+
docId: string;
|
|
83
|
+
types: string[];
|
|
84
|
+
extends: string[];
|
|
85
|
+
sections: Section[];
|
|
86
|
+
localdefs: LocalDef[];
|
|
87
|
+
operations: Operation[];
|
|
88
|
+
setup: any;
|
|
89
|
+
imports: ImportDef[];
|
|
90
|
+
symbols: Record<string, { docId?: string; slug?: string }>;
|
|
91
|
+
}>();
|
|
92
|
+
|
|
93
|
+
for (const filePath of filePaths) {
|
|
94
|
+
const content = fileContents.get(filePath)!;
|
|
95
|
+
|
|
96
|
+
// Parse frontmatter
|
|
97
|
+
const { frontmatter, content: mdContent, docId, kind, types, extends: extends_ } =
|
|
98
|
+
parseFrontMatter(content, filePath);
|
|
99
|
+
|
|
100
|
+
// Parse sections
|
|
101
|
+
const sections = parseSections(mdContent, docId, filePath);
|
|
102
|
+
|
|
103
|
+
// Create file representation (lightweight - just sections)
|
|
104
|
+
files.push({
|
|
105
|
+
docId,
|
|
106
|
+
path: filePath,
|
|
107
|
+
name: frontmatter.Name,
|
|
108
|
+
sections,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Index all sections
|
|
112
|
+
for (const section of getAllSections(sections)) {
|
|
113
|
+
allSections.set(section.id, section);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Extract local definitions
|
|
117
|
+
const localdefs = extractLocalDefs(sections, docId, filePath);
|
|
118
|
+
for (const localdef of localdefs) {
|
|
119
|
+
allLocaldefs.set(localdef.id, localdef);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Extract operations
|
|
123
|
+
const operations = extractOperations(sections, docId, filePath);
|
|
124
|
+
for (const operation of operations) {
|
|
125
|
+
allOperations.set(operation.id, operation);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Extract setup (if present)
|
|
129
|
+
const setupSection = findSection(sections, 'setup');
|
|
130
|
+
const setup = setupSection ? {
|
|
131
|
+
kind: 'setup' as const,
|
|
132
|
+
id: `${docId}::setup`, // Use :: for concept IDs
|
|
133
|
+
docId,
|
|
134
|
+
slug: 'setup',
|
|
135
|
+
name: 'Setup',
|
|
136
|
+
content: setupSection.content,
|
|
137
|
+
types: [],
|
|
138
|
+
extends: [],
|
|
139
|
+
sectionRef: setupSection.id, // sectionRef uses # for section references
|
|
140
|
+
} : undefined;
|
|
141
|
+
|
|
142
|
+
// Extract imports
|
|
143
|
+
const { imports, symbols } = extractImports(content, docId);
|
|
144
|
+
|
|
145
|
+
// Store document parts
|
|
146
|
+
docParts.set(docId, {
|
|
147
|
+
filePath,
|
|
148
|
+
content,
|
|
149
|
+
frontmatter,
|
|
150
|
+
docId,
|
|
151
|
+
types,
|
|
152
|
+
extends: extends_,
|
|
153
|
+
sections,
|
|
154
|
+
localdefs,
|
|
155
|
+
operations,
|
|
156
|
+
setup,
|
|
157
|
+
imports,
|
|
158
|
+
symbols,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Resolve imports
|
|
162
|
+
for (const importDef of imports) {
|
|
163
|
+
const resolved = legacyResolveImportTarget(importDef.target, docId, fileMap);
|
|
164
|
+
|
|
165
|
+
// Store resolved as ConceptId (string) per schema
|
|
166
|
+
if (resolved.docId) {
|
|
167
|
+
const resolvedId = resolved.slug
|
|
168
|
+
? `${resolved.docId}#${resolved.slug}`
|
|
169
|
+
: resolved.docId;
|
|
170
|
+
importDef.resolved = resolvedId;
|
|
171
|
+
|
|
172
|
+
// Create import edge
|
|
173
|
+
allEdges.push({
|
|
174
|
+
from: docId,
|
|
175
|
+
to: resolvedId,
|
|
176
|
+
role: 'imports',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Update symbol table (keeps object format for easy lookup)
|
|
181
|
+
if (symbols[importDef.label]) {
|
|
182
|
+
symbols[importDef.label] = resolved;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
allImports.push(importDef);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Extract links and create edges
|
|
189
|
+
for (const section of getAllSections(sections)) {
|
|
190
|
+
const linkEdges = extractLinksFromSection(
|
|
191
|
+
section,
|
|
192
|
+
section.content,
|
|
193
|
+
symbols,
|
|
194
|
+
fileMap
|
|
195
|
+
);
|
|
196
|
+
allEdges.push(...linkEdges);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Create extends edges for local definitions
|
|
200
|
+
for (const localdef of localdefs) {
|
|
201
|
+
for (const parent of localdef.extends) {
|
|
202
|
+
const resolvedParent = resolveSymbol(parent, docId, allLocaldefs, docs, symbols);
|
|
203
|
+
if (resolvedParent) {
|
|
204
|
+
allEdges.push({
|
|
205
|
+
from: localdef.id,
|
|
206
|
+
to: resolvedParent,
|
|
207
|
+
role: 'extends',
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
warn(`Unresolved extends: ${parent} in ${localdef.id}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Build final document structures with inline arrays
|
|
217
|
+
for (const [docId, parts] of docParts) {
|
|
218
|
+
const isPlaybook = parts.types.some((t) => t.toLowerCase() === 'playbook');
|
|
219
|
+
|
|
220
|
+
if (isPlaybook) {
|
|
221
|
+
// Extract sequence from ExecutePlaybook operation
|
|
222
|
+
const sequence = extractPlaybookSequence(parts.sections);
|
|
223
|
+
|
|
224
|
+
const doc: Playbook = {
|
|
225
|
+
kind: 'playbook',
|
|
226
|
+
id: parts.docId,
|
|
227
|
+
docId: parts.docId,
|
|
228
|
+
slug: parts.docId.toLowerCase(),
|
|
229
|
+
name: parts.frontmatter.Name,
|
|
230
|
+
content: parts.content,
|
|
231
|
+
types: parts.types,
|
|
232
|
+
extends: parts.extends,
|
|
233
|
+
sectionRef: `${parts.docId}#`, // Root reference
|
|
234
|
+
imports: parts.imports,
|
|
235
|
+
localdefs: parts.localdefs,
|
|
236
|
+
setup: parts.setup!,
|
|
237
|
+
operations: parts.operations,
|
|
238
|
+
sequence,
|
|
239
|
+
};
|
|
240
|
+
docs.push(doc);
|
|
241
|
+
} else {
|
|
242
|
+
const doc: BusyDocument = {
|
|
243
|
+
kind: 'document',
|
|
244
|
+
id: parts.docId,
|
|
245
|
+
docId: parts.docId,
|
|
246
|
+
slug: parts.docId.toLowerCase(),
|
|
247
|
+
name: parts.frontmatter.Name,
|
|
248
|
+
content: parts.content,
|
|
249
|
+
types: parts.types,
|
|
250
|
+
extends: parts.extends,
|
|
251
|
+
sectionRef: `${parts.docId}#`, // Root reference
|
|
252
|
+
imports: parts.imports,
|
|
253
|
+
localdefs: parts.localdefs,
|
|
254
|
+
setup: parts.setup!,
|
|
255
|
+
operations: parts.operations,
|
|
256
|
+
};
|
|
257
|
+
docs.push(doc);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Inherit operations from parent documents
|
|
262
|
+
inheritOperations(docs, allOperations);
|
|
263
|
+
|
|
264
|
+
// Build concepts array (includes all documents as ConceptBase)
|
|
265
|
+
const concepts: ConceptBase[] = docs.map((doc) => ({
|
|
266
|
+
kind: doc.kind,
|
|
267
|
+
id: doc.id,
|
|
268
|
+
docId: doc.docId,
|
|
269
|
+
slug: doc.slug,
|
|
270
|
+
name: doc.name,
|
|
271
|
+
content: doc.content,
|
|
272
|
+
types: doc.types,
|
|
273
|
+
extends: doc.extends,
|
|
274
|
+
sectionRef: doc.sectionRef,
|
|
275
|
+
children: [], // ConceptBase has children for hierarchy
|
|
276
|
+
}));
|
|
277
|
+
|
|
278
|
+
// Build byId index
|
|
279
|
+
const byId: Record<string, Section | LocalDef | Operation | ConceptBase> = {};
|
|
280
|
+
|
|
281
|
+
for (const concept of concepts) {
|
|
282
|
+
byId[concept.id] = concept;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const [id, section] of allSections) {
|
|
286
|
+
byId[id] = section;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const [id, localdef] of allLocaldefs) {
|
|
290
|
+
byId[id] = localdef;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for (const [id, operation] of allOperations) {
|
|
294
|
+
byId[id] = operation;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Reclassify edges based on target type
|
|
298
|
+
// Links to operations should be 'calls', links to defs/concepts should be 'ref'
|
|
299
|
+
for (const edge of allEdges) {
|
|
300
|
+
if (edge.role === 'ref') {
|
|
301
|
+
const target = byId[edge.to];
|
|
302
|
+
if (target && target.kind === 'operation') {
|
|
303
|
+
edge.role = 'calls';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Build byFile index
|
|
309
|
+
const byFile: Record<string, { concept: BusyDocument | Playbook; bySlug: Record<string, Section> }> = {};
|
|
310
|
+
|
|
311
|
+
for (const doc of docs) {
|
|
312
|
+
const bySlug: Record<string, Section> = {};
|
|
313
|
+
const parts = docParts.get(doc.docId);
|
|
314
|
+
|
|
315
|
+
if (parts) {
|
|
316
|
+
for (const section of getAllSections(parts.sections)) {
|
|
317
|
+
bySlug[section.slug] = section;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
byFile[doc.docId] = { concept: doc, bySlug };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const repo: Repo = {
|
|
325
|
+
files,
|
|
326
|
+
concepts,
|
|
327
|
+
localdefs: Object.fromEntries(allLocaldefs),
|
|
328
|
+
operations: Object.fromEntries(allOperations),
|
|
329
|
+
imports: allImports,
|
|
330
|
+
byId,
|
|
331
|
+
byFile,
|
|
332
|
+
edges: allEdges,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
debug.parser(
|
|
336
|
+
'Loaded repo: %d docs, %d concepts, %d localdefs, %d operations, %d imports, %d edges',
|
|
337
|
+
docs.length,
|
|
338
|
+
concepts.length,
|
|
339
|
+
allLocaldefs.size,
|
|
340
|
+
allOperations.size,
|
|
341
|
+
allImports.length,
|
|
342
|
+
allEdges.length
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return repo;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Inherit operations from parent documents
|
|
350
|
+
* Inherits from:
|
|
351
|
+
* 1. Documents in the 'extends' array (explicit extension)
|
|
352
|
+
* 2. Documents in the 'types' array (implicit type-based inheritance)
|
|
353
|
+
*/
|
|
354
|
+
function inheritOperations(
|
|
355
|
+
docs: (BusyDocument | Playbook)[],
|
|
356
|
+
allOperations: Map<string, Operation>
|
|
357
|
+
): void {
|
|
358
|
+
// Build doc lookup by name
|
|
359
|
+
const docByName = new Map<string, BusyDocument | Playbook>();
|
|
360
|
+
for (const doc of docs) {
|
|
361
|
+
docByName.set(doc.name, doc);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Process each document
|
|
365
|
+
for (const doc of docs) {
|
|
366
|
+
// Collect parent names from both extends and types
|
|
367
|
+
const parentNames = [...doc.extends, ...doc.types];
|
|
368
|
+
|
|
369
|
+
if (parentNames.length === 0) continue;
|
|
370
|
+
|
|
371
|
+
// Get operations currently in this document
|
|
372
|
+
const existingOps = new Set<string>();
|
|
373
|
+
for (const [id, op] of allOperations) {
|
|
374
|
+
if (op.docId === doc.docId) {
|
|
375
|
+
existingOps.add(op.slug);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Inherit from parent documents
|
|
380
|
+
for (const parentName of parentNames) {
|
|
381
|
+
const parentDoc = docByName.get(parentName);
|
|
382
|
+
if (!parentDoc) {
|
|
383
|
+
debug.parser('Parent document not found: %s', parentName);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Find all operations in parent document
|
|
388
|
+
for (const [id, op] of allOperations) {
|
|
389
|
+
if (op.docId === parentDoc.docId) {
|
|
390
|
+
// If operation not overridden in child, inherit it
|
|
391
|
+
if (!existingOps.has(op.slug)) {
|
|
392
|
+
const inheritedOp: Operation = {
|
|
393
|
+
...op,
|
|
394
|
+
id: `${doc.docId}::${op.slug}`, // Use :: for concept IDs
|
|
395
|
+
docId: doc.docId,
|
|
396
|
+
};
|
|
397
|
+
allOperations.set(inheritedOp.id, inheritedOp);
|
|
398
|
+
existingOps.add(op.slug);
|
|
399
|
+
debug.parser(
|
|
400
|
+
'Inherited operation %s from %s to %s',
|
|
401
|
+
op.name,
|
|
402
|
+
parentDoc.name,
|
|
403
|
+
doc.name
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Resolve a symbol (name or label) to a node ID
|
|
414
|
+
*/
|
|
415
|
+
function resolveSymbol(
|
|
416
|
+
nameOrLabel: string,
|
|
417
|
+
currentDocId: string,
|
|
418
|
+
localdefs: Map<string, LocalDef>,
|
|
419
|
+
docs: (BusyDocument | Playbook)[],
|
|
420
|
+
symbols: Record<string, { docId?: string; slug?: string }>
|
|
421
|
+
): string | undefined {
|
|
422
|
+
// 1. Check for LocalDef in same doc
|
|
423
|
+
const localdefId = `${currentDocId}::${nameOrLabel.toLowerCase()}`;
|
|
424
|
+
if (localdefs.has(localdefId)) {
|
|
425
|
+
return localdefId;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 2. Check for Concept/Doc by Name
|
|
429
|
+
const doc = docs.find((d) => d.name === nameOrLabel);
|
|
430
|
+
if (doc) {
|
|
431
|
+
return doc.docId;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 3. Check import symbol table
|
|
435
|
+
const symbol = symbols[nameOrLabel];
|
|
436
|
+
if (symbol?.docId) {
|
|
437
|
+
return symbol.slug ? `${symbol.docId}#${symbol.slug}` : symbol.docId;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Extract sequence of operations from a playbook's ExecutePlaybook operation
|
|
445
|
+
* Looks for sections with "Step" in the title and extracts Target metadata
|
|
446
|
+
*/
|
|
447
|
+
function extractPlaybookSequence(sections: Section[]): string[] {
|
|
448
|
+
const sequence: string[] = [];
|
|
449
|
+
|
|
450
|
+
// Find ExecutePlaybook operation in the Operations section
|
|
451
|
+
const allSecs = getAllSections(sections);
|
|
452
|
+
const executePlaybook = allSecs.find(
|
|
453
|
+
(sec) => sec.title.toLowerCase() === 'executeplaybook'
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (!executePlaybook) {
|
|
457
|
+
return sequence;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Look for child sections that are steps (contain "step" in title, case-insensitive)
|
|
461
|
+
for (const child of executePlaybook.children) {
|
|
462
|
+
if (child.title.toLowerCase().includes('step')) {
|
|
463
|
+
// Extract Target field from content
|
|
464
|
+
// Pattern: - **Target:** `OperationName`
|
|
465
|
+
const targetMatch = child.content.match(/^\s*-\s*\*\*Target:\*\*\s*`([^`]+)`/m);
|
|
466
|
+
if (targetMatch) {
|
|
467
|
+
sequence.push(targetMatch[1]);
|
|
468
|
+
debug.parser('Found playbook sequence step: %s -> %s', child.title, targetMatch[1]);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return sequence;
|
|
474
|
+
}
|
package/src/merge.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Repo, File, ConceptBase, LocalDef, Operation, ImportDef, Edge, Section } from './types/schema.js';
|
|
2
|
+
import { debug } from './utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Merge multiple repos into a single repo
|
|
6
|
+
* Later repos override earlier ones when there are conflicts
|
|
7
|
+
*/
|
|
8
|
+
export function mergeRepos(...repos: Repo[]): Repo {
|
|
9
|
+
if (repos.length === 0) {
|
|
10
|
+
throw new Error('At least one repo is required');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (repos.length === 1) {
|
|
14
|
+
return repos[0];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
debug.parser('Merging %d repos', repos.length);
|
|
18
|
+
|
|
19
|
+
const merged: Repo = {
|
|
20
|
+
files: [],
|
|
21
|
+
concepts: [],
|
|
22
|
+
localdefs: {},
|
|
23
|
+
operations: {},
|
|
24
|
+
imports: [],
|
|
25
|
+
byId: {},
|
|
26
|
+
byFile: {},
|
|
27
|
+
edges: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Merge files (later repos override)
|
|
31
|
+
const filesByDocId = new Map<string, File>();
|
|
32
|
+
for (const repo of repos) {
|
|
33
|
+
for (const file of repo.files) {
|
|
34
|
+
filesByDocId.set(file.docId, file);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
merged.files = Array.from(filesByDocId.values());
|
|
38
|
+
|
|
39
|
+
// Merge concepts (later repos override)
|
|
40
|
+
const conceptsById = new Map<string, ConceptBase>();
|
|
41
|
+
for (const repo of repos) {
|
|
42
|
+
for (const concept of repo.concepts) {
|
|
43
|
+
conceptsById.set(concept.id, concept);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
merged.concepts = Array.from(conceptsById.values());
|
|
47
|
+
|
|
48
|
+
// Merge localdefs (later repos override)
|
|
49
|
+
for (const repo of repos) {
|
|
50
|
+
Object.assign(merged.localdefs, repo.localdefs);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Merge operations (later repos override)
|
|
54
|
+
for (const repo of repos) {
|
|
55
|
+
Object.assign(merged.operations, repo.operations);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Merge imports (append all, handle duplicates by ID)
|
|
59
|
+
const importsByDocId = new Map<string, Map<string, ImportDef>>();
|
|
60
|
+
for (const repo of repos) {
|
|
61
|
+
for (const imp of repo.imports) {
|
|
62
|
+
if (!importsByDocId.has(imp.docId)) {
|
|
63
|
+
importsByDocId.set(imp.docId, new Map());
|
|
64
|
+
}
|
|
65
|
+
importsByDocId.get(imp.docId)!.set(imp.id, imp);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const docImports of importsByDocId.values()) {
|
|
69
|
+
merged.imports.push(...docImports.values());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Merge byId (later repos override)
|
|
73
|
+
for (const repo of repos) {
|
|
74
|
+
Object.assign(merged.byId, repo.byId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Merge byFile (later repos override)
|
|
78
|
+
for (const repo of repos) {
|
|
79
|
+
Object.assign(merged.byFile, repo.byFile);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Merge edges (append all, deduplicate)
|
|
83
|
+
const edgeSet = new Set<string>();
|
|
84
|
+
for (const repo of repos) {
|
|
85
|
+
for (const edge of repo.edges) {
|
|
86
|
+
const key = `${edge.from}→${edge.to}:${edge.role}`;
|
|
87
|
+
if (!edgeSet.has(key)) {
|
|
88
|
+
edgeSet.add(key);
|
|
89
|
+
merged.edges.push(edge);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
debug.parser(
|
|
95
|
+
'Merged repo: %d files, %d concepts, %d localdefs, %d operations, %d imports, %d edges',
|
|
96
|
+
merged.files.length,
|
|
97
|
+
merged.concepts.length,
|
|
98
|
+
Object.keys(merged.localdefs).length,
|
|
99
|
+
Object.keys(merged.operations).length,
|
|
100
|
+
merged.imports.length,
|
|
101
|
+
merged.edges.length
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return merged;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extend a base repo with additional files
|
|
109
|
+
* This is a convenience wrapper around mergeRepos
|
|
110
|
+
*/
|
|
111
|
+
export function extendRepo(baseRepo: Repo, extensionRepo: Repo): Repo {
|
|
112
|
+
debug.parser('Extending base repo with extension');
|
|
113
|
+
return mergeRepos(baseRepo, extensionRepo);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Load a repo from JSON and validate it against the schema
|
|
118
|
+
*/
|
|
119
|
+
export function loadRepoFromJSON(json: string): Repo {
|
|
120
|
+
const data = JSON.parse(json);
|
|
121
|
+
// Basic validation - could use Zod here but keeping it simple for now
|
|
122
|
+
if (!data.files || !data.concepts) {
|
|
123
|
+
throw new Error('Invalid repo JSON: missing required fields');
|
|
124
|
+
}
|
|
125
|
+
return data as Repo;
|
|
126
|
+
}
|