archgraph-argo 0.22.0 → 0.22.2
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.
|
@@ -76,7 +76,7 @@ Never add what the graph already has. Reuse is the default; there is no reject m
|
|
|
76
76
|
<LosslessWrite>
|
|
77
77
|
Never lose stored content silently. A write must never reduce existing content unless it is lossless (merge/delta) or you explicitly acknowledge the reduction; omission never means deletion.
|
|
78
78
|
1. Structured fields merge: element testcases merge by `name`; view membership accepts a delta `{ add, remove }`; relationship attributes merge by `name`. Omitting an existing entry preserves it. Delete only with an explicit `op: "remove"` or an explicit delta remove.
|
|
79
|
-
2. Scalar text is guarded: `description`, `statement`, `document`, `name`, `view_name` are full-value fields. A
|
|
79
|
+
2. Scalar text is guarded: `description`, `statement`, `document`, `name`, `view_name` are full-value fields. A reworded line is not a loss; dropping prior lines, or genuinely losing structured tokens (commit hashes, ids, versions, dates, paths — separator/space changes do NOT count), is blocked unless you pass `acknowledgeLoss: true` (and `lossJustification` when the loss is major). Read the current value first and edit as a minimal diff.
|
|
80
80
|
3. Destructive removals (`removeElement` / `removeRelationship` / `removeView`) require `acknowledgeLoss: true` — per mutation, or once for the whole set via the top-level `acknowledgeLoss` on `applySystemArchitectureMutation`; the full removed object is appended to the NDJSON tombstone ledger `design/KG/SystemArchitecture.tombstones.ndjson` (rotated by size) for recovery.
|
|
81
81
|
4. Always read the `lossless` loss report in the response (preview and apply). It lists removed text lines / testcases / members / objects. If it is blocked, fix the mutation—do not retry blindly and do not disable the gate.
|
|
82
82
|
</LosslessWrite>
|
|
@@ -893,11 +893,42 @@ function fullProjection(graph, qeaPath, opts) {
|
|
|
893
893
|
// ---------------------------------------------------------------------------
|
|
894
894
|
// Export
|
|
895
895
|
// ---------------------------------------------------------------------------
|
|
896
|
+
// EA stores connector layout in two distinct t_diagramlinks columns, and they mean
|
|
897
|
+
// different things:
|
|
898
|
+
// - Path = the connector ROUTE: user-adjusted bend points as "x:y;x:y;" in diagram
|
|
899
|
+
// coordinates (empty when EA auto-routes a straight/orthogonal line).
|
|
900
|
+
// - Geometry = the non-route OVERRIDE tokens (SX/SY/EX/EY dock offsets, EDGE route
|
|
901
|
+
// style and label positions). It never contains bend points.
|
|
902
|
+
// Reading Geometry as if it were the line was the original defect: callers got the
|
|
903
|
+
// override string and could not rebuild the connector. We expose the route under `path`
|
|
904
|
+
// (+ parsed `points`), the route style as `edge`, and keep the override as `geometry`.
|
|
905
|
+
function parseRoutePoints(raw) {
|
|
906
|
+
const s = String(raw === null || raw === undefined ? '' : raw).trim();
|
|
907
|
+
if (s === '') { return []; }
|
|
908
|
+
const points = [];
|
|
909
|
+
for (const seg of s.split(';')) {
|
|
910
|
+
const t = seg.trim();
|
|
911
|
+
if (t === '') { continue; }
|
|
912
|
+
const parts = t.split(':');
|
|
913
|
+
if (parts.length < 2) { continue; }
|
|
914
|
+
const x = Number(parts[0]);
|
|
915
|
+
const y = Number(parts[1]);
|
|
916
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) { continue; }
|
|
917
|
+
points.push({ x, y });
|
|
918
|
+
}
|
|
919
|
+
return points;
|
|
920
|
+
}
|
|
921
|
+
function parseEdgeToken(geometry) {
|
|
922
|
+
const m = /(?:^|;)EDGE=(-?\d+)(?:;|$)/.exec(String(geometry === null || geometry === undefined ? '' : geometry));
|
|
923
|
+
return m ? Number(m[1]) : null;
|
|
924
|
+
}
|
|
925
|
+
|
|
896
926
|
// Read the EA diagram GEOMETRY for one KG view from a .qea model (read-only).
|
|
897
927
|
// The view maps to the diagram anchored by the deterministic ea_guid (diag:<viewId>)
|
|
898
928
|
// or the schema_view_id StyleEx token written by the sync. Returns:
|
|
899
929
|
// - element boxes = t_diagramobjects rects joined to t_object.Alias (schema id)
|
|
900
|
-
// - connector lines = t_diagramlinks rows joined to the connector's schema_id tag
|
|
930
|
+
// - connector lines = t_diagramlinks rows joined to the connector's schema_id tag,
|
|
931
|
+
// with the ROUTE (t_diagramlinks.Path) parsed into `points`
|
|
901
932
|
// Returns null when the view has no matching EA diagram. Never writes EA geometry.
|
|
902
933
|
function readViewDiagramGeometry(qeaPath, viewId) {
|
|
903
934
|
const v = String(viewId === null || viewId === undefined ? '' : viewId).trim();
|
|
@@ -920,13 +951,14 @@ function readViewDiagramGeometry(qeaPath, viewId) {
|
|
|
920
951
|
left: Number(r.left), top: Number(r.top), right: Number(r.right), bottom: Number(r.bottom),
|
|
921
952
|
}));
|
|
922
953
|
const relationships = db.prepare(
|
|
923
|
-
'SELECT t.VALUE AS id, dl.
|
|
954
|
+
'SELECT t.VALUE AS id, dl.Path AS path, dl.Geometry AS geometry ' +
|
|
924
955
|
'FROM t_diagramlinks dl JOIN t_connectortag t ON t.ElementID = dl.ConnectorID AND t.Property = ? ' +
|
|
925
956
|
'WHERE dl.DiagramID = ? ORDER BY dl.ConnectorID'
|
|
926
|
-
).all('schema_id', diagramId).map((r) =>
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
957
|
+
).all('schema_id', diagramId).map((r) => {
|
|
958
|
+
const path = String(r.path === null || r.path === undefined ? '' : r.path);
|
|
959
|
+
const geometry = String(r.geometry === null || r.geometry === undefined ? '' : r.geometry);
|
|
960
|
+
return { id: String(r.id), path, points: parseRoutePoints(path), edge: parseEdgeToken(geometry), geometry };
|
|
961
|
+
});
|
|
930
962
|
return { diagramId, elements, relationships };
|
|
931
963
|
} finally {
|
|
932
964
|
try { db.close(); } catch { /* ignore */ }
|
|
@@ -66,12 +66,38 @@ function normalizeSegments(text) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function tokenize(segment) {
|
|
69
|
-
const
|
|
69
|
+
const normalized = String(segment === undefined || segment === null ? '' : segment).normalize('NFKC');
|
|
70
|
+
const matches = normalized.match(TOKEN_RE);
|
|
70
71
|
return matches ? matches.map(token => token.toLowerCase()) : [];
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
// A "structured identity" token whose silent loss matters: commit hashes, ids,
|
|
75
|
+
// versions, dates, path-like strings. Bare separators ("/") and short plain
|
|
76
|
+
// numbers are NOT structured — treating them so made benign edits false-block.
|
|
73
77
|
function isStructuredToken(token) {
|
|
74
|
-
|
|
78
|
+
if (!token) return false;
|
|
79
|
+
if (/^[0-9a-f]{7,40}$/i.test(token)) return true; // commit hash / long hex
|
|
80
|
+
if (/[/\\]/.test(token) && /[a-z]/i.test(token)) return true; // path-like (has a letter)
|
|
81
|
+
if (/[a-z]/i.test(token) && /\d/.test(token) && /[-_]/.test(token)) return true; // id-like AT-rules-04
|
|
82
|
+
if (/^[a-z]*\d+(?:\.\d+)+$/i.test(token)) return true; // version-like stix2.1 / v2.1 / 1.2.3
|
|
83
|
+
if (/^\d{4}-\d{2}(?:-\d{2})?$/.test(token)) return true; // date-like 2026-09-15
|
|
84
|
+
if (/^\d{5,}$/.test(token)) return true; // long numeric id
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// NFKC + strip all whitespace + lowercase: tolerant haystack for the "is this
|
|
89
|
+
// structured token still present anywhere in the new text?" substring check.
|
|
90
|
+
// This drops false positives from separator joins (769465c:bc7f418) and spacing
|
|
91
|
+
// (STIX2.1 -> STIX 2.1) while still catching a genuinely removed token.
|
|
92
|
+
function normalizeForTokenSearch(value) {
|
|
93
|
+
return String(value === undefined || value === null ? '' : value).normalize('NFKC').replace(/\s+/g, '').toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Separator-insensitive form: also drop the punctuation that joins structured
|
|
97
|
+
// tokens, so "A/B" -> "A、B" or "v2.1" -> "v2 1" is still recognised as the same
|
|
98
|
+
// token (same content, punctuation changed) and not mistaken for a loss.
|
|
99
|
+
function comparableTokenForm(value) {
|
|
100
|
+
return normalizeForTokenSearch(value).replace(/[/\\:.\-_]/g, '');
|
|
75
101
|
}
|
|
76
102
|
|
|
77
103
|
// Sørensen–Dice coefficient over token sets (deterministic, no network).
|
|
@@ -97,7 +123,7 @@ function detectTextLoss(oldText, newText) {
|
|
|
97
123
|
}
|
|
98
124
|
const newSegments = normalizeSegments(newText);
|
|
99
125
|
const newNormalized = normalizeForCompare(newText);
|
|
100
|
-
const
|
|
126
|
+
const newHaystack = comparableTokenForm(newText);
|
|
101
127
|
const newSegmentTokens = newSegments.map(tokenize);
|
|
102
128
|
const removedSegments = [];
|
|
103
129
|
const modifiedSegments = [];
|
|
@@ -105,7 +131,7 @@ function detectTextLoss(oldText, newText) {
|
|
|
105
131
|
for (const segment of oldSegments) {
|
|
106
132
|
const segmentTokens = tokenize(segment);
|
|
107
133
|
for (const token of segmentTokens) {
|
|
108
|
-
if (isStructuredToken(token) && !
|
|
134
|
+
if (isStructuredToken(token) && !newHaystack.includes(comparableTokenForm(token))) structuredTokensRemoved.push(token);
|
|
109
135
|
}
|
|
110
136
|
if (newNormalized.includes(segment)) continue; // kept verbatim
|
|
111
137
|
const best = newSegmentTokens.reduce((max, tokens) => Math.max(max, diceCoefficient(segmentTokens, tokens)), 0);
|
|
@@ -226,7 +226,7 @@ const TOOLS = [
|
|
|
226
226
|
},
|
|
227
227
|
{
|
|
228
228
|
name: 'getArchitectureViewContext',
|
|
229
|
-
description: 'read-only query that resolves one view by view_id into its complete membership: the view object, every member element (from included_elements), every member relationship (from included_relationships), the parent element, and optionally child sub-views declared by member elements. Resolves ids into full canonical objects instead of returning raw id lists. Optional includeEaGeometry (default false) additionally returns the EA diagram geometry of the resolved view.',
|
|
229
|
+
description: 'read-only query that resolves one view by view_id into its complete membership: the view object, every member element (from included_elements), every member relationship (from included_relationships), the parent element, and optionally child sub-views declared by member elements. Resolves ids into full canonical objects instead of returning raw id lists. Optional includeEaGeometry (default false) additionally returns the EA diagram geometry of the resolved view: element boxes plus each connector ROUTE (t_diagramlinks.Path as `path` + parsed `points`, with the non-route Geometry override kept separately under `geometry`).',
|
|
230
230
|
inputSchema: viewContextInputSchema(),
|
|
231
231
|
},
|
|
232
232
|
{
|
|
@@ -475,7 +475,7 @@ function viewContextInputSchema() {
|
|
|
475
475
|
view_id: { type: 'string', description: 'The id of the view to resolve.' },
|
|
476
476
|
includeParentElement: { type: 'boolean', description: 'Default: true. Resolve the parent element referenced by the view.' },
|
|
477
477
|
includeChildViews: { type: 'boolean', description: 'Default: false. Include child views declared by member elements through subdiagram_views.' },
|
|
478
|
-
includeEaGeometry: { type: 'boolean', description: 'Default: false (opt-in). When true, additionally resolve the diagram GEOMETRY (element boxes + connector line
|
|
478
|
+
includeEaGeometry: { type: 'boolean', description: 'Default: false (opt-in). When true, additionally resolve the diagram GEOMETRY (element boxes + connector line routes) for this view from the workspace EA model (.qea) and return it under a `geometry` field aligned by schema id with the resolved members. Each geometry relationship carries: `path` (the EA route from t_diagramlinks.Path, "" when EA auto-routes), `points` (the parsed [{x,y}] waypoints), `edge` (the EDGE route-style token or null) and `geometry` (the raw SX/SY/EX/EY override string, which contains NO waypoints). By default the EA model is never touched and no `geometry` field is returned; a missing EA model/diagram yields geometry.present=false, never an error.' },
|
|
479
479
|
},
|
|
480
480
|
additionalProperties: false,
|
|
481
481
|
};
|
|
@@ -726,9 +726,11 @@ function buildIntentElementContext(context, args = {}) {
|
|
|
726
726
|
// model unless the caller explicitly sets includeEaGeometry=true. The workspace's
|
|
727
727
|
// EA model (.qea, the SQLite carrier this toolchain can read) may hold human-laid-out
|
|
728
728
|
// diagram geometry for a view — element boxes (t_diagramobjects rects) and connector
|
|
729
|
-
//
|
|
729
|
+
// routes (t_diagramlinks.Path). When present it is returned under `geometry`, aligned
|
|
730
730
|
// by schema id with the resolved members, so an image-capable LLM can redraw the view
|
|
731
|
-
// faithfully.
|
|
731
|
+
// faithfully. Each connector also carries the non-route t_diagramlinks.Geometry override
|
|
732
|
+
// string under a distinct `geometry` key, so the route is never confused with the
|
|
733
|
+
// SX/SY/EX/EY/EDGE tokens. Absent model/diagram → present:false (never an error).
|
|
732
734
|
const EA_GEOMETRY_MODEL_EXTENSIONS = new Set(['.qea']);
|
|
733
735
|
function findEaGeometryModelPath(workspaceRoot) {
|
|
734
736
|
try {
|
package/package.json
CHANGED