spec-wave 2.1.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 +327 -0
- package/README.zh-CN.md +327 -0
- package/RELEASING.md +52 -0
- package/assets/README.md +30 -0
- package/assets/ci/samples/README.md +76 -0
- package/assets/ci/samples/hgm-ingest.yml.example +42 -0
- package/assets/ci/samples/lint-wiki-delta.pin.yml.example +47 -0
- package/assets/ci/samples/lint-wiki-delta.yml.example +49 -0
- package/assets/ci/samples/pytest.yml.example +49 -0
- package/assets/ci/samples/quality.yml.example +56 -0
- package/assets/ci/samples/skills-validate.yml.example +38 -0
- package/assets/ci/samples/tech-graph.yml.example +55 -0
- package/assets/coding_wiki/templates/README.md +115 -0
- package/assets/coding_wiki/templates/_index.md +19 -0
- package/assets/coding_wiki/templates/context.md +18 -0
- package/assets/coding_wiki/templates/stable.md +26 -0
- package/assets/coding_wiki/templates/topics/wiki_layout.md +13 -0
- package/assets/coding_wiki/templates/topics/wikilinks_export.md +13 -0
- package/assets/coding_wiki/templates/volatile.md +25 -0
- package/assets/docs/POINTER_ONBOARDING.md +9 -0
- package/assets/docs/POINTER_RUNBOOK_wiki_delta.md +18 -0
- package/assets/docs/POINTER_SDD_HAT_FLOW.md +8 -0
- package/assets/docs/POINTER_USER_GUIDE.md +8 -0
- package/assets/graph/stubs/README.md +6 -0
- package/assets/graph/templates/00_main.graph.yaml +72 -0
- package/assets/graph/templates/00_main.md +85 -0
- package/assets/graph/templates/01_struct.md +38 -0
- package/assets/graph/templates/02_version.md +7 -0
- package/assets/graph/templates/10_flow_MAIN.graph.yaml +105 -0
- package/assets/graph/templates/10_flow_MAIN.md +103 -0
- package/assets/graph/templates/99_mermaid_protocol.md +156 -0
- package/assets/graph/templates/README.md +66 -0
- package/assets/harness/README.md +18 -0
- package/assets/harness/discipline-coverage.yaml +320 -0
- package/assets/harness/invokes/README.md +22 -0
- package/assets/harness/invokes/TEMPLATE_invoke.md +61 -0
- package/assets/harness/lifecycle.yaml +127 -0
- package/assets/harness/prompts/00-orchestrator.md +91 -0
- package/assets/harness/prompts/10-spec-requirements.md +63 -0
- package/assets/harness/prompts/10-task-requirements.md +70 -0
- package/assets/harness/prompts/20-spec-audit.md +50 -0
- package/assets/harness/prompts/20-task-audit.md +90 -0
- package/assets/harness/prompts/30-execute-code.md +64 -0
- package/assets/harness/prompts/40-self-check.md +65 -0
- package/assets/harness/prompts/FRAGMENT_00_delegate_only_v1_zh.md +19 -0
- package/assets/harness/prompts/FRAGMENT_30_gate_verify_v1_zh.md +34 -0
- package/assets/harness/prompts/FRAGMENT_30_invoke_block_v1_zh.md +28 -0
- package/assets/harness/prompts/FRAGMENT_hat_reanchor_v1_zh.md +19 -0
- package/assets/harness/prompts/README.md +67 -0
- package/assets/harness/prompts/TEMPLATE_30_gate_stop.md +36 -0
- package/assets/harness/templates/FRAGMENT_task_domain_infer_v1_zh.md +16 -0
- package/assets/harness/templates/ONTOLOGY_consumer_slice_v1.md +41 -0
- package/assets/harness/templates/QUICKREF_v1_zh.md +35 -0
- package/assets/harness/templates/README.md +50 -0
- package/assets/harness/templates/TASK_TEMPLATE.md +133 -0
- package/assets/harness/templates/TASK_done_README.md +76 -0
- package/assets/harness/templates/TASK_epic.md +103 -0
- package/assets/harness/templates/TASK_graph_bootstrap.md +115 -0
- package/assets/harness/templates/VIEW_done_by_domain.md +66 -0
- package/assets/harness/templates/VIEW_done_thin_pointer.md +16 -0
- package/assets/ide/adapters/AGENTS.md.fragment.example +33 -0
- package/assets/ide/adapters/CLAUDE.md.fragment.example +32 -0
- package/assets/ide/adapters/README.md +57 -0
- package/assets/ide/adapters/cursor-harness-starter.mdc.example +39 -0
- package/assets/ide/commands/claude/kit/apply-standards.md +10 -0
- package/assets/ide/commands/claude/kit/gate-status.md +10 -0
- package/assets/ide/commands/claude/kit/graph-check.md +8 -0
- package/assets/ide/commands/claude/kit/hat-00-delegate.md +9 -0
- package/assets/ide/commands/claude/kit/hat-10-spec.md +9 -0
- package/assets/ide/commands/claude/kit/hat-10-task.md +9 -0
- package/assets/ide/commands/claude/kit/hat-20-spec-audit.md +9 -0
- package/assets/ide/commands/claude/kit/hat-20-task-audit.md +9 -0
- package/assets/ide/commands/claude/kit/hat-reanchor.md +8 -0
- package/assets/ide/commands/claude/kit/init-guide.md +16 -0
- package/assets/ide/commands/claude/kit/sync-prompts-guide.md +9 -0
- package/assets/ide/commands/claude/kit/verify.md +16 -0
- package/assets/ide/commands/cursor/kit-apply-standards.md +12 -0
- package/assets/ide/commands/cursor/kit-gate-status.md +12 -0
- package/assets/ide/commands/cursor/kit-graph-check.md +10 -0
- package/assets/ide/commands/cursor/kit-hat-00-delegate.md +11 -0
- package/assets/ide/commands/cursor/kit-hat-10-spec.md +11 -0
- package/assets/ide/commands/cursor/kit-hat-10-task.md +11 -0
- package/assets/ide/commands/cursor/kit-hat-20-spec-audit.md +11 -0
- package/assets/ide/commands/cursor/kit-hat-20-task-audit.md +11 -0
- package/assets/ide/commands/cursor/kit-hat-reanchor.md +10 -0
- package/assets/ide/commands/cursor/kit-init-guide.md +18 -0
- package/assets/ide/commands/cursor/kit-sync-prompts-guide.md +11 -0
- package/assets/ide/commands/cursor/kit-verify.md +18 -0
- package/assets/ide/host-adapt/README.md +113 -0
- package/assets/ide/host-adapt/examples/mvp-hosts.yaml +78 -0
- package/assets/ide/host-adapt/host-adapt.schema.json +97 -0
- package/assets/ide/skills-orch/README.md +16 -0
- package/assets/ide/skills-orch/kit-apply-standards/SKILL.md +17 -0
- package/assets/ide/skills-orch/kit-gate-status/SKILL.md +17 -0
- package/assets/ide/skills-orch/kit-hat-reanchor/SKILL.md +15 -0
- package/assets/ide/skills-orch/kit-init-guide/SKILL.md +23 -0
- package/assets/ide/skills-orch/kit-verify/SKILL.md +23 -0
- package/assets/ontology.yaml +113 -0
- package/assets/skills/README.md +37 -0
- package/assets/skills/harness-00-delegate-only/SKILL.md +19 -0
- package/assets/skills/harness-10-spec/SKILL.md +63 -0
- package/assets/skills/harness-10-task/SKILL.md +70 -0
- package/assets/skills/harness-20-spec-audit/SKILL.md +50 -0
- package/assets/skills/harness-20-task-audit/SKILL.md +90 -0
- package/assets/skills/harness-20-task-audit/references/TEMPLATE_30_gate_stop.md +36 -0
- package/assets/skills/harness-hat-reanchor/SKILL.md +19 -0
- package/assets/standards/POINTER_workspace_truth_v1_zh.md +42 -0
- package/assets/standards/README.md +35 -0
- package/assets/standards/SOURCES_v1_zh.md +49 -0
- package/assets/standards/TEMPLATE_CODING_BASELINE_L1_v1_zh.md +88 -0
- package/assets/standards/TEMPLATE_CODING_BASELINE_L2_backend_v1_zh.md +75 -0
- package/assets/standards/TEMPLATE_CODING_BASELINE_L2_frontend_v1_zh.md +76 -0
- package/bin/dsh-coding-kit.js +7 -0
- package/bin/specgate.js +7 -0
- package/bin/validate_template_compliance.py +470 -0
- package/cordis.patch.yml +5 -0
- package/lib/cli-checks.d.ts +68 -0
- package/lib/cli-checks.js +812 -0
- package/lib/cli-checks.js.map +1 -0
- package/lib/cli-graph-hgm.d.ts +74 -0
- package/lib/cli-graph-hgm.js +416 -0
- package/lib/cli-graph-hgm.js.map +1 -0
- package/lib/cli-graph-yaml.d.ts +79 -0
- package/lib/cli-graph-yaml.js +602 -0
- package/lib/cli-graph-yaml.js.map +1 -0
- package/lib/cli-graph.d.ts +1 -0
- package/lib/cli-graph.js +199 -0
- package/lib/cli-graph.js.map +1 -0
- package/lib/cli-host.d.ts +24 -0
- package/lib/cli-host.js +1197 -0
- package/lib/cli-host.js.map +1 -0
- package/lib/cli-lifecycle.d.ts +2 -0
- package/lib/cli-lifecycle.js +401 -0
- package/lib/cli-lifecycle.js.map +1 -0
- package/lib/cli-refresh-ide-blocks.d.ts +66 -0
- package/lib/cli-refresh-ide-blocks.js +484 -0
- package/lib/cli-refresh-ide-blocks.js.map +1 -0
- package/lib/cli-shared.d.ts +74 -0
- package/lib/cli-shared.js +281 -0
- package/lib/cli-shared.js.map +1 -0
- package/lib/cli-skills.d.ts +52 -0
- package/lib/cli-skills.js +363 -0
- package/lib/cli-skills.js.map +1 -0
- package/lib/cli-status.d.ts +2 -0
- package/lib/cli-status.js +251 -0
- package/lib/cli-status.js.map +1 -0
- package/lib/cli-sync-prompts.d.ts +25 -0
- package/lib/cli-sync-prompts.js +158 -0
- package/lib/cli-sync-prompts.js.map +1 -0
- package/lib/cli-sync.d.ts +2 -0
- package/lib/cli-sync.js +124 -0
- package/lib/cli-sync.js.map +1 -0
- package/lib/cli-task-extra.d.ts +53 -0
- package/lib/cli-task-extra.js +408 -0
- package/lib/cli-task-extra.js.map +1 -0
- package/lib/cli-timeline.d.ts +28 -0
- package/lib/cli-timeline.js +68 -0
- package/lib/cli-timeline.js.map +1 -0
- package/lib/cli-wiki.d.ts +21 -0
- package/lib/cli-wiki.js +189 -0
- package/lib/cli-wiki.js.map +1 -0
- package/lib/cli.d.ts +22 -0
- package/lib/cli.js +1034 -0
- package/lib/cli.js.map +1 -0
- package/lib/host-contract.d.ts +23 -0
- package/lib/host-contract.js +143 -0
- package/lib/host-contract.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +290 -0
- package/lib/index.js.map +1 -0
- package/lib/yaml.d.ts +4 -0
- package/lib/yaml.js +10 -0
- package/lib/yaml.js.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export declare const SCHEMA_VERSION = "inform_graph.v3";
|
|
2
|
+
export declare class GraphYamlError extends Error {
|
|
3
|
+
readonly filePath: string | null;
|
|
4
|
+
readonly line: number | null;
|
|
5
|
+
constructor(message: string, opts?: {
|
|
6
|
+
path?: string | null;
|
|
7
|
+
line?: number | null;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
type YamlNode = {
|
|
11
|
+
id?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
kind?: string;
|
|
14
|
+
};
|
|
15
|
+
type YamlEdge = {
|
|
16
|
+
from?: string;
|
|
17
|
+
to?: string;
|
|
18
|
+
mark?: string;
|
|
19
|
+
type?: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
anchors?: {
|
|
22
|
+
path?: string;
|
|
23
|
+
symbol?: string;
|
|
24
|
+
line?: number;
|
|
25
|
+
}[];
|
|
26
|
+
};
|
|
27
|
+
type YamlGraph = {
|
|
28
|
+
graph_id?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
version?: string;
|
|
32
|
+
direction?: string;
|
|
33
|
+
schema_version?: string;
|
|
34
|
+
notes?: unknown;
|
|
35
|
+
nodes?: YamlNode[];
|
|
36
|
+
edges?: YamlEdge[];
|
|
37
|
+
};
|
|
38
|
+
export declare function loadYaml(filePath: string): YamlGraph;
|
|
39
|
+
export declare function validateGraphYaml(data: YamlGraph | null | undefined, filePath?: string | null): string[];
|
|
40
|
+
export declare function allGraphIds(inputRoot: string, opts?: {
|
|
41
|
+
recursive?: boolean;
|
|
42
|
+
maxDepth?: number;
|
|
43
|
+
skipDirs?: string[];
|
|
44
|
+
includeShared?: boolean;
|
|
45
|
+
}): string[];
|
|
46
|
+
export declare function yamlPathFor(inputRoot: string, graphId: string): string;
|
|
47
|
+
export declare function mdPathFor(inputRoot: string, graphId: string): string;
|
|
48
|
+
export declare function resolveGraphJsonPath(inputRoot: string, explicitPath?: string | null): string;
|
|
49
|
+
/**
|
|
50
|
+
* 源内容派生戳:同 UTF-8 字节 → 同 stamp。形态 `sha256-<16hex>`,YAML 当纯量(非嵌套键)。
|
|
51
|
+
* 用于 compile md frontmatter 与 export graph.json 的 generated_at,去掉 wall-clock。
|
|
52
|
+
*/
|
|
53
|
+
export declare function contentStamp(source: string): string;
|
|
54
|
+
export declare function buildGraphPayload(inputRoot: string, opts?: {
|
|
55
|
+
generatedAt?: string | null;
|
|
56
|
+
freezeId?: string;
|
|
57
|
+
recursive?: boolean;
|
|
58
|
+
}): Record<string, unknown>;
|
|
59
|
+
export declare function exportGraphJson(inputRoot: string, opts?: {
|
|
60
|
+
outPath?: string | null;
|
|
61
|
+
recursive?: boolean;
|
|
62
|
+
generatedAt?: string | null;
|
|
63
|
+
freezeId?: string;
|
|
64
|
+
}): {
|
|
65
|
+
outPath: string;
|
|
66
|
+
payload: Record<string, unknown>;
|
|
67
|
+
};
|
|
68
|
+
export declare function generateMarkdown(data: YamlGraph, opts?: {
|
|
69
|
+
sourcePath?: string | null;
|
|
70
|
+
sourceRaw?: string | null;
|
|
71
|
+
generatedAt?: string | null;
|
|
72
|
+
}): string;
|
|
73
|
+
export declare function compileGraph(graphId: string, inputRoot: string, outputPath?: string | null): string;
|
|
74
|
+
export declare function loadGraphJson(graphJsonPath: string): Record<string, unknown> | null;
|
|
75
|
+
export declare function checkGraph(graphId: string, inputRoot: string, graphJsonPath: string): {
|
|
76
|
+
ok: boolean;
|
|
77
|
+
diff: string;
|
|
78
|
+
};
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { yamlLoad } from "./yaml.js";
|
|
5
|
+
export const SCHEMA_VERSION = 'inform_graph.v3';
|
|
6
|
+
const FREEZE_ID = 'TECH_GRAPH_S2_FREEZE_20260519_V2_3';
|
|
7
|
+
const DEFAULT_SKIP_DIRS = new Set(['node_modules', '.git', 'shared']);
|
|
8
|
+
export class GraphYamlError extends Error {
|
|
9
|
+
constructor(message, opts = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'GraphYamlError';
|
|
12
|
+
this.filePath = opts.path ?? null;
|
|
13
|
+
this.line = opts.line ?? null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function loadYaml(filePath) {
|
|
17
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
18
|
+
try {
|
|
19
|
+
return yamlLoad(raw);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const e = err;
|
|
23
|
+
const line = e.mark?.line != null ? e.mark.line + 1 : null;
|
|
24
|
+
throw new GraphYamlError(`YAML 解析失败: ${e.message ?? String(err)}`, { path: filePath, line });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function validateGraphYaml(data, filePath = null) {
|
|
28
|
+
const errors = [];
|
|
29
|
+
if (data == null || typeof data !== 'object' || Array.isArray(data)) {
|
|
30
|
+
errors.push('根节点须为 object');
|
|
31
|
+
return errors;
|
|
32
|
+
}
|
|
33
|
+
const required = ['graph_id', 'title', 'nodes', 'edges'];
|
|
34
|
+
for (const key of required) {
|
|
35
|
+
if (!(key in data))
|
|
36
|
+
errors.push(`缺少必填字段: ${key}`);
|
|
37
|
+
}
|
|
38
|
+
if (data.schema_version != null && data.schema_version !== SCHEMA_VERSION) {
|
|
39
|
+
errors.push(`schema_version 建议为 ${SCHEMA_VERSION},实际为 ${data.schema_version}`);
|
|
40
|
+
}
|
|
41
|
+
// DEF-032③:声明值(data.graph_id)为 graph_id 唯一真值源,须为裸 slug(禁 /);
|
|
42
|
+
// 路径命名空间 id(如 l0/00_main)仅作输入兼容定位(allGraphIds / --graph-id),不参与校验与输出。
|
|
43
|
+
if (data.graph_id != null && !/^[a-zA-Z0-9_]+$/.test(String(data.graph_id))) {
|
|
44
|
+
errors.push(`graph_id 非法: ${data.graph_id}`);
|
|
45
|
+
}
|
|
46
|
+
if (data.nodes != null) {
|
|
47
|
+
if (!Array.isArray(data.nodes))
|
|
48
|
+
errors.push('nodes 须为 array');
|
|
49
|
+
else {
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
data.nodes.forEach((n, i) => {
|
|
52
|
+
if (n == null || typeof n !== 'object' || Array.isArray(n)) {
|
|
53
|
+
errors.push(`nodes[${i}] 须为 object`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!n.id)
|
|
57
|
+
errors.push(`nodes[${i}] 缺少 id`);
|
|
58
|
+
else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(String(n.id))) {
|
|
59
|
+
errors.push(`nodes[${i}].id 非法: ${n.id}`);
|
|
60
|
+
}
|
|
61
|
+
if (!n.label)
|
|
62
|
+
errors.push(`nodes[${i}] 缺少 label`);
|
|
63
|
+
if (n.id && seen.has(n.id))
|
|
64
|
+
errors.push(`重复节点 id: ${n.id}`);
|
|
65
|
+
if (n.id)
|
|
66
|
+
seen.add(n.id);
|
|
67
|
+
if (n.kind != null && !['flow', 'struct', 'external'].includes(n.kind)) {
|
|
68
|
+
errors.push(`nodes[${i}].kind 非法: ${n.kind}`);
|
|
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
|
+
data.edges.forEach((e, i) => {
|
|
79
|
+
if (e == null || typeof e !== 'object' || Array.isArray(e)) {
|
|
80
|
+
errors.push(`edges[${i}] 须为 object`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!e.from)
|
|
84
|
+
errors.push(`edges[${i}] 缺少 from`);
|
|
85
|
+
if (!e.to)
|
|
86
|
+
errors.push(`edges[${i}] 缺少 to`);
|
|
87
|
+
if (e.from && !nodeIds.has(e.from))
|
|
88
|
+
errors.push(`edges[${i}] 引用未知节点: ${e.from}`);
|
|
89
|
+
if (e.to && !nodeIds.has(e.to))
|
|
90
|
+
errors.push(`edges[${i}] 引用未知节点: ${e.to}`);
|
|
91
|
+
if (e.anchors != null) {
|
|
92
|
+
if (!Array.isArray(e.anchors))
|
|
93
|
+
errors.push(`edges[${i}].anchors 须为 array`);
|
|
94
|
+
else {
|
|
95
|
+
e.anchors.forEach((a, j) => {
|
|
96
|
+
if (!a || typeof a !== 'object')
|
|
97
|
+
errors.push(`edges[${i}].anchors[${j}] 须为 object`);
|
|
98
|
+
else if (!a.path)
|
|
99
|
+
errors.push(`edges[${i}].anchors[${j}] 缺少 path`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (errors.length > 0 && filePath)
|
|
107
|
+
return errors.map((e) => `${filePath}: ${e}`);
|
|
108
|
+
return errors;
|
|
109
|
+
}
|
|
110
|
+
export function allGraphIds(inputRoot, opts = {}) {
|
|
111
|
+
if (!existsSync(inputRoot))
|
|
112
|
+
return [];
|
|
113
|
+
const recursive = opts.recursive !== false;
|
|
114
|
+
const maxDepth = opts.maxDepth ?? 8;
|
|
115
|
+
const skipDirs = new Set(opts.skipDirs || [...DEFAULT_SKIP_DIRS]);
|
|
116
|
+
if (opts.includeShared)
|
|
117
|
+
skipDirs.delete('shared');
|
|
118
|
+
if (!recursive) {
|
|
119
|
+
return readdirSync(inputRoot)
|
|
120
|
+
.filter((name) => name.endsWith('.graph.yaml'))
|
|
121
|
+
.map((name) => name.slice(0, -'.graph.yaml'.length))
|
|
122
|
+
.sort();
|
|
123
|
+
}
|
|
124
|
+
const found = new Map();
|
|
125
|
+
const walk = (dir, depth, relPrefix) => {
|
|
126
|
+
if (depth > maxDepth)
|
|
127
|
+
return;
|
|
128
|
+
let entries;
|
|
129
|
+
try {
|
|
130
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
for (const ent of entries) {
|
|
136
|
+
const name = ent.name;
|
|
137
|
+
if (ent.isDirectory()) {
|
|
138
|
+
if (skipDirs.has(name))
|
|
139
|
+
continue;
|
|
140
|
+
const nextRel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
141
|
+
walk(path.join(dir, name), depth + 1, nextRel);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!ent.isFile() || !name.endsWith('.graph.yaml'))
|
|
145
|
+
continue;
|
|
146
|
+
const id = relPrefix
|
|
147
|
+
? `${relPrefix}/${name.slice(0, -'.graph.yaml'.length)}`
|
|
148
|
+
: name.slice(0, -'.graph.yaml'.length);
|
|
149
|
+
const abs = path.join(dir, name);
|
|
150
|
+
if (found.has(id)) {
|
|
151
|
+
throw new GraphYamlError(`graphId 冲突: "${id}"\n - ${found.get(id)}\n - ${abs}`);
|
|
152
|
+
}
|
|
153
|
+
found.set(id, abs);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
walk(inputRoot, 0, '');
|
|
157
|
+
return [...found.keys()].sort();
|
|
158
|
+
}
|
|
159
|
+
export function yamlPathFor(inputRoot, graphId) {
|
|
160
|
+
return path.join(inputRoot, `${graphId}.graph.yaml`);
|
|
161
|
+
}
|
|
162
|
+
export function mdPathFor(inputRoot, graphId) {
|
|
163
|
+
return path.join(inputRoot, `${graphId}.md`);
|
|
164
|
+
}
|
|
165
|
+
export function resolveGraphJsonPath(inputRoot, explicitPath = null) {
|
|
166
|
+
if (explicitPath)
|
|
167
|
+
return explicitPath;
|
|
168
|
+
const shared = path.join(inputRoot, 'shared', 'graph.json');
|
|
169
|
+
if (existsSync(shared))
|
|
170
|
+
return shared;
|
|
171
|
+
return path.join(inputRoot, 'graph.json');
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 源内容派生戳:同 UTF-8 字节 → 同 stamp。形态 `sha256-<16hex>`,YAML 当纯量(非嵌套键)。
|
|
175
|
+
* 用于 compile md frontmatter 与 export graph.json 的 generated_at,去掉 wall-clock。
|
|
176
|
+
*/
|
|
177
|
+
export function contentStamp(source) {
|
|
178
|
+
const hex = createHash('sha256').update(source, 'utf8').digest('hex').slice(0, 16);
|
|
179
|
+
return `sha256-${hex}`;
|
|
180
|
+
}
|
|
181
|
+
function stampYamlCorpus(inputRoot, graphIds) {
|
|
182
|
+
const parts = [];
|
|
183
|
+
for (const graphId of graphIds) {
|
|
184
|
+
parts.push(`${graphId}\n${readFileSync(yamlPathFor(inputRoot, graphId), 'utf8')}`);
|
|
185
|
+
}
|
|
186
|
+
return contentStamp(parts.join('\0'));
|
|
187
|
+
}
|
|
188
|
+
function classifyLabel(label) {
|
|
189
|
+
const t = String(label || '').trim();
|
|
190
|
+
if (t.startsWith('::') && t.length > 2)
|
|
191
|
+
return [t.slice(2).trim() || 'meta', true];
|
|
192
|
+
if (t.includes('~>'))
|
|
193
|
+
return ['async_calls', false];
|
|
194
|
+
if (t.includes('?>') || t === '?>')
|
|
195
|
+
return ['condition', true];
|
|
196
|
+
return ['depends_on', true];
|
|
197
|
+
}
|
|
198
|
+
function edgeToGraphV2(edge) {
|
|
199
|
+
const mark = edge.mark || '';
|
|
200
|
+
const label = edge.label || '';
|
|
201
|
+
const explicitType = edge.type || '';
|
|
202
|
+
let baseMark;
|
|
203
|
+
let baseLabel;
|
|
204
|
+
let inferredType;
|
|
205
|
+
if (mark) {
|
|
206
|
+
if (mark === 'classDiagram') {
|
|
207
|
+
baseMark = 'classDiagram';
|
|
208
|
+
baseLabel = '';
|
|
209
|
+
inferredType = 'has_metadata';
|
|
210
|
+
}
|
|
211
|
+
else if (mark === '~>') {
|
|
212
|
+
baseMark = '~>';
|
|
213
|
+
// DEF-031(D1):拓扑协议标记作为边属性呈现,不丢弃 label 文本
|
|
214
|
+
baseLabel = label;
|
|
215
|
+
inferredType = 'async_calls';
|
|
216
|
+
}
|
|
217
|
+
else if (mark === '?>') {
|
|
218
|
+
baseMark = '?>';
|
|
219
|
+
baseLabel = label;
|
|
220
|
+
inferredType = 'condition';
|
|
221
|
+
}
|
|
222
|
+
else if (mark.startsWith('::')) {
|
|
223
|
+
baseMark = mark;
|
|
224
|
+
baseLabel = label;
|
|
225
|
+
inferredType = mark.slice(2) || 'meta';
|
|
226
|
+
}
|
|
227
|
+
else if (mark.startsWith('[') && mark.endsWith(']')) {
|
|
228
|
+
baseMark = mark;
|
|
229
|
+
baseLabel = label;
|
|
230
|
+
inferredType = 'depends_on';
|
|
231
|
+
}
|
|
232
|
+
else if (mark === '->') {
|
|
233
|
+
baseMark = '->';
|
|
234
|
+
baseLabel = label;
|
|
235
|
+
inferredType = label ? classifyLabel(label)[0] : 'depends_on';
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
baseMark = mark;
|
|
239
|
+
baseLabel = label;
|
|
240
|
+
inferredType = label ? classifyLabel(label)[0] : 'depends_on';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
baseMark = '->';
|
|
245
|
+
baseLabel = label;
|
|
246
|
+
if (!label)
|
|
247
|
+
inferredType = 'depends_on';
|
|
248
|
+
else if (label === 'classDiagram') {
|
|
249
|
+
baseMark = 'classDiagram';
|
|
250
|
+
baseLabel = '';
|
|
251
|
+
inferredType = 'has_metadata';
|
|
252
|
+
}
|
|
253
|
+
else if (label === '?>') {
|
|
254
|
+
baseMark = '?>';
|
|
255
|
+
baseLabel = '';
|
|
256
|
+
inferredType = 'condition';
|
|
257
|
+
}
|
|
258
|
+
else if (label.startsWith('::')) {
|
|
259
|
+
baseMark = label;
|
|
260
|
+
baseLabel = '';
|
|
261
|
+
inferredType = label.slice(2) || 'meta';
|
|
262
|
+
}
|
|
263
|
+
else
|
|
264
|
+
inferredType = classifyLabel(label)[0];
|
|
265
|
+
}
|
|
266
|
+
const finalType = explicitType || inferredType;
|
|
267
|
+
return { mark: baseMark, type: finalType, sync: finalType !== 'async_calls', label: baseLabel };
|
|
268
|
+
}
|
|
269
|
+
function normalizeAnchors(anchors) {
|
|
270
|
+
if (!anchors)
|
|
271
|
+
return [];
|
|
272
|
+
return anchors.map((a) => {
|
|
273
|
+
const out = { path: a.path || '', symbol: a.symbol || '' };
|
|
274
|
+
if (a.line != null)
|
|
275
|
+
out.line = a.line;
|
|
276
|
+
return out;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
export function buildGraphPayload(inputRoot, opts = {}) {
|
|
280
|
+
if (!existsSync(inputRoot))
|
|
281
|
+
throw new GraphYamlError(`输入目录不存在: ${inputRoot}`);
|
|
282
|
+
const recursive = opts.recursive !== false;
|
|
283
|
+
const freezeId = opts.freezeId ?? FREEZE_ID;
|
|
284
|
+
const graphIds = allGraphIds(inputRoot, { recursive });
|
|
285
|
+
const nodes = [];
|
|
286
|
+
const edges = [];
|
|
287
|
+
const graphs = [];
|
|
288
|
+
for (const graphId of graphIds) {
|
|
289
|
+
const yamlPath = yamlPathFor(inputRoot, graphId);
|
|
290
|
+
const data = loadYaml(yamlPath);
|
|
291
|
+
const validationErrors = validateGraphYaml(data, yamlPath);
|
|
292
|
+
if (validationErrors.length > 0)
|
|
293
|
+
throw new GraphYamlError(validationErrors.join('\n'));
|
|
294
|
+
// DEF-032①(D2):graph_id 真值源 = yaml 声明值(如 00_main);
|
|
295
|
+
// 路径命名空间 id(如 l0/00_main)仅作输入兼容定位,不写入输出。
|
|
296
|
+
const declaredId = data.graph_id != null ? String(data.graph_id) : graphId;
|
|
297
|
+
graphs.push({
|
|
298
|
+
id: declaredId,
|
|
299
|
+
title: data.title,
|
|
300
|
+
source_yaml_path: path.relative(inputRoot, yamlPath).replace(/\\/g, '/'),
|
|
301
|
+
});
|
|
302
|
+
for (const n of data.nodes || []) {
|
|
303
|
+
nodes.push({ id: n.id, label: n.label, graph_id: declaredId });
|
|
304
|
+
}
|
|
305
|
+
for (const e of data.edges || []) {
|
|
306
|
+
const { mark, type, sync, label } = edgeToGraphV2(e);
|
|
307
|
+
edges.push({
|
|
308
|
+
from: e.from,
|
|
309
|
+
to: e.to,
|
|
310
|
+
mark,
|
|
311
|
+
type,
|
|
312
|
+
sync,
|
|
313
|
+
label,
|
|
314
|
+
anchors: normalizeAnchors(e.anchors),
|
|
315
|
+
graph_id: declaredId,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
nodes.sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
320
|
+
edges.sort((a, b) => {
|
|
321
|
+
for (const k of ['graph_id', 'from', 'to', 'mark', 'type', 'sync', 'label']) {
|
|
322
|
+
const va = a[k] ?? '';
|
|
323
|
+
const vb = b[k] ?? '';
|
|
324
|
+
if (va !== vb)
|
|
325
|
+
return String(va).localeCompare(String(vb));
|
|
326
|
+
}
|
|
327
|
+
return 0;
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
schema_version: 'graph_v2',
|
|
331
|
+
freeze_id: freezeId,
|
|
332
|
+
generated_at: opts.generatedAt || stampYamlCorpus(inputRoot, graphIds),
|
|
333
|
+
nodes,
|
|
334
|
+
edges,
|
|
335
|
+
graphs,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
export function exportGraphJson(inputRoot, opts = {}) {
|
|
339
|
+
const payload = buildGraphPayload(inputRoot, opts);
|
|
340
|
+
const outPath = opts.outPath || path.join(inputRoot, 'shared', 'graph.json');
|
|
341
|
+
mkdirSync(path.dirname(outPath), { recursive: true });
|
|
342
|
+
writeFileSync(outPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
343
|
+
return { outPath, payload };
|
|
344
|
+
}
|
|
345
|
+
function formatAnchorComment(anchor) {
|
|
346
|
+
// Mermaid 行注释只认 %%(flowchart.md § Comments;官网 syntax-reference)。
|
|
347
|
+
// IDE Markdown 预览遇到 // 注释会解析失败,导致整图节点横排、边丢失(DEF-023)。
|
|
348
|
+
const p = anchor.path || '';
|
|
349
|
+
const symbol = anchor.symbol || '';
|
|
350
|
+
const line = anchor.line;
|
|
351
|
+
if (!p)
|
|
352
|
+
return '';
|
|
353
|
+
if (line != null)
|
|
354
|
+
return `%% → ${p}#L${line}`;
|
|
355
|
+
if (symbol)
|
|
356
|
+
return `%% → ${p}::${symbol}`;
|
|
357
|
+
return `%% → ${p}`;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Mermaid 文本转义(entity code,flowchart.md § Entity codes to escape characters)。
|
|
361
|
+
* 顺序敏感:先转 #,避免二次转义后续生成的 entity code。
|
|
362
|
+
*/
|
|
363
|
+
function escapeMermaidText(text) {
|
|
364
|
+
return String(text)
|
|
365
|
+
.replace(/#/g, '#35;')
|
|
366
|
+
.replace(/"/g, '#quot;')
|
|
367
|
+
.replace(/\|/g, '#124;');
|
|
368
|
+
}
|
|
369
|
+
function generateMermaid(data) {
|
|
370
|
+
const nodes = new Map((data.nodes || []).map((n) => [n.id || '', n]));
|
|
371
|
+
const direction = data.direction || 'TD';
|
|
372
|
+
const lines = [`flowchart ${direction}`];
|
|
373
|
+
for (const [nid, node] of nodes) {
|
|
374
|
+
const label = node.label || nid;
|
|
375
|
+
// 节点文本一律双引号包裹(flowchart.md § Special characters that break syntax),
|
|
376
|
+
// 否则含空格 / () / / / + 等字符的标签会让 IDE 预览解析失败(DEF-023)。
|
|
377
|
+
const text = `"${escapeMermaidText(label)}"`;
|
|
378
|
+
let shape;
|
|
379
|
+
if (label.includes('子流程') || label.endsWith('子流程'))
|
|
380
|
+
shape = `[[${text}]]`;
|
|
381
|
+
else if (nid === 'Q' || nid === 'E')
|
|
382
|
+
shape = `[[${text}]]`;
|
|
383
|
+
else
|
|
384
|
+
shape = `[${text}]`;
|
|
385
|
+
lines.push(` ${nid}${shape}`);
|
|
386
|
+
}
|
|
387
|
+
lines.push('');
|
|
388
|
+
for (const e of data.edges || []) {
|
|
389
|
+
const src = e.from;
|
|
390
|
+
const dst = e.to;
|
|
391
|
+
const mark = e.mark || '->';
|
|
392
|
+
const label = e.label || '';
|
|
393
|
+
// 带标签边官方形态:src -->|"text"| dst(flowchart.md § A link with arrow head and text)。
|
|
394
|
+
// yaml 中 label: "->" 表示裸执行边(99_mermaid_protocol.md §6),不渲染文本。
|
|
395
|
+
let text = label && label !== '->' ? label : '';
|
|
396
|
+
if (!text && mark && mark !== '->')
|
|
397
|
+
text = mark;
|
|
398
|
+
let edgeLine;
|
|
399
|
+
if (text)
|
|
400
|
+
edgeLine = ` ${src} -->|"${escapeMermaidText(text)}"| ${dst}`;
|
|
401
|
+
else
|
|
402
|
+
edgeLine = ` ${src} --> ${dst}`;
|
|
403
|
+
lines.push(edgeLine);
|
|
404
|
+
for (const a of e.anchors || []) {
|
|
405
|
+
const comment = formatAnchorComment(a);
|
|
406
|
+
if (comment)
|
|
407
|
+
lines.push(` ${comment}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
lines.push('');
|
|
411
|
+
lines.push(' classDef phase fill:#e1f5fe,stroke:#01579b,stroke-width:2px');
|
|
412
|
+
lines.push(' classDef doc fill:#fff8e1,stroke:#ff6f00,stroke-width:1px');
|
|
413
|
+
lines.push(' classDef infra fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px');
|
|
414
|
+
// DEF-033(R6):class 段以 nodes[].kind 为真值源(kind→class 映射,同 generateNodeTable 的 kind 读取);
|
|
415
|
+
// 无 kind(或未知 kind)时保留 id 推断作兜底(历史行为,仅供未标注 kind 的旧 yaml)。
|
|
416
|
+
const KIND_TO_CLASS = { flow: 'phase', struct: 'doc', external: 'infra' };
|
|
417
|
+
const classGroups = { phase: [], doc: [], infra: [] };
|
|
418
|
+
for (const [nid, node] of nodes) {
|
|
419
|
+
let cls = node.kind ? KIND_TO_CLASS[node.kind] : undefined;
|
|
420
|
+
if (!cls) {
|
|
421
|
+
// 兜底:id 推断(无 kind 或未知 kind 时)
|
|
422
|
+
if (['Q', 'E', 'U1', 'U2', 'RAG', 'T2S', 'RPC', 'FTS'].includes(nid))
|
|
423
|
+
cls = 'phase';
|
|
424
|
+
else if (nid.includes('DOC'))
|
|
425
|
+
cls = 'doc';
|
|
426
|
+
else if (['AUTH', 'EV_TYPES'].includes(nid))
|
|
427
|
+
cls = 'infra';
|
|
428
|
+
}
|
|
429
|
+
if (cls)
|
|
430
|
+
classGroups[cls].push(nid);
|
|
431
|
+
}
|
|
432
|
+
if (classGroups.phase.length)
|
|
433
|
+
lines.push(` class ${classGroups.phase.join(',')} phase`);
|
|
434
|
+
if (classGroups.doc.length)
|
|
435
|
+
lines.push(` class ${classGroups.doc.join(',')} doc`);
|
|
436
|
+
if (classGroups.infra.length)
|
|
437
|
+
lines.push(` class ${classGroups.infra.join(',')} infra`);
|
|
438
|
+
return lines.join('\n');
|
|
439
|
+
}
|
|
440
|
+
function generateNodeTable(data) {
|
|
441
|
+
const lines = ['### Nodes', '', '| ID | Label | Kind |', '|----|-------|------|'];
|
|
442
|
+
for (const n of data.nodes || []) {
|
|
443
|
+
const label = String(n.label || '').replace(/\|/g, '\\|');
|
|
444
|
+
lines.push(`| ${n.id} | ${label} | ${n.kind || ''} |`);
|
|
445
|
+
}
|
|
446
|
+
return lines.join('\n');
|
|
447
|
+
}
|
|
448
|
+
function generateEdgeTable(data) {
|
|
449
|
+
const lines = [
|
|
450
|
+
'### Edges',
|
|
451
|
+
'',
|
|
452
|
+
'| From | To | Mark | Type | Label | Anchors |',
|
|
453
|
+
'|------|----|------|------|-------|---------|',
|
|
454
|
+
];
|
|
455
|
+
for (const e of data.edges || []) {
|
|
456
|
+
const { mark, type } = edgeToGraphV2(e);
|
|
457
|
+
const label = String(e.label || '').replace(/\|/g, '\\|');
|
|
458
|
+
const anchors = e.anchors || [];
|
|
459
|
+
const anchorSummary = anchors.length ? `${anchors.length} anchor(s)` : '';
|
|
460
|
+
lines.push(`| ${e.from || ''} | ${e.to || ''} | ${mark} | ${type} | ${label} | ${anchorSummary} |`);
|
|
461
|
+
}
|
|
462
|
+
return lines.join('\n');
|
|
463
|
+
}
|
|
464
|
+
function generateNotesSection(data) {
|
|
465
|
+
const notes = data.notes;
|
|
466
|
+
if (!notes)
|
|
467
|
+
return '';
|
|
468
|
+
let body;
|
|
469
|
+
if (typeof notes === 'string')
|
|
470
|
+
body = notes;
|
|
471
|
+
else if (Array.isArray(notes))
|
|
472
|
+
body = notes.join('\n\n');
|
|
473
|
+
else
|
|
474
|
+
body = String(notes);
|
|
475
|
+
return `\n\n## Notes\n\n${body}\n`;
|
|
476
|
+
}
|
|
477
|
+
function generateSubGraphLinks(graphId) {
|
|
478
|
+
if (graphId !== '00_main')
|
|
479
|
+
return '';
|
|
480
|
+
return [
|
|
481
|
+
'## Sub-graph Links',
|
|
482
|
+
'',
|
|
483
|
+
'- `Struct`: [`01_struct.md`](01_struct.md)(手写 · 无 `.graph.yaml`)',
|
|
484
|
+
'- `Version`: [`02_version.md`](02_version.md)(手写 · 无 `.graph.yaml`)',
|
|
485
|
+
'- 子图编辑源见 `docs/_tech_graph/*.graph.yaml`',
|
|
486
|
+
'',
|
|
487
|
+
].join('\n');
|
|
488
|
+
}
|
|
489
|
+
export function generateMarkdown(data, opts = {}) {
|
|
490
|
+
const graphId = data.graph_id || 'main';
|
|
491
|
+
const title = data.title || graphId;
|
|
492
|
+
const description = data.description || '';
|
|
493
|
+
const version = data.version || '';
|
|
494
|
+
const generatedAt = opts.generatedAt || contentStamp(opts.sourceRaw ?? JSON.stringify(data));
|
|
495
|
+
const src = opts.sourcePath || `docs/_tech_graph/${graphId}.graph.yaml`;
|
|
496
|
+
const frontmatter = `---
|
|
497
|
+
graph_id: ${graphId}
|
|
498
|
+
version: ${version}
|
|
499
|
+
generated_at: ${generatedAt}
|
|
500
|
+
source: ${src}
|
|
501
|
+
---
|
|
502
|
+
`;
|
|
503
|
+
const header = `# ${title}\n\n${description}`.trim();
|
|
504
|
+
const mermaid = generateMermaid(data);
|
|
505
|
+
const subLinks = generateSubGraphLinks(graphId);
|
|
506
|
+
const notes = generateNotesSection(data);
|
|
507
|
+
const body = `${header}
|
|
508
|
+
|
|
509
|
+
## Mermaid
|
|
510
|
+
|
|
511
|
+
\`\`\`mermaid
|
|
512
|
+
${mermaid}
|
|
513
|
+
\`\`\`
|
|
514
|
+
|
|
515
|
+
## Structured Data
|
|
516
|
+
|
|
517
|
+
${generateNodeTable(data)}
|
|
518
|
+
|
|
519
|
+
${generateEdgeTable(data)}${notes}${subLinks ? `\n\n${subLinks}` : ''}
|
|
520
|
+
`;
|
|
521
|
+
return `${frontmatter}\n${body}`;
|
|
522
|
+
}
|
|
523
|
+
export function compileGraph(graphId, inputRoot, outputPath = null) {
|
|
524
|
+
const yamlPath = yamlPathFor(inputRoot, graphId);
|
|
525
|
+
if (!existsSync(yamlPath))
|
|
526
|
+
throw new GraphYamlError(`YAML 源不存在: ${yamlPath}`);
|
|
527
|
+
const raw = readFileSync(yamlPath, 'utf8');
|
|
528
|
+
const data = loadYaml(yamlPath);
|
|
529
|
+
const validationErrors = validateGraphYaml(data, yamlPath);
|
|
530
|
+
if (validationErrors.length > 0)
|
|
531
|
+
throw new GraphYamlError(validationErrors.join('\n'));
|
|
532
|
+
const md = generateMarkdown(data, {
|
|
533
|
+
sourcePath: path.relative(inputRoot, yamlPath).replace(/\\/g, '/'),
|
|
534
|
+
sourceRaw: raw,
|
|
535
|
+
});
|
|
536
|
+
const outPath = outputPath || mdPathFor(inputRoot, graphId);
|
|
537
|
+
writeFileSync(outPath, md, 'utf8');
|
|
538
|
+
return outPath;
|
|
539
|
+
}
|
|
540
|
+
export function loadGraphJson(graphJsonPath) {
|
|
541
|
+
if (!existsSync(graphJsonPath))
|
|
542
|
+
return null;
|
|
543
|
+
const raw = readFileSync(graphJsonPath, 'utf8');
|
|
544
|
+
try {
|
|
545
|
+
return JSON.parse(raw);
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
const e = err;
|
|
549
|
+
throw new GraphYamlError(`graph.json 解析失败: ${e.message ?? String(err)}`, { path: graphJsonPath });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
export function checkGraph(graphId, inputRoot, graphJsonPath) {
|
|
553
|
+
const yamlPath = yamlPathFor(inputRoot, graphId);
|
|
554
|
+
if (!existsSync(yamlPath))
|
|
555
|
+
throw new GraphYamlError(`YAML 源不存在: ${yamlPath}`);
|
|
556
|
+
const yamlData = loadYaml(yamlPath);
|
|
557
|
+
const validationErrors = validateGraphYaml(yamlData, yamlPath);
|
|
558
|
+
if (validationErrors.length > 0)
|
|
559
|
+
throw new GraphYamlError(validationErrors.join('\n'));
|
|
560
|
+
if (!existsSync(graphJsonPath))
|
|
561
|
+
return { ok: false, diff: `graph.json 不存在: ${graphJsonPath}` };
|
|
562
|
+
const graphJson = loadGraphJson(graphJsonPath);
|
|
563
|
+
// DEF-032②(D3):与 export 同一 id 真值源(yaml 声明值),路径 id 仅作输入兼容定位。
|
|
564
|
+
const declaredId = yamlData.graph_id != null ? String(yamlData.graph_id) : graphId;
|
|
565
|
+
const jsonNodes = (graphJson?.nodes || []).filter((n) => n?.graph_id === declaredId);
|
|
566
|
+
const jsonEdges = (graphJson?.edges || []).filter((e) => e?.graph_id === declaredId && 'from' in e && 'to' in e);
|
|
567
|
+
const yamlNodes = new Map((yamlData.nodes || []).map((n) => [n.id || '', n]));
|
|
568
|
+
const yamlNodeIds = new Set(yamlNodes.keys());
|
|
569
|
+
const jsonNodeIds = new Set(jsonNodes.map((n) => n.id || ''));
|
|
570
|
+
const diffs = [];
|
|
571
|
+
if (yamlNodeIds.size !== jsonNodeIds.size || ![...yamlNodeIds].every((id) => jsonNodeIds.has(id))) {
|
|
572
|
+
const onlyYaml = [...yamlNodeIds].filter((id) => !jsonNodeIds.has(id));
|
|
573
|
+
const onlyJson = [...jsonNodeIds].filter((id) => !yamlNodeIds.has(id));
|
|
574
|
+
if (onlyYaml.length)
|
|
575
|
+
diffs.push(`Nodes only in YAML: ${onlyYaml.sort().join(', ')}`);
|
|
576
|
+
if (onlyJson.length)
|
|
577
|
+
diffs.push(`Nodes only in JSON: ${onlyJson.sort().join(', ')}`);
|
|
578
|
+
}
|
|
579
|
+
if ((yamlData.nodes || []).length !== jsonNodes.length) {
|
|
580
|
+
diffs.push(`Node count mismatch: YAML=${(yamlData.nodes || []).length}, JSON=${jsonNodes.length}`);
|
|
581
|
+
}
|
|
582
|
+
const yamlEdgeSet = new Set((yamlData.edges || []).map((e) => {
|
|
583
|
+
const { mark, type } = edgeToGraphV2(e);
|
|
584
|
+
return JSON.stringify([e.from, e.to, mark, type]);
|
|
585
|
+
}));
|
|
586
|
+
const jsonEdgeSet = new Set(jsonEdges.map((e) => JSON.stringify([e.from, e.to, e.mark || '->', e.type || 'depends_on'])));
|
|
587
|
+
if (yamlEdgeSet.size !== jsonEdgeSet.size || ![...yamlEdgeSet].every((k) => jsonEdgeSet.has(k))) {
|
|
588
|
+
const onlyYaml = [...yamlEdgeSet].filter((k) => !jsonEdgeSet.has(k)).map((k) => JSON.parse(k));
|
|
589
|
+
const onlyJson = [...jsonEdgeSet].filter((k) => !yamlEdgeSet.has(k)).map((k) => JSON.parse(k));
|
|
590
|
+
if (onlyYaml.length)
|
|
591
|
+
diffs.push(`Edges only in YAML: ${JSON.stringify(onlyYaml)}`);
|
|
592
|
+
if (onlyJson.length)
|
|
593
|
+
diffs.push(`Edges only in JSON: ${JSON.stringify(onlyJson)}`);
|
|
594
|
+
}
|
|
595
|
+
if ((yamlData.edges || []).length !== jsonEdges.length) {
|
|
596
|
+
diffs.push(`Edge count mismatch: YAML=${(yamlData.edges || []).length}, JSON=${jsonEdges.length}`);
|
|
597
|
+
}
|
|
598
|
+
if (diffs.length === 0)
|
|
599
|
+
return { ok: true, diff: '' };
|
|
600
|
+
return { ok: false, diff: diffs.join('\n') };
|
|
601
|
+
}
|
|
602
|
+
//# sourceMappingURL=cli-graph-yaml.js.map
|