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,230 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { parseXnl, stringifyLineBlock, wordToString, XnlParseError } from 'xnl-core';
|
|
4
|
+
import type { XnlNode, DataElementNode, XnlWord } from 'xnl-core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* codument engineering registry adapter.
|
|
8
|
+
*
|
|
9
|
+
* The registry is a plain, host-git-versioned working tree of `.xnl` files under
|
|
10
|
+
* `codument/engineering/<plane>/<category>/...`. We load/save via xnl-core (parse +
|
|
11
|
+
* lineBlock formatter) and index nodes by their namespaced id. No parallel vcs repo
|
|
12
|
+
* is persisted; node-level 3-way merge (see merge.ts) uses xnl-vfs ephemerally.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface EngineeringNodeRef {
|
|
16
|
+
/** Namespaced id, e.g. `backend.howto.orders.add_endpoint`. */
|
|
17
|
+
id: string;
|
|
18
|
+
node: DataElementNode;
|
|
19
|
+
/** Path relative to the engineering dir, e.g. `backend/howto/orders.xnl`. */
|
|
20
|
+
file: string;
|
|
21
|
+
/** `engineering://<plane>/<category>/<topic>/<name>` derived from the file path + id. */
|
|
22
|
+
uri: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EngineeringRegistry {
|
|
26
|
+
dir: string;
|
|
27
|
+
/** relPath -> top-level nodes of that file (preserves order for save). */
|
|
28
|
+
files: Map<string, XnlNode[]>;
|
|
29
|
+
/** namespaced id -> ref. */
|
|
30
|
+
index: Map<string, EngineeringNodeRef>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isDataElement(node: XnlNode | undefined): node is DataElementNode {
|
|
34
|
+
return Boolean(node && typeof node === 'object' && (node as DataElementNode).kind === 'DataElement');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Read a node's namespaced id from `#word` or `metadata.id`. */
|
|
38
|
+
export function readNodeId(node: XnlNode): string | undefined {
|
|
39
|
+
if (!isDataElement(node)) return undefined;
|
|
40
|
+
const fromWord = wordToString((node as { id?: XnlWord }).id);
|
|
41
|
+
if (fromWord) return fromWord;
|
|
42
|
+
const metaId = node.metadata?.id;
|
|
43
|
+
if (typeof metaId === 'string') return metaId;
|
|
44
|
+
if (metaId && typeof metaId === 'object' && (metaId as XnlWord).kind === 'Word') {
|
|
45
|
+
return wordToString(metaId as XnlWord);
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Last segment of a namespaced id (`a.b.name` -> `name`). */
|
|
51
|
+
export function nodeName(id: string): string {
|
|
52
|
+
const parts = id.split('.');
|
|
53
|
+
return parts[parts.length - 1];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Namespace prefix of a namespaced id (`a.b.name` -> `a.b`, `name` -> ``).
|
|
58
|
+
* The namespace is the id minus its trailing `name` segment; a bare name yields
|
|
59
|
+
* the empty string. Used by id↔path alignment and engineering-uri derivation.
|
|
60
|
+
*/
|
|
61
|
+
export function idNamespace(id: string): string {
|
|
62
|
+
const parts = id.split('.');
|
|
63
|
+
return parts.slice(0, -1).join('.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Build `engineering://<plane>/<category>/<topic>/<name>` from a file path and node id. */
|
|
67
|
+
export function engineeringUri(relFile: string, id: string): string {
|
|
68
|
+
const segs = relFile.split(path.sep).filter(Boolean);
|
|
69
|
+
const plane = segs[0] ?? 'global';
|
|
70
|
+
const category = segs[1] ?? 'overview';
|
|
71
|
+
const topic = (segs[2] ?? 'index').replace(/\.xnl$/i, '');
|
|
72
|
+
return `engineering://${plane}/${category}/${topic}/${nodeName(id)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isHidden(name: string): boolean {
|
|
76
|
+
return name.startsWith('.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function walkXnlFiles(dir: string, base: string, out: string[]): void {
|
|
80
|
+
if (!fs.existsSync(dir)) return;
|
|
81
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
82
|
+
if (isHidden(entry.name)) continue; // skip .node-meta, .tmp, .xnl-vcs, etc.
|
|
83
|
+
const abs = path.join(dir, entry.name);
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
walkXnlFiles(abs, base, out);
|
|
86
|
+
} else if (entry.isFile() && entry.name.endsWith('.xnl')) {
|
|
87
|
+
out.push(path.relative(base, abs));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Load the engineering registry from a working tree directory. */
|
|
93
|
+
export function loadEngineeringRegistry(dir: string): EngineeringRegistry {
|
|
94
|
+
const files = new Map<string, XnlNode[]>();
|
|
95
|
+
const index = new Map<string, EngineeringNodeRef>();
|
|
96
|
+
const relFiles: string[] = [];
|
|
97
|
+
walkXnlFiles(dir, dir, relFiles);
|
|
98
|
+
relFiles.sort();
|
|
99
|
+
|
|
100
|
+
for (const relFile of relFiles) {
|
|
101
|
+
const content = fs.readFileSync(path.join(dir, relFile), 'utf-8');
|
|
102
|
+
const nodes = parseXnl(content, { textBlockStyle: true }).nodes;
|
|
103
|
+
files.set(relFile, nodes);
|
|
104
|
+
for (const node of nodes) {
|
|
105
|
+
const id = readNodeId(node);
|
|
106
|
+
if (!id || !isDataElement(node)) continue;
|
|
107
|
+
if (index.has(id)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Duplicate engineering node id '${id}' in '${relFile}' and '${index.get(id)!.file}'`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
index.set(id, { id, node, file: relFile, uri: engineeringUri(relFile, id) });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { dir, files, index };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A non-fatal load issue collected by {@link loadEngineeringRegistrySafe}: an XNL
|
|
121
|
+
* parse error (`syntax`) or a cross-file duplicate id (`duplicate-id`). The
|
|
122
|
+
* validate engine maps these onto its own finding shape.
|
|
123
|
+
*/
|
|
124
|
+
export interface LoadIssue {
|
|
125
|
+
kind: 'syntax' | 'duplicate-id';
|
|
126
|
+
/** Path relative to the registry dir. */
|
|
127
|
+
file: string;
|
|
128
|
+
/** 1-based line, when known (parse errors carry one). */
|
|
129
|
+
line?: number;
|
|
130
|
+
message: string;
|
|
131
|
+
/** For duplicate ids: the other file that already defined the id. */
|
|
132
|
+
otherFile?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface SafeRegistryResult {
|
|
136
|
+
registry: EngineeringRegistry;
|
|
137
|
+
issues: LoadIssue[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Load the engineering registry without throwing. Files that fail XNL parsing are
|
|
142
|
+
* skipped (their error is collected as a `syntax` issue); duplicate ids are
|
|
143
|
+
* collected as `duplicate-id` issues (first definition wins in the index)
|
|
144
|
+
* instead of aborting the load. The non-safe {@link loadEngineeringRegistry}
|
|
145
|
+
* keeps its throwing contract for existing callers (lint, merge, archive).
|
|
146
|
+
*/
|
|
147
|
+
export function loadEngineeringRegistrySafe(dir: string): SafeRegistryResult {
|
|
148
|
+
const files = new Map<string, XnlNode[]>();
|
|
149
|
+
const index = new Map<string, EngineeringNodeRef>();
|
|
150
|
+
const issues: LoadIssue[] = [];
|
|
151
|
+
const relFiles: string[] = [];
|
|
152
|
+
walkXnlFiles(dir, dir, relFiles);
|
|
153
|
+
relFiles.sort();
|
|
154
|
+
|
|
155
|
+
for (const relFile of relFiles) {
|
|
156
|
+
const content = fs.readFileSync(path.join(dir, relFile), 'utf-8');
|
|
157
|
+
let nodes: XnlNode[];
|
|
158
|
+
try {
|
|
159
|
+
nodes = parseXnl(content, { textBlockStyle: true }).nodes;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (err instanceof XnlParseError) {
|
|
162
|
+
issues.push({ kind: 'syntax', file: relFile, line: err.line, message: err.message });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
files.set(relFile, nodes);
|
|
168
|
+
for (const node of nodes) {
|
|
169
|
+
const id = readNodeId(node);
|
|
170
|
+
if (!id || !isDataElement(node)) continue;
|
|
171
|
+
const existing = index.get(id);
|
|
172
|
+
if (existing) {
|
|
173
|
+
issues.push({
|
|
174
|
+
kind: 'duplicate-id',
|
|
175
|
+
file: relFile,
|
|
176
|
+
otherFile: existing.file,
|
|
177
|
+
message: `Duplicate engineering node id '${id}' in '${relFile}' and '${existing.file}'`,
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
index.set(id, { id, node, file: relFile, uri: engineeringUri(relFile, id) });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { registry: { dir, files, index }, issues };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface SerializeOptions {
|
|
189
|
+
/**
|
|
190
|
+
* Marker factory for markerless text blocks. Defaults to xnl-core's ULID factory,
|
|
191
|
+
* which preserves XNL text nodes' escape-free property (a markerless `?>` block
|
|
192
|
+
* gets a stable unique marker on first write, then round-trips unchanged).
|
|
193
|
+
* Tests may inject a deterministic factory; production MUST keep the ULID default.
|
|
194
|
+
*/
|
|
195
|
+
textMarkerFactory?: () => string;
|
|
196
|
+
/**
|
|
197
|
+
* Block text style (default true for engineering files): render each text element's
|
|
198
|
+
* content on its own indented line(s) between `<tag ?m>` and `</?m>`, so registry
|
|
199
|
+
* files stay readable and diff-friendly. Paired with `parseXnl(.., {textBlockStyle})`.
|
|
200
|
+
*/
|
|
201
|
+
textBlockStyle?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Serialize top-level nodes of a file to XNL (lineBlock pretty, block text style). */
|
|
205
|
+
export function serializeEngineeringFile(nodes: XnlNode[], opts: SerializeOptions = {}): string {
|
|
206
|
+
return nodes.map((n) => stringifyLineBlock(n, { textBlockStyle: true, ...opts })).join('\n\n') + '\n';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Write one engineering file back to the working tree. */
|
|
210
|
+
export function saveEngineeringFile(
|
|
211
|
+
dir: string,
|
|
212
|
+
relFile: string,
|
|
213
|
+
nodes: XnlNode[],
|
|
214
|
+
opts: SerializeOptions = {},
|
|
215
|
+
): void {
|
|
216
|
+
const abs = path.join(dir, relFile);
|
|
217
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
218
|
+
fs.writeFileSync(abs, serializeEngineeringFile(nodes, opts), 'utf-8');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Save the whole registry back to its working tree. */
|
|
222
|
+
export function saveEngineeringRegistry(
|
|
223
|
+
registry: EngineeringRegistry,
|
|
224
|
+
targetDir = registry.dir,
|
|
225
|
+
opts: SerializeOptions = {},
|
|
226
|
+
): void {
|
|
227
|
+
for (const [relFile, nodes] of registry.files) {
|
|
228
|
+
saveEngineeringFile(targetDir, relFile, nodes, opts);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { XnlNode, DataElementNode, TextElementNode } from 'xnl-core';
|
|
2
|
+
import { isDataElement, readNodeId, type EngineeringRegistry } from './registry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Node schema validation for the engineering registry: kind vocabulary and
|
|
6
|
+
* minimal required representations per engineering knowledge kind.
|
|
7
|
+
* See std/spec/engineering-node-schema.md.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const ENGINEERING_KINDS = [
|
|
11
|
+
'overview',
|
|
12
|
+
'howto',
|
|
13
|
+
'rule',
|
|
14
|
+
'example',
|
|
15
|
+
'reference',
|
|
16
|
+
'troubleshooting',
|
|
17
|
+
'runbook',
|
|
18
|
+
'code-map',
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
type Element = DataElementNode | TextElementNode;
|
|
22
|
+
|
|
23
|
+
function isElement(node: XnlNode | undefined): node is Element {
|
|
24
|
+
return Boolean(
|
|
25
|
+
node && typeof node === 'object' &&
|
|
26
|
+
((node as Element).kind === 'DataElement' || (node as Element).kind === 'TextElement'),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function bodyChildren(node: DataElementNode): Element[] {
|
|
31
|
+
return (node.body ?? []).filter(isElement);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function childTags(node: DataElementNode): Set<string> {
|
|
35
|
+
return new Set(bodyChildren(node).map((c) => c.tag));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function metaString(node: DataElementNode, key: string): string | undefined {
|
|
39
|
+
const v = node.metadata?.[key];
|
|
40
|
+
return typeof v === 'string' ? v : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function nodeKind(node: DataElementNode): string | undefined {
|
|
44
|
+
return metaString(node, 'kind');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validateEngineeringNode(node: XnlNode): string[] {
|
|
48
|
+
const errors: string[] = [];
|
|
49
|
+
if (!isDataElement(node)) return errors;
|
|
50
|
+
const id = readNodeId(node);
|
|
51
|
+
const where = id ? `#${id}` : `<${node.tag}>`;
|
|
52
|
+
|
|
53
|
+
if (!id) {
|
|
54
|
+
errors.push(`${where}: node has no id (use #<plane>.<category>.<topic>.<name>)`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const kind = nodeKind(node);
|
|
58
|
+
if (!kind) {
|
|
59
|
+
errors.push(`${where}: missing 'kind'`);
|
|
60
|
+
return errors;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const isShell = kind.includes(':');
|
|
64
|
+
if (!isShell && !(ENGINEERING_KINDS as readonly string[]).includes(kind)) {
|
|
65
|
+
errors.push(`${where}: unknown engineering kind '${kind}'`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tags = childTags(node);
|
|
69
|
+
const req = (cond: boolean, msg: string) => {
|
|
70
|
+
if (!cond) errors.push(`${where} (kind=${kind}): ${msg}`);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
switch (kind) {
|
|
74
|
+
case 'overview':
|
|
75
|
+
req(tags.has('desc'), 'overview requires a <desc> block');
|
|
76
|
+
req(tags.has('mental-model'), 'overview requires a <mental-model> block');
|
|
77
|
+
break;
|
|
78
|
+
case 'howto':
|
|
79
|
+
req(tags.has('when-to-use'), 'howto requires a <when-to-use> block');
|
|
80
|
+
req(tags.has('steps'), 'howto requires a <steps> block');
|
|
81
|
+
req(tags.has('verification'), 'howto requires a <verification> block');
|
|
82
|
+
break;
|
|
83
|
+
case 'rule':
|
|
84
|
+
req(tags.has('rule'), 'rule requires a <rule> block');
|
|
85
|
+
req(tags.has('rationale'), 'rule requires a <rationale> block');
|
|
86
|
+
req(tags.has('enforcement'), 'rule requires an <enforcement> block');
|
|
87
|
+
break;
|
|
88
|
+
case 'example':
|
|
89
|
+
req(tags.has('scenario'), 'example requires a <scenario> block');
|
|
90
|
+
req(tags.has('walkthrough'), 'example requires a <walkthrough> block');
|
|
91
|
+
break;
|
|
92
|
+
case 'reference':
|
|
93
|
+
req(tags.has('scope'), 'reference requires a <scope> block');
|
|
94
|
+
req(tags.has('source-of-truth'), 'reference requires a <source-of-truth> block');
|
|
95
|
+
req(tags.has('update-procedure'), 'reference requires an <update-procedure> block');
|
|
96
|
+
break;
|
|
97
|
+
case 'troubleshooting':
|
|
98
|
+
req(tags.has('symptoms'), 'troubleshooting requires a <symptoms> block');
|
|
99
|
+
req(tags.has('diagnosis'), 'troubleshooting requires a <diagnosis> block');
|
|
100
|
+
req(tags.has('fix'), 'troubleshooting requires a <fix> block');
|
|
101
|
+
break;
|
|
102
|
+
case 'runbook':
|
|
103
|
+
req(tags.has('preconditions'), 'runbook requires a <preconditions> block');
|
|
104
|
+
req(tags.has('steps'), 'runbook requires a <steps> block');
|
|
105
|
+
req(tags.has('verification'), 'runbook requires a <verification> block');
|
|
106
|
+
req(tags.has('rollback'), 'runbook requires a <rollback> block');
|
|
107
|
+
break;
|
|
108
|
+
case 'code-map':
|
|
109
|
+
req(tags.has('scope'), 'code-map requires a <scope> block');
|
|
110
|
+
req(tags.has('paths'), 'code-map requires a <paths> block');
|
|
111
|
+
req(tags.has('update-procedure'), 'code-map requires an <update-procedure> block');
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return errors;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function validateEngineeringRegistry(registry: EngineeringRegistry): string[] {
|
|
121
|
+
const errors: string[] = [];
|
|
122
|
+
for (const nodes of registry.files.values()) {
|
|
123
|
+
for (const node of nodes) errors.push(...validateEngineeringNode(node));
|
|
124
|
+
}
|
|
125
|
+
return errors;
|
|
126
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import type { DataElementNode, AttributeMap } from 'xnl-core';
|
|
3
|
+
import {
|
|
4
|
+
loadEngineeringRegistrySafe,
|
|
5
|
+
isDataElement,
|
|
6
|
+
readNodeId,
|
|
7
|
+
nodeName,
|
|
8
|
+
} from './registry';
|
|
9
|
+
import { validateEngineeringNode } from './schema';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Engineering validation engine. Checks every engineering `.xnl` file across
|
|
13
|
+
* XNL syntax, node-schema semantics, hierarchy, and URI reference shape.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type ValidateLayer = 'syntax' | 'schema' | 'hierarchy';
|
|
17
|
+
export type ValidateSeverity = 'error' | 'warning';
|
|
18
|
+
|
|
19
|
+
export interface ValidateFinding {
|
|
20
|
+
file: string;
|
|
21
|
+
line?: number;
|
|
22
|
+
layer: ValidateLayer;
|
|
23
|
+
severity: ValidateSeverity;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ValidateMode = 'registry' | 'deltas';
|
|
28
|
+
|
|
29
|
+
export interface ValidateOptions {
|
|
30
|
+
/**
|
|
31
|
+
* registry: `<plane>/<category>/<topic>.xnl` or `<plane>/<category>/<topic>/index.xnl`.
|
|
32
|
+
* deltas: same layout under a track's `engineering_deltas/`.
|
|
33
|
+
*/
|
|
34
|
+
mode?: ValidateMode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const KNOWN_PLANES = new Set([
|
|
38
|
+
'global',
|
|
39
|
+
'backend',
|
|
40
|
+
'surface',
|
|
41
|
+
'runtime',
|
|
42
|
+
'storage',
|
|
43
|
+
'pipelines',
|
|
44
|
+
'agents',
|
|
45
|
+
'operations',
|
|
46
|
+
'cli',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const KNOWN_CATEGORIES = new Set([
|
|
50
|
+
'overview',
|
|
51
|
+
'howto',
|
|
52
|
+
'rules',
|
|
53
|
+
'examples',
|
|
54
|
+
'reference',
|
|
55
|
+
'troubleshooting',
|
|
56
|
+
'runbooks',
|
|
57
|
+
'code-map',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const ENGINEERING_SCHEME = 'engineering://';
|
|
61
|
+
const MODELING_SCHEME = 'modeling://';
|
|
62
|
+
const BEHAVIOR_SCHEME = 'behavior://';
|
|
63
|
+
const DECISION_SCHEME = 'decision://';
|
|
64
|
+
|
|
65
|
+
interface PathLoc {
|
|
66
|
+
plane: string;
|
|
67
|
+
category: string;
|
|
68
|
+
topic: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pathLoc(relFile: string): PathLoc {
|
|
72
|
+
const segs = relFile.split(path.sep).filter(Boolean);
|
|
73
|
+
const plane = segs[0] ?? '';
|
|
74
|
+
const category = segs[1] ?? '';
|
|
75
|
+
let topic = segs[2] ?? '';
|
|
76
|
+
if (topic === 'index.xnl' && segs.length >= 3) topic = segs[1] ?? '';
|
|
77
|
+
topic = topic.replace(/\.xnl$/i, '');
|
|
78
|
+
return { plane, category, topic };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function collectRefs(meta: AttributeMap | undefined): string[] {
|
|
82
|
+
const out: string[] = [];
|
|
83
|
+
const visit = (v: unknown): void => {
|
|
84
|
+
if (typeof v === 'string') {
|
|
85
|
+
if (
|
|
86
|
+
v.startsWith(ENGINEERING_SCHEME) ||
|
|
87
|
+
v.startsWith(MODELING_SCHEME) ||
|
|
88
|
+
v.startsWith(BEHAVIOR_SCHEME) ||
|
|
89
|
+
v.startsWith(DECISION_SCHEME)
|
|
90
|
+
) {
|
|
91
|
+
out.push(v);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(v)) {
|
|
96
|
+
for (const item of v) visit(item);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (v && typeof v === 'object') {
|
|
100
|
+
for (const item of Object.values(v as Record<string, unknown>)) visit(item);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
if (meta) for (const v of Object.values(meta)) visit(v);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkIdPathAlignment(node: DataElementNode, relFile: string, loc: PathLoc): ValidateFinding[] {
|
|
108
|
+
const findings: ValidateFinding[] = [];
|
|
109
|
+
const id = readNodeId(node);
|
|
110
|
+
if (!id) return findings;
|
|
111
|
+
const parts = id.split('.');
|
|
112
|
+
|
|
113
|
+
if (parts.length < 4) {
|
|
114
|
+
findings.push({
|
|
115
|
+
file: relFile,
|
|
116
|
+
layer: 'hierarchy',
|
|
117
|
+
severity: 'error',
|
|
118
|
+
message: `#${id}: id must be '#<plane>.<category>.<topic>.<name>'`,
|
|
119
|
+
});
|
|
120
|
+
return findings;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const [idPlane, idCategory, idTopic] = parts;
|
|
124
|
+
if (idPlane !== loc.plane) {
|
|
125
|
+
findings.push({
|
|
126
|
+
file: relFile,
|
|
127
|
+
layer: 'hierarchy',
|
|
128
|
+
severity: 'error',
|
|
129
|
+
message: `#${id}: id plane '${idPlane}' does not match path plane '${loc.plane}' (${relFile})`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (idCategory !== loc.category) {
|
|
133
|
+
findings.push({
|
|
134
|
+
file: relFile,
|
|
135
|
+
layer: 'hierarchy',
|
|
136
|
+
severity: 'error',
|
|
137
|
+
message: `#${id}: id category '${idCategory}' does not match path category '${loc.category}' (${relFile})`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (idTopic !== loc.topic) {
|
|
141
|
+
findings.push({
|
|
142
|
+
file: relFile,
|
|
143
|
+
layer: 'hierarchy',
|
|
144
|
+
severity: 'error',
|
|
145
|
+
message: `#${id}: id topic '${idTopic}' does not match path topic '${loc.topic}' (${relFile})`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return findings;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function checkReferences(node: DataElementNode, relFile: string, knownEngineeringUris: Set<string>): ValidateFinding[] {
|
|
153
|
+
const findings: ValidateFinding[] = [];
|
|
154
|
+
const id = readNodeId(node);
|
|
155
|
+
const where = id ? `#${id}` : `<${node.tag}>`;
|
|
156
|
+
|
|
157
|
+
for (const ref of collectRefs(node.metadata)) {
|
|
158
|
+
if (ref.startsWith(ENGINEERING_SCHEME)) {
|
|
159
|
+
if (!knownEngineeringUris.has(ref)) {
|
|
160
|
+
findings.push({
|
|
161
|
+
file: relFile,
|
|
162
|
+
layer: 'hierarchy',
|
|
163
|
+
severity: 'error',
|
|
164
|
+
message: `${where}: dangling engineering reference '${ref}'`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const [scheme, minParts] = ref.startsWith(MODELING_SCHEME)
|
|
171
|
+
? [MODELING_SCHEME, 3]
|
|
172
|
+
: ref.startsWith(BEHAVIOR_SCHEME)
|
|
173
|
+
? [BEHAVIOR_SCHEME, 2]
|
|
174
|
+
: ref.startsWith(DECISION_SCHEME)
|
|
175
|
+
? [DECISION_SCHEME, 1]
|
|
176
|
+
: ['', 0];
|
|
177
|
+
if (!scheme) continue;
|
|
178
|
+
const rest = ref.slice(scheme.length);
|
|
179
|
+
if (rest.split('/').filter(Boolean).length < minParts) {
|
|
180
|
+
findings.push({
|
|
181
|
+
file: relFile,
|
|
182
|
+
layer: 'hierarchy',
|
|
183
|
+
severity: 'error',
|
|
184
|
+
message: `${where}: malformed reference '${ref}'`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return findings;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function checkLayout(relFiles: Iterable<string>): ValidateFinding[] {
|
|
193
|
+
const findings: ValidateFinding[] = [];
|
|
194
|
+
const planes = new Set<string>();
|
|
195
|
+
for (const relFile of relFiles) {
|
|
196
|
+
const loc = pathLoc(relFile);
|
|
197
|
+
if (loc.plane) planes.add(loc.plane);
|
|
198
|
+
if (!loc.plane || !loc.category || !loc.topic) {
|
|
199
|
+
findings.push({
|
|
200
|
+
file: relFile,
|
|
201
|
+
layer: 'hierarchy',
|
|
202
|
+
severity: 'error',
|
|
203
|
+
message: `engineering file path must be '<plane>/<category>/<topic>.xnl' or '<plane>/<category>/<topic>/index.xnl'`,
|
|
204
|
+
});
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!KNOWN_CATEGORIES.has(loc.category)) {
|
|
208
|
+
findings.push({
|
|
209
|
+
file: relFile,
|
|
210
|
+
layer: 'hierarchy',
|
|
211
|
+
severity: 'warning',
|
|
212
|
+
message: `unknown engineering category '${loc.category}' (custom categories are allowed; verify it is intentional)`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!planes.has('global')) {
|
|
218
|
+
findings.push({
|
|
219
|
+
file: '.',
|
|
220
|
+
layer: 'hierarchy',
|
|
221
|
+
severity: 'warning',
|
|
222
|
+
message: `engineering registry has no 'global' plane; this is allowed but cross-plane knowledge usually belongs there`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const plane of planes) {
|
|
227
|
+
if (plane && !KNOWN_PLANES.has(plane)) {
|
|
228
|
+
findings.push({
|
|
229
|
+
file: '.',
|
|
230
|
+
layer: 'hierarchy',
|
|
231
|
+
severity: 'warning',
|
|
232
|
+
message: `unknown engineering plane '${plane}' (custom planes are allowed; verify it is intentional)`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return findings;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function validateEngineeringTree(dir: string, opts: ValidateOptions = {}): ValidateFinding[] {
|
|
241
|
+
void opts;
|
|
242
|
+
const findings: ValidateFinding[] = [];
|
|
243
|
+
const { registry, issues } = loadEngineeringRegistrySafe(dir);
|
|
244
|
+
|
|
245
|
+
for (const issue of issues) {
|
|
246
|
+
if (issue.kind === 'syntax') {
|
|
247
|
+
findings.push({
|
|
248
|
+
file: issue.file,
|
|
249
|
+
line: issue.line,
|
|
250
|
+
layer: 'syntax',
|
|
251
|
+
severity: 'error',
|
|
252
|
+
message: issue.message,
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
findings.push({ file: issue.file, layer: 'hierarchy', severity: 'error', message: issue.message });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const knownEngineeringUris = new Set<string>();
|
|
260
|
+
for (const [relFile, nodes] of registry.files) {
|
|
261
|
+
const loc = pathLoc(relFile);
|
|
262
|
+
for (const node of nodes) {
|
|
263
|
+
const id = readNodeId(node);
|
|
264
|
+
if (!id || !isDataElement(node)) continue;
|
|
265
|
+
knownEngineeringUris.add(`${ENGINEERING_SCHEME}${loc.plane}/${loc.category}/${loc.topic}/${nodeName(id)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const [relFile, nodes] of registry.files) {
|
|
270
|
+
const loc = pathLoc(relFile);
|
|
271
|
+
for (const node of nodes) {
|
|
272
|
+
if (!isDataElement(node)) continue;
|
|
273
|
+
for (const msg of validateEngineeringNode(node)) {
|
|
274
|
+
findings.push({ file: relFile, layer: 'schema', severity: 'error', message: msg });
|
|
275
|
+
}
|
|
276
|
+
findings.push(...checkIdPathAlignment(node, relFile, loc));
|
|
277
|
+
findings.push(...checkReferences(node, relFile, knownEngineeringUris));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const layoutFiles = new Set<string>(registry.files.keys());
|
|
282
|
+
for (const issue of issues) if (issue.kind === 'syntax') layoutFiles.add(issue.file);
|
|
283
|
+
findings.push(...checkLayout(layoutFiles));
|
|
284
|
+
|
|
285
|
+
return findings;
|
|
286
|
+
}
|