depa-codument 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/package.json +63 -0
- package/src/cli/commands/archive.ts +519 -0
- package/src/cli/commands/decisions.ts +123 -0
- package/src/cli/commands/engineering.ts +105 -0
- package/src/cli/commands/init.ts +54 -0
- package/src/cli/commands/list.ts +73 -0
- package/src/cli/commands/modeling.ts +105 -0
- package/src/cli/commands/show.ts +238 -0
- package/src/cli/commands/status.ts +140 -0
- package/src/cli/commands/upgrade-track.ts +385 -0
- package/src/cli/commands/upgrade-workspace.ts +138 -0
- package/src/cli/commands/validate.ts +330 -0
- package/src/cli/engineering/config.ts +68 -0
- package/src/cli/engineering/lint.ts +58 -0
- package/src/cli/engineering/merge.ts +172 -0
- package/src/cli/engineering/registry.ts +230 -0
- package/src/cli/engineering/schema.ts +126 -0
- package/src/cli/engineering/validate.ts +286 -0
- package/src/cli/index.ts +136 -0
- package/src/cli/modeling/config.ts +68 -0
- package/src/cli/modeling/lint.ts +58 -0
- package/src/cli/modeling/merge.ts +172 -0
- package/src/cli/modeling/registry.ts +229 -0
- package/src/cli/modeling/schema.ts +160 -0
- package/src/cli/modeling/validate.ts +282 -0
- package/src/cli/utils/index.ts +941 -0
- package/src/cli/utils/install.ts +291 -0
- package/src/cli/utils/spec-xml.ts +673 -0
- package/src/cli/utils/track-time.ts +75 -0
- package/src/cli/utils/vfs.ts +102 -0
- package/src/templates/codument/README.md +59 -0
- package/src/templates/codument/attractors/product.md +17 -0
- package/src/templates/codument/attractors/project.md +10 -0
- package/src/templates/codument/backlog/README.md +33 -0
- package/src/templates/codument/config/attractor-profiles.xml +31 -0
- package/src/templates/codument/config/engineering.xml +22 -0
- package/src/templates/codument/config/modeling.xml +22 -0
- package/src/templates/codument/config/operation-hooks.xml +55 -0
- package/src/templates/codument/memory/README.md +13 -0
- package/src/templates/codument/missions/README.md +125 -0
- package/src/templates/codument/sop/README.md +14 -0
- package/src/templates/codument/std/AGENTS.md +82 -0
- package/src/templates/codument/std/attractors/depa-attractor.md +572 -0
- package/src/templates/codument/std/attractors/knowledge-tiers.md +128 -0
- package/src/templates/codument/std/attractors/model-driven-docs.md +293 -0
- package/src/templates/codument/std/attractors/project-memory.md +48 -0
- package/src/templates/codument/std/docs-impl-fractal/index.md +110 -0
- package/src/templates/codument/std/docs-modeling-fractal/index.md +156 -0
- package/src/templates/codument/std/kernel-pointer.md +19 -0
- package/src/templates/codument/std/operations/README.md +30 -0
- package/src/templates/codument/std/operations/_operation-spec.md +41 -0
- package/src/templates/codument/std/operations/archive-mission.md +66 -0
- package/src/templates/codument/std/operations/archive-track.md +238 -0
- package/src/templates/codument/std/operations/artifact-sync.md +172 -0
- package/src/templates/codument/std/operations/discuss-phase.md +214 -0
- package/src/templates/codument/std/operations/discuss.md +87 -0
- package/src/templates/codument/std/operations/docs-bootstrap.md +148 -0
- package/src/templates/codument/std/operations/gap-loop.md +301 -0
- package/src/templates/codument/std/operations/impl-mission.md +167 -0
- package/src/templates/codument/std/operations/impl-quick.md +79 -0
- package/src/templates/codument/std/operations/impl-track.md +537 -0
- package/src/templates/codument/std/operations/migrate.md +337 -0
- package/src/templates/codument/std/operations/plan-mission.md +230 -0
- package/src/templates/codument/std/operations/plan-track-wave.md +231 -0
- package/src/templates/codument/std/operations/plan-track.md +579 -0
- package/src/templates/codument/std/operations/revise-track.md +136 -0
- package/src/templates/codument/std/operations/validate.md +339 -0
- package/src/templates/codument/std/operations/verify.md +184 -0
- package/src/templates/codument/std/root-agents.md +39 -0
- package/src/templates/codument/std/sop/questioning.md +98 -0
- package/src/templates/codument/std/sop/tdd.md +26 -0
- package/src/templates/codument/std/sop/validation.md +25 -0
- package/src/templates/codument/std/sop/wave-exec.md +42 -0
- package/src/templates/codument/std/sop/workflow.md +35 -0
- package/src/templates/codument/std/spec/behavior-delta.md +36 -0
- package/src/templates/codument/std/spec/behavior-registry.md +42 -0
- package/src/templates/codument/std/spec/engineering-delta.md +68 -0
- package/src/templates/codument/std/spec/engineering-node-schema.md +86 -0
- package/src/templates/codument/std/spec/engineering-registry.md +82 -0
- package/src/templates/codument/std/spec/flow-notation.md +93 -0
- package/src/templates/codument/std/spec/folder-manifest.md +99 -0
- package/src/templates/codument/std/spec/mission-xml-spec.md +249 -0
- package/src/templates/codument/std/spec/modeling-delta.md +85 -0
- package/src/templates/codument/std/spec/modeling-node-schema.md +183 -0
- package/src/templates/codument/std/spec/modeling-registry.md +49 -0
- package/src/templates/codument/std/spec/track-xml-spec.md +272 -0
- package/src/templates/codument/std/spec/xnl-format.md +301 -0
- package/src/templates/codument/workflows/README.md +15 -0
- package/src/templates/manifest.ts +177 -0
- package/src/templates/skills/README.md +38 -0
- package/src/templates/skills/codument-archive/SKILL.md +17 -0
- package/src/templates/skills/codument-archive-mission/SKILL.md +17 -0
- package/src/templates/skills/codument-archive-track/SKILL.md +17 -0
- package/src/templates/skills/codument-artifact-sync/SKILL.md +17 -0
- package/src/templates/skills/codument-code-quality-score/SKILL.md +67 -0
- package/src/templates/skills/codument-decision-tree/SKILL.md +40 -0
- package/src/templates/skills/codument-discuss/SKILL.md +17 -0
- package/src/templates/skills/codument-discuss-phase/SKILL.md +17 -0
- package/src/templates/skills/codument-docs-bootstrap/SKILL.md +17 -0
- package/src/templates/skills/codument-gap-loop/SKILL.md +17 -0
- package/src/templates/skills/codument-impl-mission/SKILL.md +17 -0
- package/src/templates/skills/codument-impl-quick/SKILL.md +17 -0
- package/src/templates/skills/codument-impl-track/SKILL.md +17 -0
- package/src/templates/skills/codument-implement/SKILL.md +14 -0
- package/src/templates/skills/codument-migrate/SKILL.md +17 -0
- package/src/templates/skills/codument-modeling-engineering-e2e/SKILL.md +74 -0
- package/src/templates/skills/codument-plan-mission/SKILL.md +17 -0
- package/src/templates/skills/codument-plan-track/SKILL.md +17 -0
- package/src/templates/skills/codument-plan-track-wave/SKILL.md +17 -0
- package/src/templates/skills/codument-revise-track/SKILL.md +17 -0
- package/src/templates/skills/codument-track/SKILL.md +14 -0
- package/src/templates/skills/codument-validate/SKILL.md +17 -0
- package/src/templates/skills/codument-verify/SKILL.md +17 -0
- package/src/types/text-assets.d.ts +9 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { parseCodumentVfsUri } from './vfs';
|
|
4
|
+
|
|
5
|
+
export interface SpecXmlNode {
|
|
6
|
+
tag: string;
|
|
7
|
+
attrs: Record<string, string>;
|
|
8
|
+
children: SpecXmlNode[];
|
|
9
|
+
text?: string;
|
|
10
|
+
sourcePath?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SpecXmlStats {
|
|
14
|
+
requirements: number;
|
|
15
|
+
scenarios: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SELF_CLOSING = /\/\s*>$/;
|
|
19
|
+
const PATCH_ROOT_TAGS = new Set(['spec-patch', 'behavior-patch']);
|
|
20
|
+
const WRAPPER_OP_TAGS = new Set(['upsert', 'delete', 'move']);
|
|
21
|
+
|
|
22
|
+
function parseAttrs(raw: string): Record<string, string> {
|
|
23
|
+
const attrs: Record<string, string> = {};
|
|
24
|
+
const attrRegex = /([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*"([^"]*)"/g;
|
|
25
|
+
let match;
|
|
26
|
+
while ((match = attrRegex.exec(raw)) !== null) {
|
|
27
|
+
attrs[match[1]] = decodeXml(match[2]);
|
|
28
|
+
}
|
|
29
|
+
return attrs;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function decodeXml(value: string): string {
|
|
33
|
+
return value
|
|
34
|
+
.replace(/"/g, '"')
|
|
35
|
+
.replace(/'/g, "'")
|
|
36
|
+
.replace(/>/g, '>')
|
|
37
|
+
.replace(/</g, '<')
|
|
38
|
+
.replace(/&/g, '&');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function encodeXml(value: string): string {
|
|
42
|
+
return value
|
|
43
|
+
.replace(/&/g, '&')
|
|
44
|
+
.replace(/</g, '<')
|
|
45
|
+
.replace(/>/g, '>')
|
|
46
|
+
.replace(/"/g, '"');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appendText(node: SpecXmlNode, text: string): void {
|
|
50
|
+
const trimmed = text.trim();
|
|
51
|
+
if (!trimmed) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
node.text = node.text ? `${node.text}\n${trimmed}` : trimmed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseSpecXmlContent(content: string): SpecXmlNode {
|
|
58
|
+
const root: SpecXmlNode = { tag: '__root__', attrs: {}, children: [] };
|
|
59
|
+
const stack: SpecXmlNode[] = [root];
|
|
60
|
+
const tokenRegex = /<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<!\[CDATA\[[\s\S]*?\]\]>|<\/?[^>]+>/g;
|
|
61
|
+
let cursor = 0;
|
|
62
|
+
let match;
|
|
63
|
+
|
|
64
|
+
while ((match = tokenRegex.exec(content)) !== null) {
|
|
65
|
+
appendText(stack[stack.length - 1], content.slice(cursor, match.index));
|
|
66
|
+
const token = match[0];
|
|
67
|
+
cursor = match.index + token.length;
|
|
68
|
+
|
|
69
|
+
if (token.startsWith('<!--') || token.startsWith('<?')) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (token.startsWith('<![CDATA[')) {
|
|
73
|
+
appendText(stack[stack.length - 1], token.slice(9, -3));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (token.startsWith('</')) {
|
|
77
|
+
const tag = token.slice(2, -1).trim();
|
|
78
|
+
const current = stack.pop();
|
|
79
|
+
if (!current || current.tag !== tag) {
|
|
80
|
+
throw new Error(`Mismatched XML closing tag: ${tag}`);
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const inner = token.slice(1, SELF_CLOSING.test(token) ? -2 : -1).trim();
|
|
86
|
+
const firstSpace = inner.search(/\s/);
|
|
87
|
+
const tag = firstSpace === -1 ? inner : inner.slice(0, firstSpace);
|
|
88
|
+
const attrsRaw = firstSpace === -1 ? '' : inner.slice(firstSpace + 1);
|
|
89
|
+
const node: SpecXmlNode = { tag, attrs: parseAttrs(attrsRaw), children: [] };
|
|
90
|
+
stack[stack.length - 1].children.push(node);
|
|
91
|
+
if (!SELF_CLOSING.test(token)) {
|
|
92
|
+
stack.push(node);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
appendText(stack[stack.length - 1], content.slice(cursor));
|
|
97
|
+
if (stack.length !== 1) {
|
|
98
|
+
throw new Error(`Unclosed XML tag: ${stack[stack.length - 1].tag}`);
|
|
99
|
+
}
|
|
100
|
+
if (root.children.length !== 1) {
|
|
101
|
+
throw new Error('Spec XML must contain exactly one root node.');
|
|
102
|
+
}
|
|
103
|
+
return root.children[0];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function expandIncludes(node: SpecXmlNode, baseDir: string): SpecXmlNode[] {
|
|
107
|
+
if (node.tag === 'include') {
|
|
108
|
+
const href = node.attrs.href;
|
|
109
|
+
if (!href) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const includePath = path.resolve(baseDir, href);
|
|
113
|
+
const included = parseSpecXmlContent(fs.readFileSync(includePath, 'utf-8'));
|
|
114
|
+
return expandIncludes(included, path.dirname(includePath));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
node.children = node.children.flatMap((child) => expandIncludes(child, baseDir));
|
|
118
|
+
return [node];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function loadSpecXml(specPath: string): SpecXmlNode {
|
|
122
|
+
const entryPath = fs.statSync(specPath).isDirectory()
|
|
123
|
+
? path.join(specPath, 'index.xml')
|
|
124
|
+
: specPath;
|
|
125
|
+
const root = parseSpecXmlContent(fs.readFileSync(entryPath, 'utf-8'));
|
|
126
|
+
return expandIncludes(root, path.dirname(entryPath))[0];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getSpecXmlStats(root: SpecXmlNode): SpecXmlStats {
|
|
130
|
+
let requirements = 0;
|
|
131
|
+
let scenarios = 0;
|
|
132
|
+
const visit = (node: SpecXmlNode): void => {
|
|
133
|
+
if (node.tag === 'requirement') {
|
|
134
|
+
requirements++;
|
|
135
|
+
}
|
|
136
|
+
if (node.tag === 'case') {
|
|
137
|
+
scenarios++;
|
|
138
|
+
}
|
|
139
|
+
node.children.forEach(visit);
|
|
140
|
+
};
|
|
141
|
+
visit(root);
|
|
142
|
+
return { requirements, scenarios };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeSelectorTag(tag: string): string {
|
|
146
|
+
if (tag === 'requirements') return 'requirement';
|
|
147
|
+
if (tag === 'suites') return 'suite';
|
|
148
|
+
if (tag === 'cases') return 'case';
|
|
149
|
+
return tag;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function selectorPairs(selector: string): { capability: string; pairs: Array<{ tag: string; id: string }> } {
|
|
153
|
+
const parsed = parseCodumentVfsUri(selector);
|
|
154
|
+
if (parsed.scheme !== 'spec' && parsed.scheme !== 'behavior') {
|
|
155
|
+
throw new Error(`Behavior selector must use behavior:// or legacy spec://: ${selector}`);
|
|
156
|
+
}
|
|
157
|
+
const [capability, ...rest] = parsed.segments;
|
|
158
|
+
if (rest.length % 2 !== 0) {
|
|
159
|
+
throw new Error(`Behavior selector must use tag/id pairs: ${selector}`);
|
|
160
|
+
}
|
|
161
|
+
const pairs = [];
|
|
162
|
+
for (let i = 0; i < rest.length; i += 2) {
|
|
163
|
+
pairs.push({ tag: normalizeSelectorTag(rest[i]), id: rest[i + 1] });
|
|
164
|
+
}
|
|
165
|
+
return { capability, pairs };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function getSpecSelectorCapability(selector: string): string {
|
|
169
|
+
return selectorPairs(selector).capability;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function findChildIndex(parent: SpecXmlNode, pair: { tag: string; id: string }): number {
|
|
173
|
+
return parent.children.findIndex((child) => child.tag === pair.tag && child.attrs.id === pair.id);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Read the capability id from a registry root, tolerating both the current
|
|
178
|
+
* `<behaviors capability="X">` standard and the legacy `<capability id="X">` root.
|
|
179
|
+
*/
|
|
180
|
+
function rootCapability(root: SpecXmlNode): string | undefined {
|
|
181
|
+
if (root.tag === 'behaviors') return root.attrs.capability;
|
|
182
|
+
if (root.tag === 'capability') return root.attrs.id;
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function assertRootCapability(root: SpecXmlNode, capability: string, selector: string): void {
|
|
187
|
+
const actual = rootCapability(root);
|
|
188
|
+
if (actual !== capability) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Selector capability does not match root capability: ${selector} ` +
|
|
191
|
+
`(root <${root.tag}> resolves to ${actual ?? 'no capability'}, selector expects ${capability})`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function findNode(root: SpecXmlNode, selector: string): { parent: SpecXmlNode | null; node: SpecXmlNode; index: number } {
|
|
197
|
+
const { capability, pairs } = selectorPairs(selector);
|
|
198
|
+
assertRootCapability(root, capability, selector);
|
|
199
|
+
|
|
200
|
+
let parent: SpecXmlNode | null = null;
|
|
201
|
+
let node = root;
|
|
202
|
+
let index = -1;
|
|
203
|
+
for (const pair of pairs) {
|
|
204
|
+
parent = node;
|
|
205
|
+
index = findChildIndex(parent, pair);
|
|
206
|
+
if (index === -1) {
|
|
207
|
+
throw new Error(`Selector target not found: ${selector}`);
|
|
208
|
+
}
|
|
209
|
+
node = parent.children[index];
|
|
210
|
+
}
|
|
211
|
+
return { parent, node, index };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function findParentForUpsert(root: SpecXmlNode, selector: string): { parent: SpecXmlNode; pair: { tag: string; id: string } } {
|
|
215
|
+
const { capability, pairs } = selectorPairs(selector);
|
|
216
|
+
assertRootCapability(root, capability, selector);
|
|
217
|
+
if (pairs.length === 0) {
|
|
218
|
+
throw new Error('Cannot upsert the capability root.');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let parent = root;
|
|
222
|
+
for (const pair of pairs.slice(0, -1)) {
|
|
223
|
+
const index = findChildIndex(parent, pair);
|
|
224
|
+
if (index === -1) {
|
|
225
|
+
throw new Error(`Selector parent not found: ${selector}`);
|
|
226
|
+
}
|
|
227
|
+
parent = parent.children[index];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { parent, pair: pairs[pairs.length - 1] };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function findOrCreateParentForUpsert(root: SpecXmlNode, selector: string): { parent: SpecXmlNode; pair: { tag: string; id: string } } {
|
|
234
|
+
const { capability, pairs } = selectorPairs(selector);
|
|
235
|
+
assertRootCapability(root, capability, selector);
|
|
236
|
+
if (pairs.length === 0) {
|
|
237
|
+
throw new Error('Cannot upsert the capability root.');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let parent = root;
|
|
241
|
+
for (const pair of pairs.slice(0, -1)) {
|
|
242
|
+
let index = findChildIndex(parent, pair);
|
|
243
|
+
if (index === -1) {
|
|
244
|
+
const child: SpecXmlNode = { tag: pair.tag, attrs: { id: pair.id }, children: [] };
|
|
245
|
+
setSourcePath(child, parent.sourcePath);
|
|
246
|
+
parent.children.push(child);
|
|
247
|
+
index = parent.children.length - 1;
|
|
248
|
+
}
|
|
249
|
+
parent = parent.children[index];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { parent, pair: pairs[pairs.length - 1] };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function cleanPatchNode(node: SpecXmlNode): SpecXmlNode {
|
|
256
|
+
const attrs = { ...node.attrs };
|
|
257
|
+
delete attrs.op;
|
|
258
|
+
delete attrs.selector;
|
|
259
|
+
delete attrs.to;
|
|
260
|
+
return {
|
|
261
|
+
tag: node.tag,
|
|
262
|
+
attrs,
|
|
263
|
+
text: node.text,
|
|
264
|
+
children: node.children.map(cleanPatchNode),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface PatchMutation {
|
|
269
|
+
op: string;
|
|
270
|
+
selector: string;
|
|
271
|
+
to?: string;
|
|
272
|
+
node: SpecXmlNode;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function assertPatchRoot(root: SpecXmlNode): void {
|
|
276
|
+
if (!PATCH_ROOT_TAGS.has(root.tag)) {
|
|
277
|
+
throw new Error('Patch root must be <behavior-patch> or legacy <spec-patch>.');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function getPatchMutations(patchRoot: SpecXmlNode): PatchMutation[] {
|
|
282
|
+
const mutations: PatchMutation[] = [];
|
|
283
|
+
for (const child of patchRoot.children) {
|
|
284
|
+
if (WRAPPER_OP_TAGS.has(child.tag)) {
|
|
285
|
+
const selector = child.attrs.selector;
|
|
286
|
+
if (!selector) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const node = child.tag === 'upsert' ? child.children[0] : child;
|
|
290
|
+
if (!node) {
|
|
291
|
+
throw new Error('Upsert operation requires a child node.');
|
|
292
|
+
}
|
|
293
|
+
mutations.push({ op: child.tag, selector, to: child.attrs.to, node });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const op = child.attrs.op;
|
|
298
|
+
const selector = child.attrs.selector;
|
|
299
|
+
if (op && selector) {
|
|
300
|
+
mutations.push({ op, selector, to: child.attrs.to, node: child });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return mutations;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function setSourcePath(node: SpecXmlNode, sourcePath: string | undefined): void {
|
|
307
|
+
if (sourcePath) {
|
|
308
|
+
node.sourcePath = sourcePath;
|
|
309
|
+
} else {
|
|
310
|
+
delete node.sourcePath;
|
|
311
|
+
}
|
|
312
|
+
for (const child of node.children) {
|
|
313
|
+
setSourcePath(child, sourcePath);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function applySpecXmlPatchContent(specContent: string, patchContent: string): string {
|
|
318
|
+
const root = parseSpecXmlContent(specContent);
|
|
319
|
+
const patchRoot = parseSpecXmlContent(patchContent);
|
|
320
|
+
assertPatchRoot(patchRoot);
|
|
321
|
+
|
|
322
|
+
for (const mutation of getPatchMutations(patchRoot)) {
|
|
323
|
+
const { op, selector } = mutation;
|
|
324
|
+
if (op === 'upsert') {
|
|
325
|
+
const { parent, pair } = findParentForUpsert(root, selector);
|
|
326
|
+
const index = findChildIndex(parent, pair);
|
|
327
|
+
const clean = cleanPatchNode(mutation.node);
|
|
328
|
+
if (!clean.attrs.id) {
|
|
329
|
+
clean.attrs.id = pair.id;
|
|
330
|
+
}
|
|
331
|
+
if (index === -1) {
|
|
332
|
+
parent.children.push(clean);
|
|
333
|
+
} else {
|
|
334
|
+
parent.children[index] = clean;
|
|
335
|
+
}
|
|
336
|
+
} else if (op === 'delete') {
|
|
337
|
+
const target = findNode(root, selector);
|
|
338
|
+
if (!target.parent) {
|
|
339
|
+
throw new Error('Cannot delete capability root.');
|
|
340
|
+
}
|
|
341
|
+
target.parent.children.splice(target.index, 1);
|
|
342
|
+
} else if (op === 'move') {
|
|
343
|
+
const to = mutation.to;
|
|
344
|
+
if (!to) {
|
|
345
|
+
throw new Error('Move operation requires a to attribute.');
|
|
346
|
+
}
|
|
347
|
+
const target = findNode(root, selector);
|
|
348
|
+
if (!target.parent) {
|
|
349
|
+
throw new Error('Cannot move capability root.');
|
|
350
|
+
}
|
|
351
|
+
const [removed] = target.parent.children.splice(target.index, 1);
|
|
352
|
+
const { parent, pair } = findParentForUpsert(root, to);
|
|
353
|
+
removed.tag = pair.tag;
|
|
354
|
+
removed.attrs.id = pair.id;
|
|
355
|
+
const existingIndex = findChildIndex(parent, pair);
|
|
356
|
+
if (existingIndex === -1) {
|
|
357
|
+
parent.children.push(removed);
|
|
358
|
+
} else {
|
|
359
|
+
parent.children[existingIndex] = removed;
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
throw new Error(`Unsupported spec XML patch operation: ${op}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return `${serializeSpecXml(root)}\n`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function getSpecPatchCapabilities(patchContent: string): string[] {
|
|
370
|
+
const patchRoot = parseSpecXmlContent(patchContent);
|
|
371
|
+
assertPatchRoot(patchRoot);
|
|
372
|
+
|
|
373
|
+
const capabilities = new Set<string>();
|
|
374
|
+
for (const mutation of getPatchMutations(patchRoot)) {
|
|
375
|
+
const selector = mutation.selector;
|
|
376
|
+
if (selector) {
|
|
377
|
+
capabilities.add(getSpecSelectorCapability(selector));
|
|
378
|
+
}
|
|
379
|
+
const to = mutation.to;
|
|
380
|
+
if (to) {
|
|
381
|
+
capabilities.add(getSpecSelectorCapability(to));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return [...capabilities];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function resolveRegistrySpecEntry(specsDir: string, capability: string): { loadPath: string; writePath: string } {
|
|
388
|
+
const filePath = path.join(specsDir, `${capability}.xml`);
|
|
389
|
+
if (fs.existsSync(filePath)) {
|
|
390
|
+
return { loadPath: filePath, writePath: filePath };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const folderPath = path.join(specsDir, capability);
|
|
394
|
+
const indexPath = path.join(folderPath, 'index.xml');
|
|
395
|
+
if (fs.existsSync(indexPath)) {
|
|
396
|
+
return { loadPath: folderPath, writePath: indexPath };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
throw new Error(`Spec XML registry entry not found for capability: ${capability}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
interface RegistryMutationEntry {
|
|
403
|
+
root: SpecXmlNode;
|
|
404
|
+
writePath: string;
|
|
405
|
+
includedRootByPath?: Map<string, SpecXmlNode>;
|
|
406
|
+
includeNodeByPath?: Map<string, SpecXmlNode>;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function loadRegistryEntry(specsDir: string, capability: string): RegistryMutationEntry {
|
|
410
|
+
const entry = resolveRegistrySpecEntry(specsDir, capability);
|
|
411
|
+
if (fs.statSync(entry.loadPath).isDirectory()) {
|
|
412
|
+
return loadFolderRegistryEntry(entry.writePath);
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
root: loadSpecXml(entry.loadPath),
|
|
416
|
+
writePath: entry.writePath,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function cloneNodeWithoutSource(node: SpecXmlNode): SpecXmlNode {
|
|
421
|
+
return {
|
|
422
|
+
tag: node.tag,
|
|
423
|
+
attrs: { ...node.attrs },
|
|
424
|
+
text: node.text,
|
|
425
|
+
children: node.children.map(cloneNodeWithoutSource),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function markSourcePath(node: SpecXmlNode, sourcePath: string): void {
|
|
430
|
+
node.sourcePath = sourcePath;
|
|
431
|
+
for (const child of node.children) {
|
|
432
|
+
markSourcePath(child, sourcePath);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function expandRegistryIncludes(
|
|
437
|
+
node: SpecXmlNode,
|
|
438
|
+
baseDir: string,
|
|
439
|
+
includedRootByPath: Map<string, SpecXmlNode>,
|
|
440
|
+
includeNodeByPath: Map<string, SpecXmlNode>,
|
|
441
|
+
): SpecXmlNode[] {
|
|
442
|
+
if (node.tag === 'include') {
|
|
443
|
+
const href = node.attrs.href;
|
|
444
|
+
if (!href) {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
const includePath = path.resolve(baseDir, href);
|
|
448
|
+
const included = parseSpecXmlContent(fs.readFileSync(includePath, 'utf-8'));
|
|
449
|
+
markSourcePath(included, includePath);
|
|
450
|
+
included.children = included.children.flatMap((child) => expandRegistryIncludes(
|
|
451
|
+
child,
|
|
452
|
+
path.dirname(includePath),
|
|
453
|
+
includedRootByPath,
|
|
454
|
+
includeNodeByPath,
|
|
455
|
+
));
|
|
456
|
+
includedRootByPath.set(includePath, included);
|
|
457
|
+
includeNodeByPath.set(includePath, cloneNodeWithoutSource(node));
|
|
458
|
+
return [included];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
node.children = node.children.flatMap((child) => expandRegistryIncludes(
|
|
462
|
+
child,
|
|
463
|
+
node.sourcePath ? path.dirname(node.sourcePath) : baseDir,
|
|
464
|
+
includedRootByPath,
|
|
465
|
+
includeNodeByPath,
|
|
466
|
+
));
|
|
467
|
+
return [node];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function loadFolderRegistryEntry(indexPath: string): RegistryMutationEntry {
|
|
471
|
+
const includedRootByPath = new Map<string, SpecXmlNode>();
|
|
472
|
+
const includeNodeByPath = new Map<string, SpecXmlNode>();
|
|
473
|
+
const root = parseSpecXmlContent(fs.readFileSync(indexPath, 'utf-8'));
|
|
474
|
+
expandRegistryIncludes(root, path.dirname(indexPath), includedRootByPath, includeNodeByPath);
|
|
475
|
+
return {
|
|
476
|
+
root,
|
|
477
|
+
writePath: indexPath,
|
|
478
|
+
includedRootByPath,
|
|
479
|
+
includeNodeByPath,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function createRegistryEntry(specsDir: string, capability: string): RegistryMutationEntry {
|
|
484
|
+
return {
|
|
485
|
+
root: { tag: 'behaviors', attrs: { capability, version: '1' }, children: [] },
|
|
486
|
+
writePath: path.join(specsDir, `${capability}.xml`),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function applySpecXmlPatchToRegistry(patchContent: string, specsDir: string): string[] {
|
|
491
|
+
const patchRoot = parseSpecXmlContent(patchContent);
|
|
492
|
+
assertPatchRoot(patchRoot);
|
|
493
|
+
if (!fs.existsSync(specsDir)) {
|
|
494
|
+
fs.mkdirSync(specsDir, { recursive: true });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const entries = new Map<string, RegistryMutationEntry>();
|
|
498
|
+
const updated = new Set<string>();
|
|
499
|
+
|
|
500
|
+
const getExistingEntry = (capability: string): RegistryMutationEntry => {
|
|
501
|
+
const existing = entries.get(capability);
|
|
502
|
+
if (existing) {
|
|
503
|
+
return existing;
|
|
504
|
+
}
|
|
505
|
+
const entry = loadRegistryEntry(specsDir, capability);
|
|
506
|
+
entries.set(capability, entry);
|
|
507
|
+
return entry;
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const getOrCreateEntry = (capability: string): RegistryMutationEntry => {
|
|
511
|
+
const existing = entries.get(capability);
|
|
512
|
+
if (existing) {
|
|
513
|
+
return existing;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const entry = loadRegistryEntry(specsDir, capability);
|
|
517
|
+
entries.set(capability, entry);
|
|
518
|
+
return entry;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (!(error instanceof Error) || !error.message.includes('Spec XML registry entry not found')) {
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
const entry = createRegistryEntry(specsDir, capability);
|
|
524
|
+
entries.set(capability, entry);
|
|
525
|
+
return entry;
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const upsertNode = (root: SpecXmlNode, selector: string, node: SpecXmlNode): void => {
|
|
530
|
+
const { parent, pair } = findOrCreateParentForUpsert(root, selector);
|
|
531
|
+
const clean = cleanPatchNode(node);
|
|
532
|
+
if (!clean.attrs.id) {
|
|
533
|
+
clean.attrs.id = pair.id;
|
|
534
|
+
}
|
|
535
|
+
const index = findChildIndex(parent, pair);
|
|
536
|
+
const inheritedSourcePath = index === -1
|
|
537
|
+
? parent.sourcePath
|
|
538
|
+
: parent.children[index].sourcePath ?? parent.sourcePath;
|
|
539
|
+
setSourcePath(clean, inheritedSourcePath);
|
|
540
|
+
if (index === -1) {
|
|
541
|
+
parent.children.push(clean);
|
|
542
|
+
} else {
|
|
543
|
+
parent.children[index] = clean;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
for (const mutation of getPatchMutations(patchRoot)) {
|
|
548
|
+
const { op, selector } = mutation;
|
|
549
|
+
const sourceCapability = getSpecSelectorCapability(selector);
|
|
550
|
+
if (op === 'upsert') {
|
|
551
|
+
const entry = getOrCreateEntry(sourceCapability);
|
|
552
|
+
upsertNode(entry.root, selector, mutation.node);
|
|
553
|
+
updated.add(sourceCapability);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (op === 'delete') {
|
|
558
|
+
const entry = getExistingEntry(sourceCapability);
|
|
559
|
+
const target = findNode(entry.root, selector);
|
|
560
|
+
if (!target.parent) {
|
|
561
|
+
throw new Error('Cannot delete capability root.');
|
|
562
|
+
}
|
|
563
|
+
target.parent.children.splice(target.index, 1);
|
|
564
|
+
updated.add(sourceCapability);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (op === 'move') {
|
|
569
|
+
const to = mutation.to;
|
|
570
|
+
if (!to) {
|
|
571
|
+
throw new Error('Move operation requires a to attribute.');
|
|
572
|
+
}
|
|
573
|
+
const sourceEntry = getExistingEntry(sourceCapability);
|
|
574
|
+
const target = findNode(sourceEntry.root, selector);
|
|
575
|
+
if (!target.parent) {
|
|
576
|
+
throw new Error('Cannot move capability root.');
|
|
577
|
+
}
|
|
578
|
+
const [removed] = target.parent.children.splice(target.index, 1);
|
|
579
|
+
const destinationCapability = getSpecSelectorCapability(to);
|
|
580
|
+
const destinationEntry = getOrCreateEntry(destinationCapability);
|
|
581
|
+
const { parent, pair } = findOrCreateParentForUpsert(destinationEntry.root, to);
|
|
582
|
+
removed.tag = pair.tag;
|
|
583
|
+
removed.attrs.id = pair.id;
|
|
584
|
+
setSourcePath(removed, parent.sourcePath);
|
|
585
|
+
const existingIndex = findChildIndex(parent, pair);
|
|
586
|
+
if (existingIndex === -1) {
|
|
587
|
+
parent.children.push(removed);
|
|
588
|
+
} else {
|
|
589
|
+
parent.children[existingIndex] = removed;
|
|
590
|
+
}
|
|
591
|
+
updated.add(sourceCapability);
|
|
592
|
+
updated.add(destinationCapability);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
throw new Error(`Unsupported spec XML patch operation: ${op}`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
for (const [capability, entry] of [...entries.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
600
|
+
if (updated.has(capability)) {
|
|
601
|
+
writeRegistryEntry(entry);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return [...updated];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function cloneForIndex(node: SpecXmlNode, entry: RegistryMutationEntry, parentSourcePath?: string): SpecXmlNode {
|
|
609
|
+
const nodeSourcePath = node.sourcePath;
|
|
610
|
+
if (nodeSourcePath && nodeSourcePath !== parentSourcePath) {
|
|
611
|
+
const includeNode = entry.includeNodeByPath?.get(nodeSourcePath);
|
|
612
|
+
if (includeNode) {
|
|
613
|
+
return cloneNodeWithoutSource(includeNode);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const nextParentSourcePath = nodeSourcePath ?? parentSourcePath;
|
|
618
|
+
return {
|
|
619
|
+
tag: node.tag,
|
|
620
|
+
attrs: { ...node.attrs },
|
|
621
|
+
text: node.text,
|
|
622
|
+
children: node.children.map((child) => cloneForIndex(child, entry, nextParentSourcePath)),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function writeRegistryEntry(entry: RegistryMutationEntry): void {
|
|
627
|
+
if (!entry.includedRootByPath || !entry.includeNodeByPath) {
|
|
628
|
+
fs.writeFileSync(entry.writePath, `${serializeSpecXml(entry.root)}\n`, 'utf-8');
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const reachableSourcePaths = new Set<string>();
|
|
633
|
+
const collect = (node: SpecXmlNode): void => {
|
|
634
|
+
if (node.sourcePath) {
|
|
635
|
+
reachableSourcePaths.add(node.sourcePath);
|
|
636
|
+
}
|
|
637
|
+
node.children.forEach(collect);
|
|
638
|
+
};
|
|
639
|
+
collect(entry.root);
|
|
640
|
+
|
|
641
|
+
for (const [sourcePath, sourceRoot] of entry.includedRootByPath.entries()) {
|
|
642
|
+
if (reachableSourcePaths.has(sourcePath)) {
|
|
643
|
+
fs.writeFileSync(sourcePath, `${serializeSpecXml(cloneForIndex(sourceRoot, entry, sourcePath))}\n`, 'utf-8');
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
fs.writeFileSync(entry.writePath, `${serializeSpecXml(cloneForIndex(entry.root, entry))}\n`, 'utf-8');
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export function serializeSpecXml(node: SpecXmlNode, indent = 0): string {
|
|
651
|
+
const pad = ' '.repeat(indent);
|
|
652
|
+
const attrs = Object.entries(node.attrs)
|
|
653
|
+
.map(([key, value]) => ` ${key}="${encodeXml(value)}"`)
|
|
654
|
+
.join('');
|
|
655
|
+
|
|
656
|
+
if (node.children.length === 0 && !node.text) {
|
|
657
|
+
return `${pad}<${node.tag}${attrs} />`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (node.children.length === 0 && node.text) {
|
|
661
|
+
return `${pad}<${node.tag}${attrs}>${encodeXml(node.text)}</${node.tag}>`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const lines = [`${pad}<${node.tag}${attrs}>`];
|
|
665
|
+
if (node.text) {
|
|
666
|
+
lines.push(`${' '.repeat(indent + 1)}${encodeXml(node.text)}`);
|
|
667
|
+
}
|
|
668
|
+
for (const child of node.children) {
|
|
669
|
+
lines.push(serializeSpecXml(child, indent + 1));
|
|
670
|
+
}
|
|
671
|
+
lines.push(`${pad}</${node.tag}>`);
|
|
672
|
+
return lines.join('\n');
|
|
673
|
+
}
|