archgraph-argo 0.15.2 → 0.16.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.
@@ -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();
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: ea-human-reconcile
3
+ description: "把人类从 EA 提取的 draft 提议(results/human-draft.md,内嵌 JSON 提议集)交给 Agent 后,Agent 结合当前全局(canonical 意图图 design/KG/SystemArchitecture.json + Neo4j 语义检索)逐条/整体分析并给出建议,最终由人类裁决是否写入。它是『人类 EA 草稿 → 语义 diff 提取 → 人类裁决写回』的最后一步:Agent 只做分析+建议,绝不自作主张 apply。Use when the user hands over an EA human-draft proposal set (human-draft.md) and wants the agent to analyze it against the whole architecture and propose, leaving the final decision to the human. Keywords: EA human draft, reconcile, EA 人类草稿裁决, draft review, 语义diff建议, ARGO preview/apply, 人类裁决."
4
+ argument-hint: human-draft
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # EA HUMAN DRAFT RECONCILE(EA 人类草稿 → Agent 分析建议 → 人类裁决)
9
+
10
+ Agent 的职责:结合全局分析 draft 中的每条提议、给出建议与理由、识别风险,把结论呈现给人类,由人类裁决。Agent 不自动执行写入。
11
+
12
+ ## 前置
13
+
14
+ - [ ] 存在 `results/human-draft.md`(人类从 EA 提取的草稿提议集)。
15
+ - [ ] 工作区有 `design/KG/SystemArchitecture.json`(canonical 意图图)——分析全局的依据。
16
+ - [ ] ARGO MCP 可用。关键查询手段:
17
+ - 语义/上下文:`getSystemArchitecture`(带 query.purpose + query.intent)、`getIntentElementContext`(看单个元素/关系/视图的依赖与受影响对象)。
18
+ - 结构/类型:`queryNeo4jGraph`(只读 Cypher,如 `MATCH (e:Element {graphKey: $graphKey, type:'Business Actor'}) ...`;先 `{schema:true}` 查投影 schema)。
19
+ - 校验:`validateSystemArchitecture`;写图:`previewSystemArchitectureMutation` / `applySystemArchitectureMutation`。
20
+
21
+ ## 原则
22
+
23
+ - **人类裁决**:Agent 只分析、给建议、识别风险;**MUST NOT 未经人类裁决就 `applySystemArchitectureMutation`(或任何写图)**。人类拍板(采纳/拒绝/修改)后才写。
24
+ - **全局优先**:不只对着 draft 字段,要用查询手段读 canonical 中相关对象及其依赖(被谁引用、级联影响谁、子视图是否悬空、命名/类型是否与全局一致、是否与既有元素重复冲突),再下判断。
25
+ - **删除/破坏性提议单独把关**:对 `removeElement` / `removeRelationship` / `removeView`,必须指出 canonical 里谁引用它、删除会级联影响谁,并给出「保留 / 改挂 / 确认删除」的建议,交由人类确认。
26
+ - **新增/更新提议**:核对 type / name / description / attributes 是否与全局命名与类型一致、是否与既有元素/关系重复冲突、应挂在哪个 parent / 视图下。
27
+ - **视图提议**:核对成员是否落在正确层级(parent_element_id 是否合理)、删视图会否使相关 subdiagram_views 悬空。
28
+ - **只读取、不泄露**:MUST NOT 读取/复述 `.env` 里的 secret 或推断敏感配置值。
29
+
30
+ ## 目标
31
+
32
+ 读 draft → 结合全局逐条分析 → 给出「逐条建议 + 整体结论 + 待人类裁决项」。具体产出:
33
+
34
+ 1. **操作统计**:add/update/remove × element/relationship/view。
35
+ 2. **逐条建议**:每条含 `提议`(op + id/名称)、`分析`(全局依赖/冲突/层级)、`建议`(采纳/拒绝/修改后采纳,附理由)、`风险`(若有)。
36
+ 3. **整体结论**:这批草稿的整体风险等级、必人工确认的高风险项。
37
+ 4. **待裁决**:明确列出需人类拍板的条目,并询问哪些采纳、哪些修改、哪些拒绝。
38
+
39
+ **等待人类答复之后**才进入写回;若人类同意,才经 `previewSystemArchitectureMutation` →(确认后)`applySystemArchitectureMutation`;拒绝/搁置的不写。
package/install-argo.ps1 CHANGED
@@ -761,10 +761,9 @@ $skillSrc = Join-Path (Join-Path $argoDir 'skills') 'argo-init'
761
761
  $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
-
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')
764
+ $reconcileSkillSrc = Join-Path (Join-Path $argoDir 'skills') 'ea-human-reconcile'
765
+ Write-Host ' argo\skills\ea-human-reconcile -> $SkillsRoot\ea-human-reconcile (EA human draft reconcile skill)'
766
+ Copy-Tree -Source $reconcileSkillSrc -Destination (Join-Path $SkillsRoot 'ea-human-reconcile')
768
767
 
769
768
  $ruleSrc = Join-Path (Join-Path $argoDir 'rules') 'archgraph.instructions.md'
770
769
  $ruleDest = Join-Path $PromptsRoot 'archgraph.instructions.md'
@@ -780,8 +779,8 @@ Copy-Item -Force -Path $depsSrc -Destination $depsDest
780
779
  $cursorSkillDest = Join-Path $CursorSkillsRoot 'argo-init'
781
780
  Write-Host "[7/22] argo\skills\argo-init -> $cursorSkillDest (Cursor)"
782
781
  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')
782
+ Write-Host ' argo\skills\ea-human-reconcile -> $CursorSkillsRoot\ea-human-reconcile (Cursor)'
783
+ Copy-Tree -Source $reconcileSkillSrc -Destination (Join-Path $CursorSkillsRoot 'ea-human-reconcile')
785
784
 
786
785
  $mcpBridgeSrc = Join-Path $argoDir 'mcp-bridges'
787
786
  $mcpBridgeDest = Join-Path $CursorMcpBridgesRoot ''
@@ -791,8 +790,8 @@ Copy-Tree -Source $mcpBridgeSrc -Destination $mcpBridgeDest
791
790
  $openCodeSkillDest = Join-Path $OpenCodeSkillsRoot 'argo-init'
792
791
  Write-Host "[8/22] argo\skills\argo-init -> $openCodeSkillDest (OpenCode)"
793
792
  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')
793
+ Write-Host ' argo\skills\ea-human-reconcile -> $OpenCodeSkillsRoot\ea-human-reconcile (OpenCode)'
794
+ Copy-Tree -Source $reconcileSkillSrc -Destination (Join-Path $OpenCodeSkillsRoot 'ea-human-reconcile')
796
795
 
797
796
  Write-Host "[9/22] argo\rules\archgraph.instructions.md -> $OpenCodeAgentsPath (OpenCode global AGENTS.md)"
798
797
  Add-AgentsRule -AgentsPath $OpenCodeAgentsPath -RulePath $ruleSrc
@@ -830,9 +829,8 @@ if ($SkipDsh) {
830
829
 
831
830
  Write-Host "[16/22] argo\skills\argo-init -> $dshSkillDest (DeepSeek Harness skill)"
832
831
  Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $dshSkillDest
833
-
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')
832
+ Write-Host ' argo\skills\ea-human-reconcile -> $DshHome\skills\ea-human-reconcile (DeepSeek Harness skill)'
833
+ Copy-Tree -Source (Join-Path $argoDir 'skills\ea-human-reconcile') -Destination (Join-Path (Join-Path $DshHome 'skills') 'ea-human-reconcile')
836
834
 
837
835
  Write-Host "[17/22] argo\rules\<WakeupGuideline> -> $DshHome\plugins\dsh-argo-wakeup\index.js (DeepSeek Harness wakeup plugin)"
838
836
  $wakeupDshPath = New-DshWakeupPlugin -DshHome $DshHome -RuleText $ruleSrcContent
@@ -894,9 +892,8 @@ if ($SkipOpenClaw) {
894
892
 
895
893
  Write-Host "[21/22] argo\skills\argo-init -> $openClawSkillDest (OpenClaw managed skill, all agents)"
896
894
  Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $openClawSkillDest
897
-
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')
895
+ Write-Host ' argo\skills\ea-human-reconcile -> $OpenClawHome\skills\ea-human-reconcile (OpenClaw managed skill, all agents)'
896
+ Copy-Tree -Source (Join-Path $argoDir 'skills\ea-human-reconcile') -Destination (Join-Path (Join-Path $OpenClawHome 'skills') 'ea-human-reconcile')
900
897
 
901
898
  Write-Host ' OpenClaw injects AGENTS.md into Project Context on every session, so the wakeup'
902
899
  Write-Host ' gate (UNCONDITIONAL STARTUP GATE) is active on the next OpenClaw session; restart'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
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,7 @@
14
14
  "argo/plugins",
15
15
  "argo/mcp-bridges",
16
16
  "argo/skills/argo-init",
17
- "argo/skills/ea-human-draft",
17
+ "argo/skills/ea-human-reconcile",
18
18
  "argo/rules",
19
19
  "argo/package.json",
20
20
  "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`),手绘模型无锚时关系不参与。