archgraph-argo 0.15.2 → 0.15.3
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.
|
@@ -410,7 +410,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
410
410
|
stages.elements = nowMs();
|
|
411
411
|
|
|
412
412
|
// element anchors + meta (idempotent, fingerprint-skipped)
|
|
413
|
-
let tagStats = { propsNew: 0, propsSkip: 0 };
|
|
413
|
+
let tagStats = { propsNew: 0, propsSkip: 0, attributesUpdated: 0, attributesAdded: 0, testsUpdated: 0, testsAdded: 0, relationshipAttributesUpdated: 0, relationshipAttributesAdded: 0 };
|
|
414
414
|
if (!o.dryRun) {
|
|
415
415
|
// minimal EA-facing anchors for compatibility with the legacy object-model exporter
|
|
416
416
|
const anchorRows = [];
|
|
@@ -532,6 +532,25 @@ function syncGraphToQea(graph, qeaPath, opts) {
|
|
|
532
532
|
db.prepare('INSERT INTO t_connectortag (ElementID, Property, VALUE, NOTES) VALUES (?,?,?,?)')
|
|
533
533
|
.run(Number(id), 'archimate_relationship_type', canonicalArchimateType(rel.type), '');
|
|
534
534
|
}
|
|
535
|
+
// Mirror relationship attributes into the connector's relationship_attributes_json
|
|
536
|
+
// tag so the human-edited EA model surfaces them (the EA-internal draft extractor
|
|
537
|
+
// diffs this visible field). Idempotent: update-in-place on the canonical value.
|
|
538
|
+
const relAttrs = (Array.isArray(rel.attributes) ? rel.attributes : []).filter(isPersistedAttribute);
|
|
539
|
+
const attrsJson = relAttrs.length > 0 ? JSON.stringify(relAttrs) : '';
|
|
540
|
+
const tagCt = db.prepare('SELECT PropertyID FROM t_connectortag WHERE ElementID=? AND Property=?').get(Number(id), 'relationship_attributes_json');
|
|
541
|
+
if (tagCt) {
|
|
542
|
+
const cur = db.prepare('SELECT VALUE AS v FROM t_connectortag WHERE PropertyID=?').get(Number(tagCt.PropertyID));
|
|
543
|
+
const curV = (cur && cur.v) || '';
|
|
544
|
+
if (curV !== attrsJson) {
|
|
545
|
+
db.prepare('UPDATE t_connectortag SET VALUE=?, NOTES=? WHERE PropertyID=?')
|
|
546
|
+
.run(attrsJson, '', Number(tagCt.PropertyID));
|
|
547
|
+
tagStats.relationshipAttributesUpdated++;
|
|
548
|
+
}
|
|
549
|
+
} else if (relAttrs.length > 0) {
|
|
550
|
+
db.prepare('INSERT INTO t_connectortag (ElementID, Property, VALUE, NOTES) VALUES (?,?,?,?)')
|
|
551
|
+
.run(Number(id), 'relationship_attributes_json', attrsJson, '');
|
|
552
|
+
tagStats.relationshipAttributesAdded++;
|
|
553
|
+
}
|
|
535
554
|
}
|
|
536
555
|
}
|
|
537
556
|
stages.relTags = nowMs();
|
package/install-argo.ps1
CHANGED
|
@@ -762,10 +762,6 @@ $skillDest = Join-Path $SkillsRoot 'argo-init'
|
|
|
762
762
|
Write-Host "[4/22] argo\skills\argo-init -> $skillDest"
|
|
763
763
|
Copy-Tree -Source $skillSrc -Destination $skillDest
|
|
764
764
|
|
|
765
|
-
$draftSkillSrc = Join-Path $argoDir 'skills\ea-human-draft'
|
|
766
|
-
Write-Host ' argo\skills\ea-human-draft -> $SkillsRoot\ea-human-draft (ea-human-draft skill)'
|
|
767
|
-
Copy-Tree -Source $draftSkillSrc -Destination (Join-Path $SkillsRoot 'ea-human-draft')
|
|
768
|
-
|
|
769
765
|
$ruleSrc = Join-Path (Join-Path $argoDir 'rules') 'archgraph.instructions.md'
|
|
770
766
|
$ruleDest = Join-Path $PromptsRoot 'archgraph.instructions.md'
|
|
771
767
|
Write-Host "[5/22] argo\rules\archgraph.instructions.md -> $ruleDest"
|
|
@@ -780,8 +776,6 @@ Copy-Item -Force -Path $depsSrc -Destination $depsDest
|
|
|
780
776
|
$cursorSkillDest = Join-Path $CursorSkillsRoot 'argo-init'
|
|
781
777
|
Write-Host "[7/22] argo\skills\argo-init -> $cursorSkillDest (Cursor)"
|
|
782
778
|
Copy-Tree -Source $skillSrc -Destination $cursorSkillDest
|
|
783
|
-
Write-Host ' argo\skills\ea-human-draft -> $CursorSkillsRoot\ea-human-draft (Cursor)'
|
|
784
|
-
Copy-Tree -Source $draftSkillSrc -Destination (Join-Path $CursorSkillsRoot 'ea-human-draft')
|
|
785
779
|
|
|
786
780
|
$mcpBridgeSrc = Join-Path $argoDir 'mcp-bridges'
|
|
787
781
|
$mcpBridgeDest = Join-Path $CursorMcpBridgesRoot ''
|
|
@@ -791,8 +785,6 @@ Copy-Tree -Source $mcpBridgeSrc -Destination $mcpBridgeDest
|
|
|
791
785
|
$openCodeSkillDest = Join-Path $OpenCodeSkillsRoot 'argo-init'
|
|
792
786
|
Write-Host "[8/22] argo\skills\argo-init -> $openCodeSkillDest (OpenCode)"
|
|
793
787
|
Copy-Tree -Source $skillSrc -Destination $openCodeSkillDest
|
|
794
|
-
Write-Host ' argo\skills\ea-human-draft -> $OpenCodeSkillsRoot\ea-human-draft (OpenCode)'
|
|
795
|
-
Copy-Tree -Source $draftSkillSrc -Destination (Join-Path $OpenCodeSkillsRoot 'ea-human-draft')
|
|
796
788
|
|
|
797
789
|
Write-Host "[9/22] argo\rules\archgraph.instructions.md -> $OpenCodeAgentsPath (OpenCode global AGENTS.md)"
|
|
798
790
|
Add-AgentsRule -AgentsPath $OpenCodeAgentsPath -RulePath $ruleSrc
|
|
@@ -831,9 +823,6 @@ if ($SkipDsh) {
|
|
|
831
823
|
Write-Host "[16/22] argo\skills\argo-init -> $dshSkillDest (DeepSeek Harness skill)"
|
|
832
824
|
Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $dshSkillDest
|
|
833
825
|
|
|
834
|
-
Write-Host ' argo\skills\ea-human-draft -> $DshHome\skills\ea-human-draft (DeepSeek Harness skill)'
|
|
835
|
-
Copy-Tree -Source (Join-Path $argoDir 'skills\ea-human-draft') -Destination (Join-Path (Join-Path $DshHome 'skills') 'ea-human-draft')
|
|
836
|
-
|
|
837
826
|
Write-Host "[17/22] argo\rules\<WakeupGuideline> -> $DshHome\plugins\dsh-argo-wakeup\index.js (DeepSeek Harness wakeup plugin)"
|
|
838
827
|
$wakeupDshPath = New-DshWakeupPlugin -DshHome $DshHome -RuleText $ruleSrcContent
|
|
839
828
|
|
|
@@ -895,9 +884,6 @@ if ($SkipOpenClaw) {
|
|
|
895
884
|
Write-Host "[21/22] argo\skills\argo-init -> $openClawSkillDest (OpenClaw managed skill, all agents)"
|
|
896
885
|
Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $openClawSkillDest
|
|
897
886
|
|
|
898
|
-
Write-Host ' argo\skills\ea-human-draft -> $OpenClawHome\skills\ea-human-draft (OpenClaw managed skill, all agents)'
|
|
899
|
-
Copy-Tree -Source (Join-Path $argoDir 'skills\ea-human-draft') -Destination (Join-Path (Join-Path $OpenClawHome 'skills') 'ea-human-draft')
|
|
900
|
-
|
|
901
887
|
Write-Host ' OpenClaw injects AGENTS.md into Project Context on every session, so the wakeup'
|
|
902
888
|
Write-Host ' gate (UNCONDITIONAL STARTUP GATE) is active on the next OpenClaw session; restart'
|
|
903
889
|
Write-Host ' the OpenClaw gateway (openclaw gateway restart) if it is already running.'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archgraph-argo",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
4
|
"description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"argo/plugins",
|
|
15
15
|
"argo/mcp-bridges",
|
|
16
16
|
"argo/skills/argo-init",
|
|
17
|
-
"argo/skills/ea-human-draft",
|
|
18
17
|
"argo/rules",
|
|
19
18
|
"argo/package.json",
|
|
20
19
|
"vendor",
|
|
@@ -1,628 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// WP2792 (AT-2792-01..06): EA human-draft -> semantic diff -> agent write-back.
|
|
4
|
-
// Compares the EA *visible object model* of two .qea snapshots (baseline committed
|
|
5
|
-
// .qea vs human-edited working .qea) and classifies the human's changes into
|
|
6
|
-
// canonical-graph proposals (addElement/updateElement/removeElement,
|
|
7
|
-
// addRelationship/updateRelationship/removeRelationship, updateView membership).
|
|
8
|
-
//
|
|
9
|
-
// Why the visible object model and NOT kg_sync_meta:
|
|
10
|
-
// kg_sync_meta is a lossless canonical mirror written only by the projector; a human
|
|
11
|
-
// editing in EA touches t_object/t_connector/t_diagram/t_diagramobjects (plus the
|
|
12
|
-
// schema_id / archimate_type anchor tags). Comparing mirrors would show zero diff for
|
|
13
|
-
// human edits. So we anchor via the schema_id tag (elements: t_objectproperties,
|
|
14
|
-
// relationships: t_connectortag; views: t_diagram schema_view_id StyleEx token OR the
|
|
15
|
-
// deterministic diag:<viewId> ea_guid written by the projection).
|
|
16
|
-
//
|
|
17
|
-
// Semantic-first (v1): pure geometry (t_diagramobjects coordinates) never becomes a
|
|
18
|
-
// canonical proposal — it is only counted (layoutOnly) and excluded.
|
|
19
|
-
//
|
|
20
|
-
// Zero third-party deps, no EA required: node:sqlite via the shared lib helper.
|
|
21
|
-
// node argo/scripts/ea-human-diff.js [--work <work.qea>] [--base <base.qea>] [--graph <json>]
|
|
22
|
-
// [--out <stem>] [--no-md] [--baseline-commit <sha>]
|
|
23
|
-
// --work defaults to the CURRENT PROJECT's root single *.qea (ARGO_EA_QEA > 仓库根唯一 *.qea),
|
|
24
|
-
// never a hardcoded filename — the flow works in any ArchGraph workspace.
|
|
25
|
-
// --base is optional: when omitted and --work is a tracked file inside the git repo,
|
|
26
|
-
// the committed (HEAD) version of --work is extracted automatically as the baseline —
|
|
27
|
-
// the day-to-day "human edited <project>.qea" flow is then a single command.
|
|
28
|
-
|
|
29
|
-
const path = require('node:path');
|
|
30
|
-
const fs = require('node:fs');
|
|
31
|
-
const lib = require('./ea-qea-sync-lib.js');
|
|
32
|
-
const { deterministicGuid } = lib;
|
|
33
|
-
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
// readSnapshot — read one .qea's semantic EA-visible projection surface
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
function parseStyleToken(styleEx, key) {
|
|
38
|
-
const text = String(styleEx === null || styleEx === undefined ? '' : styleEx);
|
|
39
|
-
const re = new RegExp('(^|;|\\s)' + key + '=([^;]*)', 'i');
|
|
40
|
-
const m = re.exec(text);
|
|
41
|
-
return m ? m[2] : '';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function readSnapshot(qeaPath, opts) {
|
|
45
|
-
const o = opts || {};
|
|
46
|
-
const db = lib.openQea(qeaPath);
|
|
47
|
-
try {
|
|
48
|
-
const snapshot = {
|
|
49
|
-
path: qeaPath,
|
|
50
|
-
syncPackageId: 0,
|
|
51
|
-
// viewId -> { diagramId, name }
|
|
52
|
-
diagramsByView: new Map(),
|
|
53
|
-
// diagramId -> viewId
|
|
54
|
-
viewByDiagram: new Map(),
|
|
55
|
-
// canonical id -> element (anchored via schema_id tag)
|
|
56
|
-
elementBySchema: new Map(),
|
|
57
|
-
// eaGuid -> element (all t_object)
|
|
58
|
-
elementByGuid: new Map(),
|
|
59
|
-
// canonical id -> relationship (anchored via t_connectortag schema_id)
|
|
60
|
-
relBySchema: new Map(),
|
|
61
|
-
// eaGuid -> relationship (all t_connector)
|
|
62
|
-
relByGuid: new Map(),
|
|
63
|
-
// diagramId -> Map(objectId -> coords)
|
|
64
|
-
placements: new Map(),
|
|
65
|
-
// informational: kg_sync_meta counts/shas (never used for proposal detection)
|
|
66
|
-
meta: { elements: 0, relationships: 0, views: 0, sha: '' },
|
|
67
|
-
knownViewIds: null, // set by resolveViews when a canonical view catalog is provided
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// kg_sync_meta informational snapshot (human edits never touch it -> identical).
|
|
71
|
-
try {
|
|
72
|
-
const rows = db.prepare('SELECT kind, key, sha FROM kg_sync_meta ORDER BY kind, key').all();
|
|
73
|
-
snapshot.meta.elements = rows.filter((r) => r.kind === 'element').length;
|
|
74
|
-
snapshot.meta.relationships = rows.filter((r) => r.kind === 'relationship').length;
|
|
75
|
-
snapshot.meta.views = rows.filter((r) => r.kind === 'view').length;
|
|
76
|
-
const hasher = require('node:crypto').createHash('sha256');
|
|
77
|
-
for (const r of rows) { hasher.update(r.kind + '|' + r.key + '|' + r.sha); }
|
|
78
|
-
snapshot.meta.sha = hasher.digest('hex');
|
|
79
|
-
} catch { /* table may be absent on a hand-drawn model */ }
|
|
80
|
-
|
|
81
|
-
// sync package id
|
|
82
|
-
const roots = db.prepare('SELECT Package_ID FROM t_package WHERE Parent_ID=0 ORDER BY Package_ID LIMIT 1').all();
|
|
83
|
-
if (roots.length > 0) {
|
|
84
|
-
const pkg = db.prepare('SELECT Package_ID FROM t_package WHERE Parent_ID=? AND Name=? LIMIT 1').get(Number(roots[0].Package_ID), lib.SYNC_PACKAGE_NAME);
|
|
85
|
-
if (pkg) { snapshot.syncPackageId = Number(pkg.Package_ID); }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// diagrams -> view mapping (schema_view_id StyleEx token)
|
|
89
|
-
const diags = db.prepare('SELECT Diagram_ID, Package_ID, Name, StyleEx, ea_guid FROM t_diagram').all();
|
|
90
|
-
for (const d of diags) {
|
|
91
|
-
const v = parseStyleToken(d.StyleEx, 'schema_view_id');
|
|
92
|
-
if (v) {
|
|
93
|
-
const diagramId = Number(d.Diagram_ID);
|
|
94
|
-
snapshot.diagramsByView.set(v, { diagramId, name: String(d.Name || ''), eaGuid: String(d.ea_guid || '') });
|
|
95
|
-
snapshot.viewByDiagram.set(diagramId, v);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// Optionally also bind known view ids via deterministic diagram guid (robust when EA
|
|
99
|
-
// dropped the StyleEx token). Canonical view catalog supplied via --graph.
|
|
100
|
-
if (o.knownViewIds && Array.isArray(o.knownViewIds)) {
|
|
101
|
-
snapshot.knownViewIds = o.knownViewIds;
|
|
102
|
-
for (const v of o.knownViewIds) {
|
|
103
|
-
if (snapshot.diagramsByView.has(v)) { continue; }
|
|
104
|
-
const guid = deterministicGuid('diag:' + v);
|
|
105
|
-
const d = db.prepare('SELECT Diagram_ID, Name FROM t_diagram WHERE ea_guid = ? LIMIT 1').get(guid);
|
|
106
|
-
if (d) {
|
|
107
|
-
const diagramId = Number(d.Diagram_ID);
|
|
108
|
-
snapshot.diagramsByView.set(v, { diagramId, name: String(d.Name || ''), eaGuid: guid });
|
|
109
|
-
snapshot.viewByDiagram.set(diagramId, v);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// element anchor tags
|
|
115
|
-
const elemTags = new Map(); // Object_ID -> {schemaId?, archimateType?}
|
|
116
|
-
try {
|
|
117
|
-
const props = db.prepare("SELECT Object_ID, Property, Value FROM t_objectproperties WHERE Property IN ('schema_id','archimate_type')").all();
|
|
118
|
-
for (const p of props) {
|
|
119
|
-
const oid = Number(p.Object_ID);
|
|
120
|
-
if (!elemTags.has(oid)) { elemTags.set(oid, {}); }
|
|
121
|
-
const t = elemTags.get(oid);
|
|
122
|
-
if (p.Property === 'schema_id') { t.schemaId = String(p.Value); }
|
|
123
|
-
if (p.Property === 'archimate_type') { t.archimateType = String(p.Value); }
|
|
124
|
-
}
|
|
125
|
-
} catch { /* ignore */ }
|
|
126
|
-
|
|
127
|
-
const elems = db.prepare(
|
|
128
|
-
'SELECT Object_ID, Alias, ea_guid, Object_Type, Stereotype, Name, Note, Status, Package_ID FROM t_object'
|
|
129
|
-
).all();
|
|
130
|
-
for (const e of elems) {
|
|
131
|
-
const rec = {
|
|
132
|
-
objectId: Number(e.Object_ID),
|
|
133
|
-
alias: e.Alias === null || e.Alias === undefined ? '' : String(e.Alias),
|
|
134
|
-
eaGuid: String(e.ea_guid || ''),
|
|
135
|
-
objectType: String(e.Object_Type || ''),
|
|
136
|
-
stereotype: String(e.Stereotype || ''),
|
|
137
|
-
name: String(e.Name || ''),
|
|
138
|
-
description: String(e.Note === null || e.Note === undefined ? '' : e.Note),
|
|
139
|
-
status: String(e.Status || ''),
|
|
140
|
-
packageId: Number(e.Package_ID || 0),
|
|
141
|
-
};
|
|
142
|
-
const tag = elemTags.get(rec.objectId);
|
|
143
|
-
if (tag && tag.schemaId) {
|
|
144
|
-
rec.schemaId = tag.schemaId;
|
|
145
|
-
rec.archimateType = tag.archimateType || '';
|
|
146
|
-
snapshot.elementBySchema.set(rec.schemaId, rec);
|
|
147
|
-
}
|
|
148
|
-
if (rec.eaGuid) { snapshot.elementByGuid.set(rec.eaGuid, rec); }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// relationship anchor tags — t_connectortag's value column is literally named VALUE,
|
|
152
|
-
// so alias it to Value for a case-stable row key (unlike t_objectproperties.Value).
|
|
153
|
-
const relTags = new Map(); // Connector_ID -> schemaId
|
|
154
|
-
try {
|
|
155
|
-
const props = db.prepare("SELECT ElementID, Property, VALUE AS Value FROM t_connectortag WHERE Property IN ('schema_id','archimate_relationship_type')").all();
|
|
156
|
-
for (const p of props) {
|
|
157
|
-
const cid = Number(p.ElementID);
|
|
158
|
-
if (!relTags.has(cid)) { relTags.set(cid, {}); }
|
|
159
|
-
const t = relTags.get(cid);
|
|
160
|
-
if (p.Property === 'schema_id') { t.schemaId = String(p.Value); }
|
|
161
|
-
if (p.Property === 'archimate_relationship_type') { t.archimateType = String(p.Value); }
|
|
162
|
-
}
|
|
163
|
-
} catch { /* ignore */ }
|
|
164
|
-
|
|
165
|
-
const conns = db.prepare(
|
|
166
|
-
'SELECT Connector_ID, ea_guid, Name, Connector_Type, Stereotype, Notes, Direction, Start_Object_ID, End_Object_ID FROM t_connector'
|
|
167
|
-
).all();
|
|
168
|
-
for (const c of conns) {
|
|
169
|
-
const rec = {
|
|
170
|
-
connectorId: Number(c.Connector_ID),
|
|
171
|
-
eaGuid: String(c.ea_guid || ''),
|
|
172
|
-
name: String(c.Name || ''),
|
|
173
|
-
connectorType: String(c.Connector_Type || ''),
|
|
174
|
-
stereotype: String(c.Stereotype || ''),
|
|
175
|
-
description: String(c.Notes === null || c.Notes === undefined ? '' : c.Notes),
|
|
176
|
-
direction: String(c.Direction || ''),
|
|
177
|
-
sourceObjectId: Number(c.Start_Object_ID || 0),
|
|
178
|
-
targetObjectId: Number(c.End_Object_ID || 0),
|
|
179
|
-
};
|
|
180
|
-
const tag = relTags.get(rec.connectorId);
|
|
181
|
-
if (tag && tag.schemaId) {
|
|
182
|
-
rec.schemaId = tag.schemaId;
|
|
183
|
-
rec.archimateType = tag.archimateType || '';
|
|
184
|
-
snapshot.relBySchema.set(rec.schemaId, rec);
|
|
185
|
-
}
|
|
186
|
-
if (rec.eaGuid) { snapshot.relByGuid.set(rec.eaGuid, rec); }
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// placements (diagram membership + geometry)
|
|
190
|
-
const objs = db.prepare(
|
|
191
|
-
'SELECT Diagram_ID, Object_ID, Sequence, RectLeft, RectTop, RectRight, RectBottom FROM t_diagramobjects'
|
|
192
|
-
).all();
|
|
193
|
-
for (const r of objs) {
|
|
194
|
-
const diagramId = Number(r.Diagram_ID);
|
|
195
|
-
if (!snapshot.placements.has(diagramId)) { snapshot.placements.set(diagramId, new Map()); }
|
|
196
|
-
snapshot.placements.get(diagramId).set(Number(r.Object_ID), {
|
|
197
|
-
left: Number(r.RectLeft || 0), top: Number(r.RectTop || 0),
|
|
198
|
-
right: Number(r.RectRight || 0), bottom: Number(r.RectBottom || 0),
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
return snapshot;
|
|
202
|
-
} finally {
|
|
203
|
-
try { db.close(); } catch { /* ignore */ }
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
// semanticDiff — classify human changes between base and work snapshots
|
|
209
|
-
// ---------------------------------------------------------------------------
|
|
210
|
-
function normText(s) {
|
|
211
|
-
return String(s === null || s === undefined ? '' : s).trim();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function elementCanonicalContext(work, rec) {
|
|
215
|
-
// Is this EA object part of the projection-owned / canonical surface?
|
|
216
|
-
if (rec.schemaId) { return true; }
|
|
217
|
-
if (rec.packageId === work.syncPackageId) { return true; }
|
|
218
|
-
// placed on any canonical view diagram?
|
|
219
|
-
for (const [diagramId, members] of work.placements) {
|
|
220
|
-
if (work.viewByDiagram.has(diagramId) && members.has(rec.objectId)) { return true; }
|
|
221
|
-
}
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function placedViewIds(work, objectId) {
|
|
226
|
-
const out = [];
|
|
227
|
-
for (const [diagramId, members] of work.placements) {
|
|
228
|
-
const viewId = work.viewByDiagram.get(diagramId);
|
|
229
|
-
if (viewId && members.has(objectId)) { out.push(viewId); }
|
|
230
|
-
}
|
|
231
|
-
return out;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function semanticDiff(base, work, opts) {
|
|
235
|
-
const o = opts || {};
|
|
236
|
-
const proposals = [];
|
|
237
|
-
const summary = {
|
|
238
|
-
addElement: 0, updateElement: 0, removeElement: 0,
|
|
239
|
-
addRelationship: 0, updateRelationship: 0, removeRelationship: 0,
|
|
240
|
-
updateView: 0,
|
|
241
|
-
layoutOnly: 0, geometryOnlyObjects: 0, outOfScopeNew: 0, removedUnanchored: 0, orphanAnchored: 0,
|
|
242
|
-
};
|
|
243
|
-
const push = (p) => { proposals.push(p); summary[p.op] = (summary[p.op] || 0) + 1; };
|
|
244
|
-
|
|
245
|
-
const metaUnchanged = base.meta.sha === work.meta.sha;
|
|
246
|
-
|
|
247
|
-
// --- geometry-only counting (never proposed) -----------------------------
|
|
248
|
-
for (const [diagramId, baseMembers] of base.placements) {
|
|
249
|
-
const viewId = base.viewByDiagram.get(diagramId);
|
|
250
|
-
if (!viewId) { continue; }
|
|
251
|
-
const workMembers = work.placements.get(diagramId);
|
|
252
|
-
if (!workMembers) { continue; }
|
|
253
|
-
for (const [objectId, baseCoords] of baseMembers) {
|
|
254
|
-
const workCoords = workMembers.get(objectId);
|
|
255
|
-
if (!workCoords) { continue; }
|
|
256
|
-
const moved = baseCoords.left !== workCoords.left || baseCoords.top !== workCoords.top ||
|
|
257
|
-
baseCoords.right !== workCoords.right || baseCoords.bottom !== workCoords.bottom;
|
|
258
|
-
if (moved) { summary.layoutOnly++; }
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// --- elements: anchored content / removal / addition ---------------------
|
|
263
|
-
const baseSchemaIds = new Set(base.elementBySchema.keys());
|
|
264
|
-
const workSchemaIds = new Set(work.elementBySchema.keys());
|
|
265
|
-
for (const schemaId of baseSchemaIds) {
|
|
266
|
-
const b = base.elementBySchema.get(schemaId);
|
|
267
|
-
const w = work.elementBySchema.get(schemaId);
|
|
268
|
-
if (!w) {
|
|
269
|
-
// anchored element removed from the model -> removeElement
|
|
270
|
-
push({ op: 'removeElement', kind: 'element', id: schemaId, sourceEa: { guid: b.eaGuid } });
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
const fields = {};
|
|
274
|
-
if (normText(b.name) !== normText(w.name)) { fields.name = w.name; }
|
|
275
|
-
if (normText(b.description) !== normText(w.description)) { fields.description = w.description; }
|
|
276
|
-
if (normText(b.status) !== normText(w.status)) { fields.status = w.status; }
|
|
277
|
-
if (Object.keys(fields).length > 0) {
|
|
278
|
-
push({ op: 'updateElement', kind: 'element', id: schemaId, fields, sourceEa: { guid: w.eaGuid } });
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
for (const schemaId of workSchemaIds) {
|
|
282
|
-
if (baseSchemaIds.has(schemaId)) { continue; }
|
|
283
|
-
// anchored id present only in work (e.g. human duplicated a tagged object) — cannot map
|
|
284
|
-
// to a canonical id safely; count and skip (agent reconciles).
|
|
285
|
-
summary.orphanAnchored++;
|
|
286
|
-
}
|
|
287
|
-
// unanchored NEW objects in work (human drew fresh boxes)
|
|
288
|
-
for (const [eaGuid, rec] of work.elementByGuid) {
|
|
289
|
-
if (rec.schemaId) { continue; }
|
|
290
|
-
if (base.elementByGuid.has(eaGuid)) { continue; }
|
|
291
|
-
if (!elementCanonicalContext(work, rec)) { summary.outOfScopeNew++; continue; }
|
|
292
|
-
const viewIds = placedViewIds(work, rec.objectId);
|
|
293
|
-
push({
|
|
294
|
-
op: 'addElement', kind: 'element', id: null,
|
|
295
|
-
proposed: {
|
|
296
|
-
name: rec.name,
|
|
297
|
-
description: rec.description === '' ? undefined : rec.description,
|
|
298
|
-
eaType: { objectType: rec.objectType, stereotype: rec.stereotype },
|
|
299
|
-
viewIds: viewIds.length > 0 ? viewIds : undefined,
|
|
300
|
-
},
|
|
301
|
-
sourceEa: { guid: eaGuid, objectId: rec.objectId },
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
// unanchored objects removed in work (never canonical -> no proposal, count only)
|
|
305
|
-
for (const [eaGuid, rec] of base.elementByGuid) {
|
|
306
|
-
if (rec.schemaId) { continue; }
|
|
307
|
-
if (work.elementByGuid.has(eaGuid)) { continue; }
|
|
308
|
-
if (elementCanonicalContext(base, rec)) { summary.removedUnanchored++; }
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// --- relationships --------------------------------------------------------
|
|
312
|
-
const baseRelIds = new Set(base.relBySchema.keys());
|
|
313
|
-
const workRelIds = new Set(work.relBySchema.keys());
|
|
314
|
-
const schemaOfObject = (snap, objectId) => {
|
|
315
|
-
for (const [, e] of snap.elementBySchema) { if (e.objectId === objectId) { return e.schemaId; } }
|
|
316
|
-
const w = snap.elementByGuid;
|
|
317
|
-
for (const [, e] of w) { if (e.objectId === objectId && !e.schemaId) { return { newGuid: e.eaGuid }; } }
|
|
318
|
-
return null;
|
|
319
|
-
};
|
|
320
|
-
for (const schemaId of baseRelIds) {
|
|
321
|
-
const b = base.relBySchema.get(schemaId);
|
|
322
|
-
const w = work.relBySchema.get(schemaId);
|
|
323
|
-
if (!w) {
|
|
324
|
-
push({ op: 'removeRelationship', kind: 'relationship', id: schemaId, sourceEa: { guid: b.eaGuid } });
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
const fields = {};
|
|
328
|
-
if (normText(b.name) !== normText(w.name)) { fields.name = w.name; }
|
|
329
|
-
if (normText(b.description) !== normText(w.description)) { fields.description = w.description; }
|
|
330
|
-
const s = schemaOfObject(work, w.sourceObjectId);
|
|
331
|
-
const t = schemaOfObject(work, w.targetObjectId);
|
|
332
|
-
const bS = schemaOfObject(base, b.sourceObjectId);
|
|
333
|
-
const bT = schemaOfObject(base, b.targetObjectId);
|
|
334
|
-
const src = s && typeof s === 'object' ? null : s;
|
|
335
|
-
const tgt = t && typeof t === 'object' ? null : t;
|
|
336
|
-
const bSrc = bS && typeof bS === 'object' ? null : bS;
|
|
337
|
-
const bTgt = bT && typeof bT === 'object' ? null : bT;
|
|
338
|
-
if (src !== bSrc) { fields.sourceId = src; }
|
|
339
|
-
if (tgt !== bTgt) { fields.targetId = tgt; }
|
|
340
|
-
if (Object.keys(fields).length > 0) {
|
|
341
|
-
push({ op: 'updateRelationship', kind: 'relationship', id: schemaId, fields, sourceEa: { guid: w.eaGuid } });
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
for (const schemaId of workRelIds) {
|
|
345
|
-
if (baseRelIds.has(schemaId)) { continue; }
|
|
346
|
-
summary.orphanAnchored++;
|
|
347
|
-
}
|
|
348
|
-
// unanchored NEW connectors in work (human drew fresh links)
|
|
349
|
-
for (const [eaGuid, rec] of work.relByGuid) {
|
|
350
|
-
if (rec.schemaId) { continue; }
|
|
351
|
-
if (base.relByGuid.has(eaGuid)) { continue; }
|
|
352
|
-
// canonical context: at least one endpoint anchored/placed on a canonical view
|
|
353
|
-
const src = work.elementByGuid.get(guidOfObject(work, rec.sourceObjectId));
|
|
354
|
-
const tgt = work.elementByGuid.get(guidOfObject(work, rec.targetObjectId));
|
|
355
|
-
const srcContext = src ? elementCanonicalContext(work, src) : false;
|
|
356
|
-
const tgtContext = tgt ? elementCanonicalContext(work, tgt) : false;
|
|
357
|
-
if (!srcContext && !tgtContext) { summary.outOfScopeNew++; continue; }
|
|
358
|
-
const viewIds = [];
|
|
359
|
-
for (const [diagramId, members] of work.placements) {
|
|
360
|
-
const viewId = work.viewByDiagram.get(diagramId);
|
|
361
|
-
if (viewId && members.has(rec.sourceObjectId) && members.has(rec.targetObjectId)) { viewIds.push(viewId); }
|
|
362
|
-
}
|
|
363
|
-
const srcRef = src ? (src.schemaId || { newGuid: src.eaGuid }) : null;
|
|
364
|
-
const tgtRef = tgt ? (tgt.schemaId || { newGuid: tgt.eaGuid }) : null;
|
|
365
|
-
push({
|
|
366
|
-
op: 'addRelationship', kind: 'relationship', id: null,
|
|
367
|
-
proposed: {
|
|
368
|
-
name: rec.name === '' ? undefined : rec.name,
|
|
369
|
-
sourceRef: srcRef, targetRef: tgtRef,
|
|
370
|
-
eaType: { connectorType: rec.connectorType, stereotype: rec.stereotype },
|
|
371
|
-
viewIds: viewIds.length > 0 ? viewIds : undefined,
|
|
372
|
-
},
|
|
373
|
-
sourceEa: { guid: eaGuid, connectorId: rec.connectorId },
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// --- view membership (anchored objects only, still present in the model) ---
|
|
378
|
-
const allViewIds = new Set([...base.viewByDiagram.values(), ...work.viewByDiagram.values()]);
|
|
379
|
-
for (const viewId of allViewIds) {
|
|
380
|
-
const bDiag = base.diagramsByView.get(viewId);
|
|
381
|
-
const wDiag = work.diagramsByView.get(viewId);
|
|
382
|
-
if (!bDiag || !wDiag) { continue; } // view's diagram added/removed wholesale: out of v1 scope
|
|
383
|
-
const bMembers = base.placements.get(bDiag.diagramId) || new Map();
|
|
384
|
-
const wMembers = work.placements.get(wDiag.diagramId) || new Map();
|
|
385
|
-
const addMembers = [];
|
|
386
|
-
const removeMembers = [];
|
|
387
|
-
for (const objectId of wMembers.keys()) {
|
|
388
|
-
if (bMembers.has(objectId)) { continue; }
|
|
389
|
-
const rec = elemByObjectId(work, objectId);
|
|
390
|
-
if (!rec || !rec.schemaId) { continue; } // unanchored new placement -> rides addElement
|
|
391
|
-
addMembers.push(rec.schemaId);
|
|
392
|
-
}
|
|
393
|
-
for (const objectId of bMembers.keys()) {
|
|
394
|
-
if (wMembers.has(objectId)) { continue; }
|
|
395
|
-
const rec = elemByObjectId(base, objectId);
|
|
396
|
-
if (!rec || !rec.schemaId) { continue; }
|
|
397
|
-
// removed from diagram but object must still exist in the model (else removeElement)
|
|
398
|
-
if (!work.elementBySchema.has(rec.schemaId)) { continue; }
|
|
399
|
-
removeMembers.push(rec.schemaId);
|
|
400
|
-
}
|
|
401
|
-
if (addMembers.length > 0 || removeMembers.length > 0) {
|
|
402
|
-
push({
|
|
403
|
-
op: 'updateView', kind: 'view', viewId,
|
|
404
|
-
addMembers: addMembers.length > 0 ? addMembers : undefined,
|
|
405
|
-
removeMembers: removeMembers.length > 0 ? removeMembers : undefined,
|
|
406
|
-
sourceEa: {},
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// orphan/other counts merge
|
|
412
|
-
summary.metaUnchanged = metaUnchanged;
|
|
413
|
-
return { proposals, summary };
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function guidOfObject(snap, objectId) {
|
|
417
|
-
for (const [, e] of snap.elementByGuid) { if (e.objectId === objectId) { return e.eaGuid; } }
|
|
418
|
-
return '';
|
|
419
|
-
}
|
|
420
|
-
function elemByObjectId(snap, objectId) {
|
|
421
|
-
for (const [, e] of snap.elementByGuid) { if (e.objectId === objectId) { return e; } }
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// ---------------------------------------------------------------------------
|
|
426
|
-
// Output rendering (JSON + Markdown)
|
|
427
|
-
// ---------------------------------------------------------------------------
|
|
428
|
-
function buildJsonResult(args) {
|
|
429
|
-
return {
|
|
430
|
-
format: 'archgraph-ea-human-diff',
|
|
431
|
-
version: 1,
|
|
432
|
-
source: { base: args.base, work: args.work },
|
|
433
|
-
baselineCommit: args.baselineCommit || null,
|
|
434
|
-
extractedAt: new Date().toISOString(),
|
|
435
|
-
summary: args.summary,
|
|
436
|
-
proposals: args.proposals,
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function renderMarkdown(result) {
|
|
441
|
-
const s = result.summary;
|
|
442
|
-
const lines = [];
|
|
443
|
-
lines.push('# EA 人类草稿语义 diff(ea-human-diff)');
|
|
444
|
-
lines.push('');
|
|
445
|
-
lines.push(`- 基线(committed .qea):\`${result.source.base}\``);
|
|
446
|
-
lines.push(`- 工作区(human-edited .qea):\`${result.source.work}\``);
|
|
447
|
-
if (result.baselineCommit) { lines.push(`- 基线 commit:\`${result.baselineCommit}\``); }
|
|
448
|
-
lines.push(`- 提取时间:${result.extractedAt}`);
|
|
449
|
-
lines.push('');
|
|
450
|
-
lines.push('## 摘要');
|
|
451
|
-
lines.push('');
|
|
452
|
-
lines.push('| 操作 | 数量 |');
|
|
453
|
-
lines.push('| --- | --- |');
|
|
454
|
-
const opOrder = ['addElement', 'updateElement', 'removeElement', 'addRelationship', 'updateRelationship', 'removeRelationship', 'updateView'];
|
|
455
|
-
for (const op of opOrder) {
|
|
456
|
-
const label = { addElement: '新增元素', updateElement: '更新元素', removeElement: '删除元素', addRelationship: '新增关系', updateRelationship: '更新关系', removeRelationship: '删除关系', updateView: '视图成员' }[op];
|
|
457
|
-
lines.push(`| ${label}(${op}) | ${s[op] || 0} |`);
|
|
458
|
-
}
|
|
459
|
-
lines.push(`| 纯几何移动(不产出,语义优先排除) | ${s.layoutOnly || 0} |`);
|
|
460
|
-
lines.push(`| 超出 canonical 作用域的新对象(跳过) | ${s.outOfScopeNew || 0} |`);
|
|
461
|
-
lines.push(`| 删除的无锚对象(从未入 canonical,跳过) | ${s.removedUnanchored || 0} |`);
|
|
462
|
-
lines.push(`| 镜像(kg_sync_meta)未变 | ${s.metaUnchanged ? '是' : '否'} |`);
|
|
463
|
-
lines.push('');
|
|
464
|
-
if (result.proposals.length === 0) {
|
|
465
|
-
lines.push('> 未检测到 canonical 语义提议(纯几何/超出作用域改动不计)。');
|
|
466
|
-
lines.push('');
|
|
467
|
-
}
|
|
468
|
-
const groups = {
|
|
469
|
-
addElement: '新增元素提议', updateElement: '更新元素提议', removeElement: '删除元素提议',
|
|
470
|
-
addRelationship: '新增关系提议', updateRelationship: '更新关系提议', removeRelationship: '删除关系提议',
|
|
471
|
-
updateView: '视图成员提议',
|
|
472
|
-
};
|
|
473
|
-
for (const op of opOrder) {
|
|
474
|
-
const items = result.proposals.filter((p) => p.op === op);
|
|
475
|
-
if (items.length === 0) { continue; }
|
|
476
|
-
lines.push(`## ${groups[op]}(${items.length})`);
|
|
477
|
-
lines.push('');
|
|
478
|
-
for (const p of items) {
|
|
479
|
-
if (op === 'addElement') {
|
|
480
|
-
lines.push(`- **${p.proposed.name}** — id 待 agent 分配;EA 类型 \`${p.proposed.eaType.objectType}\`/\`${p.proposed.eaType.stereotype}\`${p.proposed.viewIds ? `;视图候选 ${p.proposed.viewIds.join(', ')}` : ''}${p.proposed.description ? `;描述:${p.proposed.description.slice(0, 120)}` : ''};EA \`${p.sourceEa.guid}\``);
|
|
481
|
-
} else if (op === 'updateElement') {
|
|
482
|
-
lines.push(`- \`${p.id}\` — ${Object.entries(p.fields).map(([k, v]) => `${k} → ${String(v).slice(0, 80)}`).join(';')};EA \`${p.sourceEa.guid}\``);
|
|
483
|
-
} else if (op === 'removeElement' || op === 'removeRelationship') {
|
|
484
|
-
lines.push(`- \`${p.id}\` — 待 agent 确认后删除;EA \`${p.sourceEa.guid}\``);
|
|
485
|
-
} else if (op === 'addRelationship') {
|
|
486
|
-
const src = typeof p.proposed.sourceRef === 'object' ? `新元素 ${p.proposed.sourceRef.newGuid}` : p.proposed.sourceRef;
|
|
487
|
-
const tgt = typeof p.proposed.targetRef === 'object' ? `新元素 ${p.proposed.targetRef.newGuid}` : p.proposed.targetRef;
|
|
488
|
-
lines.push(`- ${p.proposed.name ? `**${p.proposed.name}** ` : ''}${src} → ${tgt};EA 类型 \`${p.proposed.eaType.connectorType}\`/\`${p.proposed.eaType.stereotype}\`;EA \`${p.sourceEa.guid}\``);
|
|
489
|
-
} else if (op === 'updateRelationship') {
|
|
490
|
-
lines.push(`- \`${p.id}\` — ${Object.entries(p.fields).map(([k, v]) => `${k} → ${String(v).slice(0, 80)}`).join(';')};EA \`${p.sourceEa.guid}\``);
|
|
491
|
-
} else if (op === 'updateView') {
|
|
492
|
-
const a = p.addMembers ? `加入:${p.addMembers.join(', ')}` : '';
|
|
493
|
-
const r = p.removeMembers ? `移除:${p.removeMembers.join(', ')}` : '';
|
|
494
|
-
lines.push(`- 视图 \`${p.viewId}\` — ${[a, r].filter(Boolean).join(';')}`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
lines.push('');
|
|
498
|
-
}
|
|
499
|
-
lines.push('> 本 diff 基于 EA 可见对象模型(schema_id 锚 tag 对齐),不读 kg_sync_meta;几何不进 canonical。交由 agent 经 ARGO preview/apply 写入图谱。');
|
|
500
|
-
lines.push('');
|
|
501
|
-
return lines.join('\n');
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// ---------------------------------------------------------------------------
|
|
505
|
-
// CLI
|
|
506
|
-
// ---------------------------------------------------------------------------
|
|
507
|
-
function parseArgs(argv) {
|
|
508
|
-
const args = { base: '', work: '', graph: '', out: '', baselineCommit: '', md: true };
|
|
509
|
-
for (let i = 0; i < argv.length; i++) {
|
|
510
|
-
const a = argv[i];
|
|
511
|
-
const next = () => (i + 1 < argv.length ? argv[++i] : '');
|
|
512
|
-
if (a === '--base') { args.base = next(); }
|
|
513
|
-
else if (a === '--work') { args.work = next(); }
|
|
514
|
-
else if (a === '--graph') { args.graph = next(); }
|
|
515
|
-
else if (a === '--out') { args.out = next(); }
|
|
516
|
-
else if (a === '--baseline-commit') { args.baselineCommit = next(); }
|
|
517
|
-
else if (a === '--no-md') { args.md = false; }
|
|
518
|
-
}
|
|
519
|
-
return args;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Auto-baseline: extract the committed (HEAD) version of the working .qea as the baseline.
|
|
523
|
-
// Lets the day-to-day flow be a single command when --work is a tracked repo file.
|
|
524
|
-
function gitShowHeadBlob(relPath) {
|
|
525
|
-
const { execFileSync } = require('node:child_process');
|
|
526
|
-
return execFileSync('git', ['cat-file', 'blob', 'HEAD:' + relPath], { maxBuffer: 512 * 1024 * 1024 });
|
|
527
|
-
}
|
|
528
|
-
function gitToplevel() {
|
|
529
|
-
const { execFileSync } = require('node:child_process');
|
|
530
|
-
try {
|
|
531
|
-
return execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
532
|
-
} catch { return ''; }
|
|
533
|
-
}
|
|
534
|
-
// Resolve the CURRENT PROJECT's .qea (same convention as the projection):
|
|
535
|
-
// ARGO_EA_QEA env > exactly one *.qea at the git top-level (else cwd).
|
|
536
|
-
// Called when --work is omitted so the day-to-day command never hardcodes a filename.
|
|
537
|
-
function resolveProjectQea() {
|
|
538
|
-
if (process.env.ARGO_EA_QEA) { return process.env.ARGO_EA_QEA; }
|
|
539
|
-
const root = gitToplevel() || process.cwd();
|
|
540
|
-
let qeas = [];
|
|
541
|
-
try { qeas = fs.readdirSync(root).filter((f) => f.toLowerCase().endsWith('.qea')); } catch { qeas = []; }
|
|
542
|
-
if (qeas.length === 1) { return path.join(root, qeas[0]); }
|
|
543
|
-
return '';
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function resolveAutoBase(workPath) {
|
|
547
|
-
const { execFileSync } = require('node:child_process');
|
|
548
|
-
const workAbs = path.resolve(process.cwd(), workPath);
|
|
549
|
-
let root;
|
|
550
|
-
try {
|
|
551
|
-
root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim();
|
|
552
|
-
} catch {
|
|
553
|
-
throw new Error('--base omitted but git toplevel unavailable; pass --base <baseline.qea> explicitly');
|
|
554
|
-
}
|
|
555
|
-
const rel = path.relative(root, workAbs).split(path.sep).join('/');
|
|
556
|
-
if (rel.startsWith('..')) {
|
|
557
|
-
throw new Error('--work (' + workAbs + ') is outside the git repo; pass --base <baseline.qea> explicitly');
|
|
558
|
-
}
|
|
559
|
-
let blob;
|
|
560
|
-
try {
|
|
561
|
-
blob = gitShowHeadBlob(rel);
|
|
562
|
-
} catch {
|
|
563
|
-
throw new Error('git HEAD has no tracked file "' + rel + '"; pass --base <baseline.qea> explicitly');
|
|
564
|
-
}
|
|
565
|
-
const tmp = path.join(require('node:os').tmpdir(), 'ea-human-diff-base-' + process.pid + '.qea');
|
|
566
|
-
fs.writeFileSync(tmp, blob);
|
|
567
|
-
let short = '';
|
|
568
|
-
try { short = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { encoding: 'utf8' }).trim(); } catch { /* ignore */ }
|
|
569
|
-
return { base: tmp, baselineCommit: short };
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function main() {
|
|
573
|
-
const args = parseArgs(process.argv.slice(2));
|
|
574
|
-
if (!args.work) {
|
|
575
|
-
const auto = resolveProjectQea();
|
|
576
|
-
if (auto) { args.work = auto; }
|
|
577
|
-
}
|
|
578
|
-
if (!args.base && !args.work) {
|
|
579
|
-
console.error('usage: node argo/scripts/ea-human-diff.js [--base <base.qea>] [--work <work.qea>] [--graph <json>] [--out <stem>] [--baseline-commit <sha>] [--no-md]');
|
|
580
|
-
console.error(' (--work optional: defaults to the current project root single *.qea; --base optional: the committed HEAD version of --work is the baseline)');
|
|
581
|
-
process.exit(2);
|
|
582
|
-
}
|
|
583
|
-
if (args.base === '' && args.work) {
|
|
584
|
-
try {
|
|
585
|
-
const auto = resolveAutoBase(args.work);
|
|
586
|
-
args.base = auto.base;
|
|
587
|
-
if (!args.baselineCommit) { args.baselineCommit = auto.baselineCommit; }
|
|
588
|
-
} catch (err) {
|
|
589
|
-
console.error('ea-human-diff: ' + err.message);
|
|
590
|
-
process.exit(2);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
for (const f of [args.base, args.work]) {
|
|
594
|
-
if (!fs.existsSync(f)) { console.error('file not found: ' + f); process.exit(2); }
|
|
595
|
-
}
|
|
596
|
-
const knownViewIds = null;
|
|
597
|
-
if (args.graph && fs.existsSync(args.graph)) {
|
|
598
|
-
try {
|
|
599
|
-
const g = JSON.parse(fs.readFileSync(args.graph, 'utf8').replace(/^\uFEFF/, ''));
|
|
600
|
-
args._knownViewIds = (g.views || []).map((v) => String(v.view_id));
|
|
601
|
-
} catch { /* optional catalog */ }
|
|
602
|
-
}
|
|
603
|
-
const base = readSnapshot(args.base, { knownViewIds: args._knownViewIds });
|
|
604
|
-
const work = readSnapshot(args.work, { knownViewIds: args._knownViewIds });
|
|
605
|
-
const { proposals, summary } = semanticDiff(base, work, {});
|
|
606
|
-
const result = buildJsonResult({ base: args.base, work: args.work, baselineCommit: args.baselineCommit, summary, proposals });
|
|
607
|
-
if (args.out) {
|
|
608
|
-
const stem = path.resolve(process.cwd(), args.out);
|
|
609
|
-
fs.writeFileSync(stem + '.json', JSON.stringify(result, null, 2), 'utf8');
|
|
610
|
-
if (args.md) {
|
|
611
|
-
fs.writeFileSync(stem + '.md', renderMarkdown(result), 'utf8');
|
|
612
|
-
}
|
|
613
|
-
console.log('ea-human-diff written: ' + stem + '.json' + (args.md ? ' + ' + stem + '.md' : ''));
|
|
614
|
-
console.log(JSON.stringify({ summary }, null, 2));
|
|
615
|
-
} else {
|
|
616
|
-
console.log(JSON.stringify(result, null, 2));
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
if (require.main === module) { main(); }
|
|
621
|
-
|
|
622
|
-
module.exports = {
|
|
623
|
-
parseStyleToken,
|
|
624
|
-
readSnapshot,
|
|
625
|
-
semanticDiff,
|
|
626
|
-
buildJsonResult,
|
|
627
|
-
renderMarkdown,
|
|
628
|
-
};
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: ea-human-draft
|
|
3
|
-
description: "把人类在 EA 里对当前项目 .qea 的草稿改动收敛进正式图谱(WP2792 draft-proposal 流程):① 断言前提(EA 已关闭 / 项目根唯一 *.qea 可还原 / 工具存在)→ ② 用 ea-human-diff 提取语义 diff 提议(默认自动发现当前项目根唯一 *.qea,JSON+Markdown,读 EA 可见对象模型、不读 kg_sync_meta、纯几何不产出)→ ③ git restore 还原 .qea 到上次提交 → ④ 把提议交给 agent/人类伙伴,按其指引经 ARGO preview/apply 写回 canonical JSON(自动增量投影回 .qea 并 commit)。Use when 人类专家用 Sparx EA 直接改了当前项目的 .qea 需要并入正式图谱、提取人类 EA 改动的语义 diff、把 .qea 还原回已提交状态、或按 draft-proposal 收敛人类草稿。Keywords: EA 人类草稿, human draft, semantic diff, ea-human-diff, draft-proposal, reverse-ea2kg, 人类参与建图."
|
|
4
|
-
argument-hint: 人类 EA 草稿收敛
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# EA HUMAN DRAFT(人类 EA 草稿 → 语义 diff → 写回图谱)
|
|
8
|
-
|
|
9
|
-
定位:**EA 只当草稿纸**,`design/KG/SystemArchitecture.json` 是唯一真源。人类在 agent 空闲间隙用 EA 改**当前项目的 `.qea`**(即仓库根的唯一 `*.qea`,不限定文件名);本技能负责把人类改动**安全地提取为提议**(断言 + diff + 还原),并把提议交给 agent/人类伙伴,按其指引经 ARGO 写回 canonical——不建立有损的全自动 EA→JSON 反向投影。
|
|
10
|
-
|
|
11
|
-
依赖工具:`argo/scripts/ea-human-diff.js`(仓库内)或部署版 `~/.argo/scripts/ea-human-diff.js`(随 `archgraph-argo` npm 包发布)。`--work` 默认指向**当前项目根唯一 `*.qea`**(自动发现,`ARGO_EA_QEA` > 仓库根唯一 `*.qea`),`--base` 可省略:自动取 git HEAD 里 `--work` 的版本作基线。
|
|
12
|
-
|
|
13
|
-
## 前置(Assert —— 全部满足才继续,任一失败即停下报告)
|
|
14
|
-
|
|
15
|
-
- [ ] 工作区含 `design/KG/SystemArchitecture.json`,且存在仓库根唯一 `*.qea`(自动发现:`Get-ChildItem *.qea` 恰一个,或 `$env:ARGO_EA_QEA` 已设)。该 `.qea` 被 git 跟踪(`git ls-files <该qea>` 有输出)。
|
|
16
|
-
- [ ] diff 工具存在:仓库 `argo/scripts/ea-human-diff.js` 或部署 `~/.argo/scripts/ea-human-diff.js`。
|
|
17
|
-
- [ ] **EA 已完全关闭**(人类改动已保存落盘)——否则读到的不是最终状态,且 `git restore` 可能被文件锁破坏/EA 关盘重写。
|
|
18
|
-
- [ ] git 可用;`HEAD` 中存在该 `.qea`(自动基线依赖)。若人类还没开改(`git status --short <该qea>` 为空)→ 说明:需先在 EA 里改、保存、关闭后再回来。
|
|
19
|
-
|
|
20
|
-
Windows 可选核实无进程持有:
|
|
21
|
-
```powershell
|
|
22
|
-
Get-Process EA -ErrorAction SilentlyContinue # 有输出则先关闭 EA
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Workflow
|
|
26
|
-
|
|
27
|
-
### 1 · Assert(断言)
|
|
28
|
-
逐条检查上方前置并报告结果。任一失败 → 停下,不做 diff、不做 revert。
|
|
29
|
-
|
|
30
|
-
### 2 · 提取语义 diff
|
|
31
|
-
仓库根执行(`--work` 默认 = 当前项目根唯一 `*.qea`,无需写死文件名;`--base` 省略 = git HEAD 版本):
|
|
32
|
-
```powershell
|
|
33
|
-
node argo/scripts/ea-human-diff.js --out results/human-draft
|
|
34
|
-
```
|
|
35
|
-
产物:
|
|
36
|
-
- `results/human-draft.json` —— 机器提议集(`proposals[]`:`op`/`kind`/`id`/`fields`/`proposed`/`sourceEa` 等 + `summary`)。
|
|
37
|
-
- `results/human-draft.md` —— 人读摘要(分类表格 + 逐条明细 + EA guid 溯源)。
|
|
38
|
-
|
|
39
|
-
先给人伙伴看 `human-draft.md`:确认是预期改动、无意外删除;留意 `layoutOnly` / `outOfScopeNew` / `removedUnanchored` 计数(这些不产出提议)。
|
|
40
|
-
若仓库根有多个 `*.qea` 或未自动发现,须显式 `--work <项目.qea>`;若 `--work` 不是 git 跟踪文件(如临时副本),须显式 `--base <committed.qea>`,不能用自动基线。
|
|
41
|
-
|
|
42
|
-
### 3 · 还原 .qea 到上次提交
|
|
43
|
-
```powershell
|
|
44
|
-
git restore <项目.qea>
|
|
45
|
-
git status --short # 应只剩 results/human-draft.* 等产物,<项目.qea> 不再 dirty
|
|
46
|
-
```
|
|
47
|
-
目的:把 `.qea` 拉回与 HEAD 一致,杜绝残留分叉;人类草稿只以提议文件形式存在,避免后续 agent 写图触发投影时静默覆盖/合并混乱。
|
|
48
|
-
|
|
49
|
-
### 4 · 交给下一步(按人类指引写回正式图谱)
|
|
50
|
-
提议**不会自动写回**。把 `human-draft.json`/`.md` 呈现给人类伙伴,按其指引执行(或交给负责写图的 agent):
|
|
51
|
-
1. 审阅每条提议(add/update/remove / 视图成员 / 删除需确认),必要时删改。
|
|
52
|
-
2. 经 ARGO MCP `previewSystemArchitectureMutation` → `applySystemArchitectureMutation` 写入 canonical JSON。
|
|
53
|
-
3. apply 后 MCP 自动增量投影回 `.qea`——**不要手工编辑 .qea 覆盖**。
|
|
54
|
-
4. 对受影响元素跑回归验收(WP2792 AT 集等)后 `git commit`,并把 commit id 登记到对应图谱元素的 `commit` 属性(见全局 ArchGraph 红线)。
|
|
55
|
-
|
|
56
|
-
## Rules
|
|
57
|
-
|
|
58
|
-
- **MUST** 只在 EA 关闭后运行;diff 前先做断言并报告。
|
|
59
|
-
- **MUST** diff 以 EA **可见对象模型**(`schema_id` 锚 tag 对齐)为准;**绝不**用 `kg_sync_meta` 判定人类改动(人类改动不进镜像)。
|
|
60
|
-
- **MUST** 提取 diff 后先 `git restore <项目.qea>` 再进入写回阶段,避免 qea 侧残留被后续投影覆盖/丢失。
|
|
61
|
-
- **MUST** 把每条**删除**提议(removeElement/removeRelationship)标记为需人类/负责 agent 确认后再 apply。
|
|
62
|
-
- **MUST NOT** 把 `human-draft.json` 当 canonical 直接写——必须先 ARGO `preview` 校验,再 `apply`。
|
|
63
|
-
- **MUST NOT** 在 `.qea` 上手工写 canonical 内容(唯一写回通道是 canonical JSON → 自动增量投影)。
|
|
64
|
-
- **MUST NOT** 读取/复述 `.env` 中的 secret。
|
|
65
|
-
- 纯几何(`t_diagramobjects` 坐标)**不进提议**(语义优先);若人类排版也是交付物,需另行走布局侧车(本技能不产出)。
|
|
66
|
-
|
|
67
|
-
## 产物
|
|
68
|
-
|
|
69
|
-
| 文件 | 内容 |
|
|
70
|
-
| --- | --- |
|
|
71
|
-
| `results/human-draft.json` | 机器提议集:source / baselineCommit / extractedAt / summary / proposals[](每条含 op / kind / id / fields / proposed / sourceEa) |
|
|
72
|
-
| `results/human-draft.md` | 人读摘要:操作计数表 + 逐条明细(EA guid 溯源)+ 排除说明(layoutOnly 等) |
|
|
73
|
-
|
|
74
|
-
## 故障排查
|
|
75
|
-
|
|
76
|
-
- `git HEAD has no tracked file "<项目.qea>"`:`--work` 未被 git 跟踪 → 显式 `--base <committed.qea>`。
|
|
77
|
-
- `git restore` 失败/文件锁:EA 还开着 → 关闭 EA 后重试。
|
|
78
|
-
- diff 为空但人类确实改过:多半只做了纯几何或超出 canonical 作用域改动 → 看 `.md` 的 `layoutOnly` / `outOfScopeNew` 计数。
|
|
79
|
-
- 关系侧提议异常(id 为 undefined):确认基线 .qea 由投影生成(`t_connectortag` 带 `schema_id`),手绘模型无锚时关系不参与。
|