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 *reworded* line is not a loss; dropping prior lines, or losing structured tokens (ids, numbers, commit hashes, paths), 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.
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.Geometry AS path ' +
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
- id: String(r.id),
928
- path: String(r.path === null || r.path === undefined ? '' : r.path),
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 matches = String(segment === undefined || segment === null ? '' : segment).match(TOKEN_RE);
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
- return /[0-9]/.test(token) || /[/\\]/.test(token) || /^[0-9a-f]{7,40}$/i.test(token);
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 newTokenSet = new Set(newSegments.flatMap(tokenize));
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) && !newTokenSet.has(token)) structuredTokensRemoved.push(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 paths) for this view from the workspace EA model (.qea) and return it under a `geometry` field aligned by schema id with the resolved members. 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.' },
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
- // line paths (t_diagramlinks). When present it is returned under `geometry`, aligned
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. Absent model/diagram present:false (never an error).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.22.0",
3
+ "version": "0.22.2",
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": {