@cyning/harness 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,547 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import yaml from 'js-yaml';
4
+
5
+ const SCHEMA_VERSION = 'inform_graph.v3';
6
+ const FREEZE_ID = 'TECH_GRAPH_S2_FREEZE_20260519_V2_3';
7
+
8
+ export class GraphYamlError extends Error {
9
+ constructor(message, { path: filePath = null, line = null } = {}) {
10
+ super(message);
11
+ this.name = 'GraphYamlError';
12
+ this.filePath = filePath;
13
+ this.line = line;
14
+ }
15
+ }
16
+
17
+ export function loadYaml(filePath) {
18
+ const raw = fs.readFileSync(filePath, 'utf8');
19
+ try {
20
+ return yaml.load(raw);
21
+ } catch (err) {
22
+ const line = err.mark ? err.mark.line + 1 : null;
23
+ throw new GraphYamlError(`YAML 解析失败: ${err.message}`, { path: filePath, line });
24
+ }
25
+ }
26
+
27
+ export function validateGraphYaml(data, filePath = null) {
28
+ const errors = [];
29
+
30
+ if (data == null || typeof data !== 'object' || Array.isArray(data)) {
31
+ errors.push('根节点须为 object');
32
+ return errors;
33
+ }
34
+
35
+ const required = ['graph_id', 'title', 'nodes', 'edges'];
36
+ for (const key of required) {
37
+ if (!(key in data)) errors.push(`缺少必填字段: ${key}`);
38
+ }
39
+
40
+ if (data.schema_version != null && data.schema_version !== SCHEMA_VERSION) {
41
+ errors.push(`schema_version 建议为 ${SCHEMA_VERSION},实际为 ${data.schema_version}`);
42
+ }
43
+
44
+ if (data.graph_id != null && !/^[a-zA-Z0-9_]+$/.test(String(data.graph_id))) {
45
+ errors.push(`graph_id 非法: ${data.graph_id}`);
46
+ }
47
+
48
+ if (data.nodes != null) {
49
+ if (!Array.isArray(data.nodes)) {
50
+ errors.push('nodes 须为 array');
51
+ } else {
52
+ const seen = new Set();
53
+ for (let i = 0; i < data.nodes.length; i += 1) {
54
+ const n = data.nodes[i];
55
+ if (n == null || typeof n !== 'object' || Array.isArray(n)) {
56
+ errors.push(`nodes[${i}] 须为 object`);
57
+ continue;
58
+ }
59
+ if (!n.id) errors.push(`nodes[${i}] 缺少 id`);
60
+ else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(String(n.id))) {
61
+ errors.push(`nodes[${i}].id 非法: ${n.id}`);
62
+ }
63
+ if (!n.label) errors.push(`nodes[${i}] 缺少 label`);
64
+ if (n.id && seen.has(n.id)) errors.push(`重复节点 id: ${n.id}`);
65
+ if (n.id) seen.add(n.id);
66
+ if (n.kind != null && !['flow', 'struct', 'external'].includes(n.kind)) {
67
+ errors.push(`nodes[${i}].kind 非法: ${n.kind}`);
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ if (data.edges != null) {
74
+ if (!Array.isArray(data.edges)) {
75
+ errors.push('edges 须为 array');
76
+ } else {
77
+ const nodeIds = new Set((data.nodes || []).map((n) => n?.id).filter(Boolean));
78
+ for (let i = 0; i < data.edges.length; i += 1) {
79
+ const e = data.edges[i];
80
+ if (e == null || typeof e !== 'object' || Array.isArray(e)) {
81
+ errors.push(`edges[${i}] 须为 object`);
82
+ continue;
83
+ }
84
+ if (!e.from) errors.push(`edges[${i}] 缺少 from`);
85
+ if (!e.to) errors.push(`edges[${i}] 缺少 to`);
86
+ if (e.from && !nodeIds.has(e.from)) errors.push(`edges[${i}] 引用未知节点: ${e.from}`);
87
+ if (e.to && !nodeIds.has(e.to)) errors.push(`edges[${i}] 引用未知节点: ${e.to}`);
88
+ if (e.anchors != null) {
89
+ if (!Array.isArray(e.anchors)) {
90
+ errors.push(`edges[${i}].anchors 须为 array`);
91
+ } else {
92
+ for (let j = 0; j < e.anchors.length; j += 1) {
93
+ const a = e.anchors[j];
94
+ if (!a || typeof a !== 'object') {
95
+ errors.push(`edges[${i}].anchors[${j}] 须为 object`);
96
+ } else if (!a.path) {
97
+ errors.push(`edges[${i}].anchors[${j}] 缺少 path`);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ if (errors.length > 0 && filePath) {
107
+ return errors.map((e) => `${filePath}: ${e}`);
108
+ }
109
+ return errors;
110
+ }
111
+
112
+ export function allGraphIds(inputRoot) {
113
+ if (!fs.existsSync(inputRoot)) return [];
114
+ return fs
115
+ .readdirSync(inputRoot)
116
+ .filter((name) => name.endsWith('.graph.yaml'))
117
+ .map((name) => name.slice(0, -'.graph.yaml'.length))
118
+ .sort();
119
+ }
120
+
121
+ export function yamlPathFor(inputRoot, graphId) {
122
+ return path.join(inputRoot, `${graphId}.graph.yaml`);
123
+ }
124
+
125
+ export function mdPathFor(inputRoot, graphId) {
126
+ return path.join(inputRoot, `${graphId}.md`);
127
+ }
128
+
129
+ function utcNowIsoZ() {
130
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
131
+ }
132
+
133
+ function classifyLabel(label) {
134
+ const t = String(label || '').trim();
135
+ if (t.startsWith('::') && t.length > 2) return [t.slice(2).trim() || 'meta', true];
136
+ if (t.includes('~>')) return ['async_calls', false];
137
+ if (t.includes('?>') || t === '?>') return ['condition', true];
138
+ return ['depends_on', true];
139
+ }
140
+
141
+ function edgeToGraphV2(edge) {
142
+ const mark = edge.mark || '';
143
+ const label = edge.label || '';
144
+ const explicitType = edge.type || '';
145
+
146
+ let baseMark;
147
+ let baseLabel;
148
+ let inferredType;
149
+
150
+ if (mark) {
151
+ if (mark === 'classDiagram') {
152
+ baseMark = 'classDiagram';
153
+ baseLabel = '';
154
+ inferredType = 'has_metadata';
155
+ } else if (mark === '~>') {
156
+ baseMark = '~>';
157
+ baseLabel = '';
158
+ inferredType = 'async_calls';
159
+ } else if (mark === '?>') {
160
+ baseMark = '?>';
161
+ baseLabel = '';
162
+ inferredType = 'condition';
163
+ } else if (mark.startsWith('::')) {
164
+ baseMark = mark;
165
+ baseLabel = '';
166
+ inferredType = mark.slice(2) || 'meta';
167
+ } else if (mark.startsWith('[') && mark.endsWith(']')) {
168
+ baseMark = mark;
169
+ baseLabel = '';
170
+ inferredType = 'depends_on';
171
+ } else if (mark === '->') {
172
+ baseMark = '->';
173
+ baseLabel = label;
174
+ inferredType = label ? classifyLabel(label)[0] : 'depends_on';
175
+ } else {
176
+ baseMark = mark;
177
+ baseLabel = label;
178
+ inferredType = label ? classifyLabel(label)[0] : 'depends_on';
179
+ }
180
+ } else {
181
+ baseMark = '->';
182
+ baseLabel = label;
183
+ if (!label) {
184
+ inferredType = 'depends_on';
185
+ } else if (label === 'classDiagram') {
186
+ baseMark = 'classDiagram';
187
+ baseLabel = '';
188
+ inferredType = 'has_metadata';
189
+ } else if (label === '?>') {
190
+ baseMark = '?>';
191
+ baseLabel = '';
192
+ inferredType = 'condition';
193
+ } else if (label.startsWith('::')) {
194
+ baseMark = label;
195
+ baseLabel = '';
196
+ inferredType = label.slice(2) || 'meta';
197
+ } else {
198
+ inferredType = classifyLabel(label)[0];
199
+ }
200
+ }
201
+
202
+ const finalType = explicitType || inferredType;
203
+ const sync = finalType !== 'async_calls';
204
+ return { mark: baseMark, type: finalType, sync, label: baseLabel };
205
+ }
206
+
207
+ function normalizeAnchors(anchors) {
208
+ if (!anchors) return [];
209
+ return anchors.map((a) => {
210
+ const out = { path: a.path, symbol: a.symbol || '' };
211
+ if (a.line != null) out.line = a.line;
212
+ return out;
213
+ });
214
+ }
215
+
216
+ export function buildGraphPayload(inputRoot, { generatedAt = null, freezeId = FREEZE_ID } = {}) {
217
+ if (!fs.existsSync(inputRoot)) {
218
+ throw new GraphYamlError(`输入目录不存在: ${inputRoot}`);
219
+ }
220
+
221
+ const graphIds = allGraphIds(inputRoot);
222
+ const nodes = [];
223
+ const edges = [];
224
+ const graphs = [];
225
+
226
+ for (const graphId of graphIds) {
227
+ const yamlPath = yamlPathFor(inputRoot, graphId);
228
+ const data = loadYaml(yamlPath);
229
+ const validationErrors = validateGraphYaml(data, yamlPath);
230
+ if (validationErrors.length > 0) {
231
+ throw new GraphYamlError(validationErrors.join('\n'));
232
+ }
233
+
234
+ graphs.push({
235
+ id: graphId,
236
+ title: data.title,
237
+ source_yaml_path: path.relative(inputRoot, yamlPath).replace(/\\/g, '/'),
238
+ });
239
+
240
+ for (const n of data.nodes) {
241
+ nodes.push({
242
+ id: n.id,
243
+ label: n.label,
244
+ graph_id: graphId,
245
+ });
246
+ }
247
+
248
+ for (const e of data.edges) {
249
+ const { mark, type, sync, label } = edgeToGraphV2(e);
250
+ edges.push({
251
+ from: e.from,
252
+ to: e.to,
253
+ mark,
254
+ type,
255
+ sync,
256
+ label,
257
+ anchors: normalizeAnchors(e.anchors),
258
+ graph_id: graphId,
259
+ });
260
+ }
261
+ }
262
+
263
+ nodes.sort((a, b) => a.id.localeCompare(b.id));
264
+ edges.sort((a, b) => {
265
+ const keys = ['graph_id', 'from', 'to', 'mark', 'type', 'sync', 'label'];
266
+ for (const k of keys) {
267
+ const va = a[k] ?? '';
268
+ const vb = b[k] ?? '';
269
+ if (va !== vb) return String(va).localeCompare(String(vb));
270
+ }
271
+ return 0;
272
+ });
273
+
274
+ return {
275
+ schema_version: 'graph_v2',
276
+ freeze_id: freezeId,
277
+ generated_at: generatedAt || utcNowIsoZ(),
278
+ nodes,
279
+ edges,
280
+ graphs,
281
+ };
282
+ }
283
+
284
+ function formatAnchorComment(anchor) {
285
+ const p = anchor.path || '';
286
+ const symbol = anchor.symbol || '';
287
+ const line = anchor.line;
288
+ if (!p) return '';
289
+ if (line != null) return `// → ${p}#L${line}`;
290
+ if (symbol) return `// → ${p}::${symbol}`;
291
+ return `// → ${p}`;
292
+ }
293
+
294
+ function generateMermaid(data) {
295
+ const nodes = new Map((data.nodes || []).map((n) => [n.id, n]));
296
+ const direction = data.direction || 'TD';
297
+ const lines = [`flowchart ${direction}`];
298
+
299
+ for (const [nid, node] of nodes) {
300
+ const label = node.label || nid;
301
+ let shape;
302
+ if (label.startsWith('>')) {
303
+ shape = `[${label}]`;
304
+ } else if (label.includes('子流程') || label.endsWith('子流程')) {
305
+ shape = `[[${label}]]`;
306
+ } else if (nid === 'Q' || nid === 'E') {
307
+ shape = `[[${label}]]`;
308
+ } else if (nid.includes('DOC')) {
309
+ shape = `[>${label}]`;
310
+ } else {
311
+ shape = `[${label}]`;
312
+ }
313
+ lines.push(` ${nid}${shape}`);
314
+ }
315
+
316
+ lines.push('');
317
+
318
+ for (const e of data.edges || []) {
319
+ const src = e.from;
320
+ const dst = e.to;
321
+ const mark = e.mark || '->';
322
+ const label = e.label || '';
323
+
324
+ let edgeLine;
325
+ if (label) {
326
+ edgeLine = ` ${src} --"${label}"--> ${dst}`;
327
+ } else if (mark && mark !== '->') {
328
+ edgeLine = ` ${src} --"${mark}"--> ${dst}`;
329
+ } else {
330
+ edgeLine = ` ${src} --> ${dst}`;
331
+ }
332
+ lines.push(edgeLine);
333
+
334
+ for (const a of e.anchors || []) {
335
+ const comment = formatAnchorComment(a);
336
+ if (comment) lines.push(` ${comment}`);
337
+ }
338
+ }
339
+
340
+ lines.push('');
341
+ lines.push(' classDef phase fill:#e1f5fe,stroke:#01579b,stroke-width:2px');
342
+ lines.push(' classDef doc fill:#fff8e1,stroke:#ff6f00,stroke-width:1px');
343
+ lines.push(' classDef infra fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px');
344
+
345
+ const phaseNodes = [...nodes.keys()].filter((n) => ['Q', 'E', 'U1', 'U2', 'RAG', 'T2S', 'RPC', 'FTS'].includes(n));
346
+ const docNodes = [...nodes.keys()].filter((n) => n.includes('DOC'));
347
+ const infraNodes = [...nodes.keys()].filter((n) => ['AUTH', 'EV_TYPES'].includes(n));
348
+
349
+ if (phaseNodes.length) lines.push(` class ${phaseNodes.join(',')} phase`);
350
+ if (docNodes.length) lines.push(` class ${docNodes.join(',')} doc`);
351
+ if (infraNodes.length) lines.push(` class ${infraNodes.join(',')} infra`);
352
+
353
+ return lines.join('\n');
354
+ }
355
+
356
+ function generateNodeTable(data) {
357
+ const lines = ['### Nodes', '', '| ID | Label | Kind |', '|----|-------|------|'];
358
+ for (const n of data.nodes || []) {
359
+ const label = String(n.label || '').replace(/\|/g, '\\|');
360
+ const kind = n.kind || '';
361
+ lines.push(`| ${n.id} | ${label} | ${kind} |`);
362
+ }
363
+ return lines.join('\n');
364
+ }
365
+
366
+ function generateEdgeTable(data) {
367
+ const lines = [
368
+ '### Edges',
369
+ '',
370
+ '| From | To | Mark | Type | Label | Anchors |',
371
+ '|------|----|------|------|-------|---------|',
372
+ ];
373
+ for (const e of data.edges || []) {
374
+ const src = e.from || '';
375
+ const dst = e.to || '';
376
+ const mark = e.mark || '->';
377
+ const type = e.type || 'depends_on';
378
+ const label = String(e.label || '').replace(/\|/g, '\\|');
379
+ const anchors = e.anchors || [];
380
+ const anchorSummary = anchors.length ? `${anchors.length} anchor(s)` : '';
381
+ lines.push(`| ${src} | ${dst} | ${mark} | ${type} | ${label} | ${anchorSummary} |`);
382
+ }
383
+ return lines.join('\n');
384
+ }
385
+
386
+ function generateNotesSection(data) {
387
+ const notes = data.notes;
388
+ if (!notes) return '';
389
+ let body;
390
+ if (typeof notes === 'string') body = notes;
391
+ else if (Array.isArray(notes)) body = notes.join('\n\n');
392
+ else body = String(notes);
393
+ return `\n\n## Notes\n\n${body}\n`;
394
+ }
395
+
396
+ function generateSubGraphLinks(graphId) {
397
+ if (graphId !== '00_main') return '';
398
+ return [
399
+ '## Sub-graph Links',
400
+ '',
401
+ "- `Struct`: [`01_struct.md`](01_struct.md)(手写 · 无 `.graph.yaml`)",
402
+ "- `Version`: [`02_version.md`](02_version.md)(手写 · 无 `.graph.yaml`)",
403
+ "- 子图编辑源见 `docs/_tech_graph/*.graph.yaml`",
404
+ '',
405
+ ].join('\n');
406
+ }
407
+
408
+ export function generateMarkdown(data, { sourcePath = null } = {}) {
409
+ const graphId = data.graph_id || 'main';
410
+ const title = data.title || graphId;
411
+ const description = data.description || '';
412
+ const version = data.version || '';
413
+ const generatedAt = utcNowIsoZ();
414
+ const src = sourcePath || `docs/_tech_graph/${graphId}.graph.yaml`;
415
+
416
+ const frontmatter = `---
417
+ graph_id: ${graphId}
418
+ version: ${version}
419
+ generated_at: ${generatedAt}
420
+ source: ${src}
421
+ ---
422
+ `;
423
+
424
+ const header = `# ${title}\n\n${description}`.trim();
425
+ const mermaid = generateMermaid(data);
426
+ const subLinks = generateSubGraphLinks(graphId);
427
+ const notes = generateNotesSection(data);
428
+
429
+ const body = `${header}
430
+
431
+ ## Mermaid
432
+
433
+ \`\`\`mermaid
434
+ ${mermaid}
435
+ \`\`\`
436
+
437
+ ## Structured Data
438
+
439
+ ${generateNodeTable(data)}
440
+
441
+ ${generateEdgeTable(data)}${notes}${subLinks ? `\n\n${subLinks}` : ''}
442
+ `;
443
+
444
+ return frontmatter + '\n' + body;
445
+ }
446
+
447
+ export function compileGraph(graphId, inputRoot, outputPath = null) {
448
+ const yamlPath = yamlPathFor(inputRoot, graphId);
449
+ if (!fs.existsSync(yamlPath)) {
450
+ throw new GraphYamlError(`YAML 源不存在: ${yamlPath}`);
451
+ }
452
+ const data = loadYaml(yamlPath);
453
+ const validationErrors = validateGraphYaml(data, yamlPath);
454
+ if (validationErrors.length > 0) {
455
+ throw new GraphYamlError(validationErrors.join('\n'));
456
+ }
457
+ const md = generateMarkdown(data, { sourcePath: path.relative(inputRoot, yamlPath).replace(/\\/g, '/') });
458
+ const outPath = outputPath || mdPathFor(inputRoot, graphId);
459
+ fs.writeFileSync(outPath, md, 'utf8');
460
+ return outPath;
461
+ }
462
+
463
+ export function loadGraphJson(graphJsonPath) {
464
+ if (!fs.existsSync(graphJsonPath)) return null;
465
+ const raw = fs.readFileSync(graphJsonPath, 'utf8');
466
+ try {
467
+ return JSON.parse(raw);
468
+ } catch (err) {
469
+ throw new GraphYamlError(`graph.json 解析失败: ${err.message}`, { path: graphJsonPath });
470
+ }
471
+ }
472
+
473
+ export function extractGraphJsonSlice(graphJson, graphId) {
474
+ const nodes = (graphJson?.nodes || []).filter((n) => n?.graph_id === graphId);
475
+ const edges = (graphJson?.edges || []).filter(
476
+ (e) => e?.graph_id === graphId && 'from' in e && 'to' in e,
477
+ );
478
+ return { nodes, edges };
479
+ }
480
+
481
+ export function diffGraphYaml(graphId, inputRoot, graphJsonPath) {
482
+ const yamlPath = yamlPathFor(inputRoot, graphId);
483
+ if (!fs.existsSync(yamlPath)) {
484
+ throw new GraphYamlError(`YAML 源不存在: ${yamlPath}`);
485
+ }
486
+ const yamlData = loadYaml(yamlPath);
487
+ const validationErrors = validateGraphYaml(yamlData, yamlPath);
488
+ if (validationErrors.length > 0) {
489
+ throw new GraphYamlError(validationErrors.join('\n'));
490
+ }
491
+
492
+ if (!fs.existsSync(graphJsonPath)) {
493
+ return { ok: false, diff: `graph.json 不存在: ${graphJsonPath}` };
494
+ }
495
+
496
+ const graphJson = loadGraphJson(graphJsonPath);
497
+ const jsonNodes = (graphJson?.nodes || []).filter((n) => n?.graph_id === graphId);
498
+ const jsonEdges = (graphJson?.edges || []).filter(
499
+ (e) => e?.graph_id === graphId && 'from' in e && 'to' in e,
500
+ );
501
+
502
+ const yamlNodes = new Map((yamlData.nodes || []).map((n) => [n.id, n]));
503
+ const yamlNodeIds = new Set(yamlNodes.keys());
504
+ const jsonNodeIds = new Set(jsonNodes.map((n) => n.id));
505
+
506
+ const diffs = [];
507
+
508
+ if (yamlNodeIds.size !== jsonNodeIds.size || ![...yamlNodeIds].every((id) => jsonNodeIds.has(id))) {
509
+ const onlyYaml = [...yamlNodeIds].filter((id) => !jsonNodeIds.has(id));
510
+ const onlyJson = [...jsonNodeIds].filter((id) => !yamlNodeIds.has(id));
511
+ if (onlyYaml.length) diffs.push(`Nodes only in YAML: ${onlyYaml.sort().join(', ')}`);
512
+ if (onlyJson.length) diffs.push(`Nodes only in JSON: ${onlyJson.sort().join(', ')}`);
513
+ }
514
+
515
+ if ((yamlData.nodes || []).length !== jsonNodes.length) {
516
+ diffs.push(`Node count mismatch: YAML=${(yamlData.nodes || []).length}, JSON=${jsonNodes.length}`);
517
+ }
518
+
519
+ const yamlEdgeSet = new Set(
520
+ (yamlData.edges || []).map((e) => {
521
+ const { mark, type } = edgeToGraphV2(e);
522
+ return JSON.stringify([e.from, e.to, mark, type]);
523
+ }),
524
+ );
525
+ const jsonEdgeSet = new Set(
526
+ jsonEdges.map((e) => JSON.stringify([e.from, e.to, e.mark || '->', e.type || 'depends_on'])),
527
+ );
528
+
529
+ if (yamlEdgeSet.size !== jsonEdgeSet.size || ![...yamlEdgeSet].every((k) => jsonEdgeSet.has(k))) {
530
+ const onlyYaml = [...yamlEdgeSet].filter((k) => !jsonEdgeSet.has(k)).map((k) => JSON.parse(k));
531
+ const onlyJson = [...jsonEdgeSet].filter((k) => !yamlEdgeSet.has(k)).map((k) => JSON.parse(k));
532
+ if (onlyYaml.length) diffs.push(`Edges only in YAML: ${JSON.stringify(onlyYaml)}`);
533
+ if (onlyJson.length) diffs.push(`Edges only in JSON: ${JSON.stringify(onlyJson)}`);
534
+ }
535
+
536
+ if ((yamlData.edges || []).length !== jsonEdges.length) {
537
+ diffs.push(`Edge count mismatch: YAML=${(yamlData.edges || []).length}, JSON=${jsonEdges.length}`);
538
+ }
539
+
540
+ if (diffs.length === 0) return { ok: true, diff: '' };
541
+ return { ok: false, diff: diffs.join('\n') };
542
+ }
543
+
544
+ export function checkGraph(graphId, inputRoot, graphJsonPath) {
545
+ const result = diffGraphYaml(graphId, inputRoot, graphJsonPath);
546
+ return result;
547
+ }
package/ontology.yaml CHANGED
@@ -3,7 +3,7 @@
3
3
  # 冲突时以 Markdown 为准 · 供未来 harness ontology-check 使用
4
4
 
5
5
  version: "1.2"
6
- product_semver: "1.0.2"
6
+ product_semver: "1.1.0"
7
7
  license: MIT
8
8
 
9
9
  classes:
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@cyning/harness",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "cyning-harness discipline package · init / upgrade / check CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "dependencies": {
8
+ "js-yaml": "^4.1.0"
9
+ },
7
10
  "bin": {
8
11
  "harness": "bin/harness.js",
9
12
  "cyning-harness": "bin/harness.js"
@@ -0,0 +1,134 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cyning.dev/schemas/cyning-harness/inform_graph.v3.json",
4
+ "title": "cyning-harness Inform Graph YAML v3",
5
+ "description": "Graph Source v3 编辑源 schema · YAML → Mermaid MD + graph.json(Inform 轨 · v1.1+)",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["graph_id", "title", "nodes", "edges"],
9
+ "properties": {
10
+ "schema_version": {
11
+ "type": "string",
12
+ "const": "inform_graph.v3",
13
+ "description": "Inform Graph YAML schema 版本(v1.1+ 产品化后固定)"
14
+ },
15
+ "graph_id": {
16
+ "type": "string",
17
+ "pattern": "^[a-zA-Z0-9_]+$",
18
+ "description": "图 ID · 与文件名 <graph_id>.graph.yaml 一致"
19
+ },
20
+ "title": {
21
+ "type": "string",
22
+ "minLength": 1,
23
+ "description": "人类可读图标题"
24
+ },
25
+ "description": {
26
+ "type": "string",
27
+ "description": "图用途简述"
28
+ },
29
+ "version": {
30
+ "type": "string",
31
+ "description": "编辑源版本号或日期"
32
+ },
33
+ "direction": {
34
+ "type": "string",
35
+ "enum": ["TD", "TB", "BT", "LR", "RL"],
36
+ "default": "TD",
37
+ "description": "Mermaid flowchart 方向"
38
+ },
39
+ "notes": {
40
+ "oneOf": [
41
+ { "type": "string" },
42
+ { "type": "array", "items": { "type": "string" } }
43
+ ],
44
+ "description": "附加说明 · compile 后写入 MD"
45
+ },
46
+ "nodes": {
47
+ "type": "array",
48
+ "items": { "$ref": "#/$defs/node" },
49
+ "minItems": 1
50
+ },
51
+ "edges": {
52
+ "type": "array",
53
+ "items": { "$ref": "#/$defs/edge" }
54
+ }
55
+ },
56
+ "$defs": {
57
+ "node": {
58
+ "type": "object",
59
+ "additionalProperties": false,
60
+ "required": ["id", "label"],
61
+ "properties": {
62
+ "id": {
63
+ "type": "string",
64
+ "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
65
+ "description": "节点唯一标识 · Mermaid 节点 ID"
66
+ },
67
+ "label": {
68
+ "type": "string",
69
+ "minLength": 1,
70
+ "description": "节点显示标签"
71
+ },
72
+ "kind": {
73
+ "type": "string",
74
+ "enum": ["flow", "struct", "external"],
75
+ "description": "节点种类(可选)"
76
+ }
77
+ }
78
+ },
79
+ "edge": {
80
+ "type": "object",
81
+ "additionalProperties": false,
82
+ "required": ["from", "to"],
83
+ "properties": {
84
+ "from": {
85
+ "type": "string",
86
+ "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
87
+ "description": "源节点 ID"
88
+ },
89
+ "to": {
90
+ "type": "string",
91
+ "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
92
+ "description": "目标节点 ID"
93
+ },
94
+ "mark": {
95
+ "type": "string",
96
+ "description": "Mermaid 箭头标记 · 如 -> ~> ?> ::branches [ok]"
97
+ },
98
+ "label": {
99
+ "type": "string",
100
+ "description": "边语义标签"
101
+ },
102
+ "type": {
103
+ "type": "string",
104
+ "description": "边类型 · 覆盖 mark 推断"
105
+ },
106
+ "anchors": {
107
+ "type": "array",
108
+ "items": { "$ref": "#/$defs/anchor" }
109
+ }
110
+ }
111
+ },
112
+ "anchor": {
113
+ "type": "object",
114
+ "additionalProperties": false,
115
+ "required": ["path"],
116
+ "properties": {
117
+ "path": {
118
+ "type": "string",
119
+ "minLength": 1,
120
+ "description": "代码/文档相对路径"
121
+ },
122
+ "symbol": {
123
+ "type": "string",
124
+ "description": "符号名 · 与 line 二选一或共存"
125
+ },
126
+ "line": {
127
+ "type": "integer",
128
+ "minimum": 1,
129
+ "description": "行号 · 与 symbol 二选一或共存"
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }