archgraph-argo 0.11.0 → 0.12.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.
@@ -1,242 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * 布局侧车(Layout Sidecar,WP2785 M1-S2,2026-09-03 修订:默认按项目隔离,AT-2785-L5)
5
- * — 图谱画布坐标独立持久化。
6
- *
7
- * 零第三方依赖(仅 Node 内置 path/fs/crypto)。与 design/KG/SystemArchitecture.json
8
- * 完全物理隔离:坐标不进图谱 JSON、不进 schema;本模块永远不读写图谱文件本身。
9
- *
10
- * 存储根(按优先级):
11
- * 1. 显式覆盖:createLayoutStore({ layoutRoot }) 选项或 EA_LAYOUT_ROOT 环境变量
12
- * → <layoutRoot>/<projectId>/<view_id>.json(测试 / 自定义集中存储根)。
13
- * 2. 默认(无任何覆盖):按项目隔离
14
- * → <projectRoot>/design/KG/ea-layouts/<view_id>.json(仍是独立文件,
15
- * 只落在 ea-layouts/ 子目录,绝不触碰 SystemArchitecture.json 本身)。
16
- *
17
- * 文件内容:{ version, graphKey, view_id, signature, updatedAt, elements: { <elementId>: {x,y} } }
18
- *
19
- * 失效键 = 视图成员身份签名(成员身份集合的 sha256,只含身份 id,不含任何内容字段):
20
- * signature = sha256(sorted(included_elements) + sorted(included_relationships))。
21
- * 图谱内容级修改(元素 name/description/attributes、关系描述等)不改变成员集合
22
- * → signature 不变 → 坐标完全不动;仅成员结构变化触发新成员补位/移除成员清理。
23
- *
24
- * 合并语义(读取时):以当前图谱视图成员为准——仍有坐标的现存成员原样保留;
25
- * 新成员(无坐标)按确定性算法补位(沿用 buildViewGraph 的圆形布局公式)并写回侧车;
26
- * 已不在成员中的坐标清理。
27
- */
28
-
29
- const path = require('node:path');
30
- const fs = require('node:fs');
31
- const crypto = require('node:crypto');
32
-
33
- const LAYOUT_VERSION = 1;
34
- // 默认按项目隔离时,侧车相对项目根的存放目录。
35
- const PROJECT_LAYOUT_DIR = path.join('design', 'KG', 'ea-layouts');
36
-
37
- /**
38
- * 解析显式存储根覆盖:选项 > EA_LAYOUT_ROOT 环境变量。
39
- * 返回 null 表示无覆盖 → 使用默认的按项目隔离存储。
40
- */
41
- function resolveLayoutRoot(explicitRoot) {
42
- if (explicitRoot) {
43
- return path.resolve(explicitRoot);
44
- }
45
- if (process.env.EA_LAYOUT_ROOT) {
46
- return path.resolve(process.env.EA_LAYOUT_ROOT);
47
- }
48
- return null;
49
- }
50
-
51
- /**
52
- * 视图成员身份签名:仅由 included_elements / included_relationships 的身份 id 集合决定,
53
- * 与元素/关系的任何内容字段无关。
54
- */
55
- function computeViewSignature(view) {
56
- const elements = Array.from(new Set((view && view.included_elements) || [])).sort();
57
- const relationships = Array.from(new Set((view && view.included_relationships) || [])).sort();
58
- const payload = `${elements.join('\u0001')}\u0000${relationships.join('\u0001')}`;
59
- return crypto.createHash('sha256').update(payload).digest('hex');
60
- }
61
-
62
- // 文件名安全化(服务端路由已限制 [A-Za-z0-9._-],此处为独立模块的防御性兜底)。
63
- function safeFileName(segment) {
64
- return String(segment).replace(/[^A-Za-z0-9._-]/g, '_');
65
- }
66
-
67
- /**
68
- * 确定性补位:沿用 buildViewGraph 的圆形布局公式(成员索引 → 圆周位置)。
69
- */
70
- function defaultPosition(index, count) {
71
- const angle = (2 * Math.PI * index) / Math.max(count, 1);
72
- return {
73
- x: Math.round(80 + 160 * Math.cos(angle)),
74
- y: Math.round(80 + 160 * Math.sin(angle)),
75
- };
76
- }
77
-
78
- function isValidPosition(pos) {
79
- return !!pos
80
- && typeof pos === 'object'
81
- && !Array.isArray(pos)
82
- && Number.isFinite(Number(pos.x))
83
- && Number.isFinite(Number(pos.y));
84
- }
85
-
86
- function createLayoutStore(options = {}) {
87
- // null → 默认按项目隔离存储;否则为显式集中存储根。
88
- const explicitRoot = resolveLayoutRoot(options.layoutRoot);
89
-
90
- /**
91
- * 目标解析:target = { project?: {id, root, graphPath}, projectId?, graphKey? }。
92
- * 显式根模式用 projectId 分桶;默认模式用 project.root 定位项目内 ea-layouts/ 目录。
93
- */
94
- function resolveTarget(target, viewId) {
95
- const t = target || {};
96
- const project = t.project || {};
97
- const projectId = t.projectId || project.id || null;
98
- const graphKey = t.graphKey || project.graphPath || null;
99
- if (explicitRoot) {
100
- if (!projectId) {
101
- throw new Error('layout store: 显式存储根模式下必须提供 projectId(或 project.id)');
102
- }
103
- return {
104
- projectId,
105
- graphKey,
106
- filePath: path.join(explicitRoot, safeFileName(projectId), `${safeFileName(viewId)}.json`),
107
- };
108
- }
109
- const projectRoot = project.root;
110
- if (!projectRoot) {
111
- throw new Error('layout store: 默认按项目隔离存储必须提供 project.root');
112
- }
113
- return {
114
- projectId,
115
- graphKey,
116
- filePath: path.join(path.resolve(projectRoot), PROJECT_LAYOUT_DIR, `${safeFileName(viewId)}.json`),
117
- };
118
- }
119
-
120
- function filePathFor(target, viewId) {
121
- return resolveTarget(target, viewId).filePath;
122
- }
123
-
124
- function readRecord(target, viewId) {
125
- let text;
126
- try {
127
- text = fs.readFileSync(filePathFor(target, viewId), 'utf8');
128
- } catch {
129
- return null;
130
- }
131
- try {
132
- const record = JSON.parse(text);
133
- if (!record || typeof record !== 'object' || Array.isArray(record)) {
134
- return null;
135
- }
136
- if (!record.elements || typeof record.elements !== 'object' || Array.isArray(record.elements)) {
137
- record.elements = {};
138
- }
139
- return record;
140
- } catch {
141
- return null;
142
- }
143
- }
144
-
145
- function writeRecord(target, viewId, record) {
146
- const filePath = filePathFor(target, viewId);
147
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
148
- const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
149
- fs.writeFileSync(tempPath, JSON.stringify(record, null, 2), 'utf8');
150
- fs.renameSync(tempPath, filePath);
151
- return filePath;
152
- }
153
-
154
- /**
155
- * 读取合并后的布局(以当前图谱视图成员为准)。
156
- * 有补位/清理/签名变化时写回侧车;纯内容级修改(签名不变)不触发写入。
157
- * 入参:{ project?: {id, root, graphPath}, projectId?, graphKey?, view }。
158
- * 返回 { signature, elements: { <elementId>: {x,y} } }。
159
- */
160
- function mergeLayout({ project, projectId, graphKey, view }) {
161
- if (!view || typeof view.view_id !== 'string') {
162
- throw new Error('mergeLayout: view with view_id is required');
163
- }
164
- const target = { project, projectId, graphKey };
165
- const viewId = view.view_id;
166
- const signature = computeViewSignature(view);
167
- const record = readRecord(target, viewId);
168
- const saved = record ? record.elements : {};
169
- const members = Array.isArray(view.included_elements) ? view.included_elements : [];
170
- const elements = {};
171
- let dirty = !record || record.signature !== signature;
172
-
173
- members.forEach((id, index) => {
174
- const pos = saved[id];
175
- if (isValidPosition(pos)) {
176
- elements[id] = { x: Number(pos.x), y: Number(pos.y) };
177
- } else {
178
- elements[id] = defaultPosition(index, members.length);
179
- dirty = true;
180
- }
181
- });
182
- for (const id of Object.keys(saved)) {
183
- if (!members.includes(id)) {
184
- dirty = true; // 已不在成员中的坐标:清理
185
- }
186
- }
187
- if (dirty) {
188
- writeRecord(target, viewId, {
189
- version: LAYOUT_VERSION,
190
- graphKey: resolveTarget(target, viewId).graphKey,
191
- view_id: viewId,
192
- signature,
193
- updatedAt: new Date().toISOString(),
194
- elements,
195
- });
196
- }
197
- return { signature, elements };
198
- }
199
-
200
- /**
201
- * 全量写入布局(按当前文档计算签名后原子写入侧车文件)。
202
- * 入参:{ project?: {id, root, graphPath}, projectId?, graphKey?, view, elements };
203
- * elements: { <elementId>: {x,y} };非法坐标抛错(由服务端映射为 400)。
204
- */
205
- function putLayout({ project, projectId, graphKey, view, elements }) {
206
- if (!view || typeof view.view_id !== 'string') {
207
- throw new Error('putLayout: view with view_id is required');
208
- }
209
- if (!elements || typeof elements !== 'object' || Array.isArray(elements)) {
210
- throw new Error('body.elements 必须是 { <elementId>: {x,y} } 对象');
211
- }
212
- const normalized = {};
213
- for (const [id, pos] of Object.entries(elements)) {
214
- if (!isValidPosition(pos)) {
215
- throw new Error(`elements['${id}'] 的坐标必须是有限的 {x,y} 数字`);
216
- }
217
- normalized[id] = { x: Number(pos.x), y: Number(pos.y) };
218
- }
219
- const target = { project, projectId, graphKey };
220
- const signature = computeViewSignature(view);
221
- writeRecord(target, view.view_id, {
222
- version: LAYOUT_VERSION,
223
- graphKey: resolveTarget(target, view.view_id).graphKey,
224
- view_id: view.view_id,
225
- signature,
226
- updatedAt: new Date().toISOString(),
227
- elements: normalized,
228
- });
229
- return { signature, view_id: view.view_id };
230
- }
231
-
232
- return { root: explicitRoot, filePathFor, readRecord, writeRecord, mergeLayout, putLayout };
233
- }
234
-
235
- module.exports = {
236
- LAYOUT_VERSION,
237
- PROJECT_LAYOUT_DIR,
238
- resolveLayoutRoot,
239
- computeViewSignature,
240
- defaultPosition,
241
- createLayoutStore,
242
- };