@restormel/graph-core 0.1.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.
@@ -0,0 +1,51 @@
1
+ # `@restormel/graph-core` — Restormel Graph **Contract v0** scope
2
+
3
+ This file **locks** what “v0” means for cross-platform work. In **restormel-keys**, `@restormel/graph-core` ships **only** Contract v0 plus the contracts-free helpers listed below. The SOPHIA app may still publish a broader `@restormel/graph-core` with legacy modules; consumers of this repo’s package should treat **this** tree as the MVP surface.
4
+
5
+ ## Contract v0 (frozen — platform review required to change)
6
+
7
+ **Single source file:** `src/viewModel.ts`
8
+
9
+ **Exported types (DTOs only):**
10
+
11
+ - `GraphNode`, `GraphEdge`, `GraphGhostNode`, `GraphGhostEdge`
12
+ - `GraphData`
13
+ - `GraphViewportCommand`, `GraphRendererProps`
14
+ - `GraphNodeSemanticStyle`, `GraphEdgeSemanticStyle`
15
+ - Supporting unions: `GraphPhase`, `GraphNodeKind`, `GraphArcKind`, `GraphRejectionReasonCode`, `GraphConflictStatus`, `GraphViewportCommandType`
16
+
17
+ **Invariants:**
18
+
19
+ - No imports from SOPHIA or `@restormel/contracts`.
20
+ - No runtime logic in `viewModel.ts` (types/interfaces only).
21
+
22
+ ## Explicitly **out of scope** for Contract v0 (future extraction targets)
23
+
24
+ These modules **depend on `@restormel/contracts`** (or reasoning-object) and are **not** part of the frozen DTO contract:
25
+
26
+ | Module | Role |
27
+ |--------|------|
28
+ | `compare.ts` | Reasoning snapshot compare |
29
+ | `lineage.ts` | Lineage report / markdown |
30
+ | `projection.ts` | Retrieval-like → graph snapshot |
31
+ | `diff.ts` | Graph diff |
32
+ | `evaluation.ts` | Reasoning graph evaluation |
33
+ | `summary.ts` | Snapshot summary |
34
+
35
+ Do **not** import these from a minimal Restormel Graph dashboard that only needs interactive rendering.
36
+
37
+ ## Co-located **render utilities** (zero `contracts` import today)
38
+
39
+ SOPHIA’s `GraphCanvas` currently imports these from the same package; they are **not** the frozen DTO file but are **allowed dependencies** for SVG rendering until moved to `ui-graph-svelte`:
40
+
41
+ | Module | Role |
42
+ |--------|------|
43
+ | `layout.ts` | `computeLayout` (orbital placement) |
44
+ | `trace.ts` | `getNodeTraceTags`, `getNodeTraceLabel`, `formatTraceTag` (label formatting) |
45
+ | `workspace.ts` | Generic filter/scope helpers (`WorkspaceGraphLike` — no contracts) |
46
+
47
+ Restormel may vendor or co-publish these alongside Contract v0 when splitting packages.
48
+
49
+ ## Package dependency note
50
+
51
+ **restormel-keys** `packages/graph-core` has **no** `@restormel/contracts` dependency. **Contract v0** (`viewModel.ts`) does not use that dependency.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Boon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # `@restormel/graph-core`
2
+
3
+ **Restormel Graph Contract v0** — portable types and contracts-free helpers for interactive graph rendering.
4
+
5
+ - **`viewModel.ts`** — frozen DTOs only (no runtime logic, no `@restormel/contracts`).
6
+ - **`layout.ts`** — `computeLayout` (orbital placement).
7
+ - **`trace.ts`** — trace tag / label helpers for canvas copy.
8
+ - **`workspace.ts`** — generic filter and focus helpers (`WorkspaceGraphLike`, `filterGraph`, …).
9
+
10
+ See **`GRAPH_CORE_V0_SCOPE.md`** for what is in v0 vs explicitly deferred (compare, lineage, projection, …).
11
+
12
+ ## Subpath exports
13
+
14
+ After `pnpm run build`, Node resolves:
15
+
16
+ - `@restormel/graph-core`
17
+ - `@restormel/graph-core/viewModel`
18
+ - `@restormel/graph-core/layout`
19
+ - `@restormel/graph-core/trace`
20
+ - `@restormel/graph-core/workspace`
21
+
22
+ ## Build
23
+
24
+ ```bash
25
+ pnpm --filter @restormel/graph-core run build
26
+ ```
27
+
28
+ ## Tests
29
+
30
+ ```bash
31
+ pnpm --filter @restormel/graph-core test
32
+ ```
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @restormel/graph-core — Restormel Graph MVP (Contract v0 + render helpers).
3
+ * Non-v0 modules (compare, lineage, …) are intentionally omitted; see GRAPH_CORE_V0_SCOPE.md.
4
+ */
5
+ export * from './layout.js';
6
+ export * from './trace.js';
7
+ export * from './viewModel.js';
8
+ export * from './workspace.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @restormel/graph-core — Restormel Graph MVP (Contract v0 + render helpers).
3
+ * Non-v0 modules (compare, lineage, …) are intentionally omitted; see GRAPH_CORE_V0_SCOPE.md.
4
+ */
5
+ export * from './layout.js';
6
+ export * from './trace.js';
7
+ export * from './viewModel.js';
8
+ export * from './workspace.js';
@@ -0,0 +1,7 @@
1
+ import type { GraphEdge, GraphNode } from './viewModel.js';
2
+ export interface LayoutPosition {
3
+ x: number;
4
+ y: number;
5
+ }
6
+ export declare function computeLayout(nodes: GraphNode[], edges: GraphEdge[], width: number, height: number): Map<string, LayoutPosition>;
7
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,EAAE,EAClB,KAAK,EAAE,SAAS,EAAE,EAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CA+C7B"}
package/dist/layout.js ADDED
@@ -0,0 +1,42 @@
1
+ export function computeLayout(nodes, edges, width, height) {
2
+ const positions = new Map();
3
+ const centerX = width / 2;
4
+ const centerY = height / 2;
5
+ const outerRadius = Math.min(width, height) * 0.35;
6
+ const innerRadius = outerRadius * 0.6;
7
+ const sources = nodes.filter((node) => node.type === 'source');
8
+ const claims = nodes.filter((node) => node.type === 'claim');
9
+ sources.forEach((node, index) => {
10
+ const angle = (index / sources.length) * Math.PI * 2 - Math.PI / 2;
11
+ positions.set(node.id, {
12
+ x: centerX + Math.cos(angle) * outerRadius,
13
+ y: centerY + Math.sin(angle) * outerRadius
14
+ });
15
+ });
16
+ const claimsBySource = new Map();
17
+ for (const claim of claims) {
18
+ const sourceEdge = edges.find((edge) => edge.to === claim.id && edge.type === 'contains');
19
+ const sourceId = sourceEdge?.from || 'orphan';
20
+ if (!claimsBySource.has(sourceId))
21
+ claimsBySource.set(sourceId, []);
22
+ claimsBySource.get(sourceId)?.push(claim);
23
+ }
24
+ for (const [sourceId, sourceClaims] of claimsBySource) {
25
+ const sourcePos = positions.get(sourceId);
26
+ const baseAngle = sourcePos
27
+ ? Math.atan2(sourcePos.y - centerY, sourcePos.x - centerX)
28
+ : 0;
29
+ sourceClaims.forEach((claim, index) => {
30
+ const offset = (index - sourceClaims.length / 2) * 0.3;
31
+ const angle = baseAngle + offset;
32
+ const radius = innerRadius +
33
+ (claim.id.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 20) -
34
+ 10;
35
+ positions.set(claim.id, {
36
+ x: centerX + Math.cos(angle) * radius,
37
+ y: centerY + Math.sin(angle) * radius
38
+ });
39
+ });
40
+ }
41
+ return positions;
42
+ }
@@ -0,0 +1,5 @@
1
+ import type { GraphNode } from './viewModel.js';
2
+ export declare function getNodeTraceTags(node: GraphNode): string[];
3
+ export declare function getNodeTraceLabel(node: GraphNode, maxTags?: number): string;
4
+ export declare function formatTraceTag(tag: string): string;
5
+ //# sourceMappingURL=trace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,EAAE,CAS1D;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,SAAI,GAAG,MAAM,CAEtE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAElD"}
package/dist/trace.js ADDED
@@ -0,0 +1,22 @@
1
+ export function getNodeTraceTags(node) {
2
+ const tags = [];
3
+ if (node.isSeed)
4
+ tags.push('seed');
5
+ else if (node.isTraversed)
6
+ tags.push('traversed');
7
+ if (node.pass_origin)
8
+ tags.push(node.pass_origin);
9
+ if (node.conflict_status && node.conflict_status !== 'none')
10
+ tags.push(node.conflict_status);
11
+ if (node.unresolved_tension_id)
12
+ tags.push('tension');
13
+ if (node.provenance_id)
14
+ tags.push('provenanced');
15
+ return tags;
16
+ }
17
+ export function getNodeTraceLabel(node, maxTags = 4) {
18
+ return getNodeTraceTags(node).slice(0, maxTags).join(' · ');
19
+ }
20
+ export function formatTraceTag(tag) {
21
+ return tag.replaceAll('_', ' ');
22
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * RESTORMEL GRAPH CONTRACT v0
3
+ * DO NOT MODIFY WITHOUT PLATFORM REVIEW
4
+ *
5
+ * Frozen DTOs for interactive graph rendering. This file is the only graph-core surface
6
+ * that defines cross-platform data shape for Restormel Graph MVP.
7
+ *
8
+ * Rules:
9
+ * - No imports from SOPHIA, `@restormel/contracts`, or any app package.
10
+ * - No runtime logic — types and interfaces only.
11
+ *
12
+ * Portable graph view-model for rendering (Restormel Graph).
13
+ * Hosts map domain snapshots into these shapes; renderers consume `GraphData` + optional semantics.
14
+ */
15
+ export type GraphPhase = 'retrieval' | 'analysis' | 'critique' | 'synthesis';
16
+ export type GraphNodeKind = 'source' | 'claim';
17
+ export type GraphConflictStatus = 'none' | 'contested' | 'unresolved' | 'resolved';
18
+ export type GraphArcKind = 'contains' | 'supports' | 'contradicts' | 'responds-to' | 'depends-on' | 'defines' | 'qualifies' | 'assumes' | 'resolves';
19
+ export type GraphRejectionReasonCode = 'seed_pool_pruned' | 'duplicate_traversal' | 'duplicate_relation' | 'missing_endpoint' | 'confidence_gate' | 'source_integrity_gate';
20
+ /** Vertex shown in the interactive graph canvas. */
21
+ export interface GraphNode {
22
+ id: string;
23
+ type: GraphNodeKind;
24
+ label: string;
25
+ phase?: GraphPhase;
26
+ domain?: string;
27
+ sourceTitle?: string;
28
+ traversalDepth?: number;
29
+ relevance?: number;
30
+ isSeed?: boolean;
31
+ isTraversed?: boolean;
32
+ confidenceBand?: 'high' | 'medium' | 'low';
33
+ depth_level?: number;
34
+ evidence_strength?: number;
35
+ novelty_score?: number;
36
+ derived_from?: string[];
37
+ pass_origin?: GraphPhase;
38
+ conflict_status?: GraphConflictStatus;
39
+ unresolved_tension_id?: string;
40
+ provenance_id?: string;
41
+ }
42
+ /** Directed typed edge between vertices. */
43
+ export interface GraphEdge {
44
+ from: string;
45
+ to: string;
46
+ type: GraphArcKind;
47
+ weight?: number;
48
+ phaseOrigin?: GraphPhase;
49
+ depth_level?: number;
50
+ evidence_strength?: number;
51
+ novelty_score?: number;
52
+ derived_from?: string[];
53
+ pass_origin?: GraphPhase;
54
+ conflict_status?: GraphConflictStatus;
55
+ unresolved_tension_id?: string;
56
+ provenance_id?: string;
57
+ relation_rationale?: string;
58
+ relation_confidence?: number;
59
+ evidence_count?: number;
60
+ evidence_sources?: string[];
61
+ }
62
+ /** Rejected / ghost vertex (optional overlay). */
63
+ export interface GraphGhostNode {
64
+ id: string;
65
+ label: string;
66
+ reasonCode: GraphRejectionReasonCode;
67
+ consideredIn?: 'seed_pool' | 'traversal' | 'relations';
68
+ sourceTitle?: string;
69
+ confidence?: number;
70
+ anchorNodeId?: string;
71
+ pass_origin?: GraphPhase;
72
+ }
73
+ /** Rejected / ghost edge (optional overlay). */
74
+ export interface GraphGhostEdge {
75
+ id: string;
76
+ from: string;
77
+ to: string;
78
+ type: GraphArcKind;
79
+ reasonCode: GraphRejectionReasonCode;
80
+ relation_confidence?: number;
81
+ rationale_source?: string;
82
+ pass_origin?: GraphPhase;
83
+ }
84
+ /** Bundle passed to a graph renderer (nodes + primary edges + optional ghost layer). */
85
+ export interface GraphData {
86
+ nodes: GraphNode[];
87
+ edges: GraphEdge[];
88
+ ghostNodes: GraphGhostNode[];
89
+ ghostEdges: GraphGhostEdge[];
90
+ }
91
+ export type GraphViewportCommandType = 'fit' | 'reset-layout';
92
+ export interface GraphViewportCommand {
93
+ type: GraphViewportCommandType;
94
+ nonce: number;
95
+ }
96
+ /**
97
+ * Semantic styling keyed by node id and by edge key `from:type:to`
98
+ * (see `graphCanvasEdgeKey` in host or duplicate in ui package).
99
+ */
100
+ export type GraphNodeSemanticStyle = {
101
+ kind: string;
102
+ shape: 'circle' | 'square' | 'diamond' | 'hexagon' | 'rounded-rect';
103
+ fill: string;
104
+ stroke: string;
105
+ glyph?: string;
106
+ radius?: number;
107
+ state?: 'default' | 'verified' | 'unresolved' | 'contradicted' | 'synthesis';
108
+ };
109
+ export type GraphEdgeSemanticStyle = {
110
+ kind: string;
111
+ stroke: string;
112
+ strokeWidth?: number;
113
+ dasharray?: string;
114
+ marker?: 'arrow-blue' | 'arrow-teal' | 'arrow-coral' | 'arrow-amber' | 'arrow-purple' | 'none';
115
+ state?: 'default' | 'verified' | 'unresolved' | 'contradicted' | 'synthesis';
116
+ };
117
+ /** Props contract for the Svelte graph canvas (`@restormel/ui-graph-svelte`). */
118
+ export interface GraphRendererProps {
119
+ nodes: GraphNode[];
120
+ edges: GraphEdge[];
121
+ ghostNodes?: GraphGhostNode[];
122
+ ghostEdges?: GraphGhostEdge[];
123
+ showGhostLayer?: boolean;
124
+ showInlineDetail?: boolean;
125
+ showStatusChip?: boolean;
126
+ showViewportControls?: boolean;
127
+ viewportCommand?: GraphViewportCommand | null;
128
+ nodeSemanticStyles?: Record<string, GraphNodeSemanticStyle>;
129
+ edgeSemanticStyles?: Record<string, GraphEdgeSemanticStyle>;
130
+ width?: number;
131
+ height?: number;
132
+ isFullscreen?: boolean;
133
+ onToggleFullscreen?: () => void;
134
+ pinnedNodeIds?: string[];
135
+ pathNodeIds?: string[];
136
+ pathEdges?: Array<{
137
+ from: string;
138
+ to: string;
139
+ }>;
140
+ focusNodeIds?: string[];
141
+ focusEdgeIds?: string[];
142
+ dimOutOfScope?: boolean;
143
+ selectedNodeId?: string | null;
144
+ onSelectedNodeChange?: (nodeId: string | null) => void;
145
+ onNodeSelect?: (nodeId: string) => void;
146
+ onJumpToReferences?: (nodeId: string) => void;
147
+ }
148
+ //# sourceMappingURL=viewModel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewModel.d.ts","sourceRoot":"","sources":["../src/viewModel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,CAAC;AAE7E,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE/C,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,UAAU,CAAC;AAEnF,MAAM,MAAM,YAAY,GACpB,UAAU,GACV,UAAU,GACV,aAAa,GACb,aAAa,GACb,YAAY,GACZ,SAAS,GACT,WAAW,GACX,SAAS,GACT,UAAU,CAAC;AAEf,MAAM,MAAM,wBAAwB,GAChC,kBAAkB,GAClB,qBAAqB,GACrB,oBAAoB,GACpB,kBAAkB,GAClB,iBAAiB,GACjB,uBAAuB,CAAC;AAE5B,oDAAoD;AACpD,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,4CAA4C;AAC5C,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,kDAAkD;AAClD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,wBAAwB,CAAC;IACrC,YAAY,CAAC,EAAE,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,UAAU,CAAC;CAC1B;AAED,gDAAgD;AAChD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,YAAY,CAAC;IACnB,UAAU,EAAE,wBAAwB,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,UAAU,CAAC;CAC1B;AAED,wFAAwF;AACxF,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,cAAc,CAAC;AAE9D,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,CAAC;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,YAAY,GAAG,cAAc,GAAG,WAAW,CAAC;CAC9E,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,aAAa,GAAG,aAAa,GAAG,cAAc,GAAG,MAAM,CAAC;IAC/F,KAAK,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,YAAY,GAAG,cAAc,GAAG,WAAW,CAAC;CAC9E,CAAC;AAEF,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,eAAe,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC9C,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;IAC5D,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,oBAAoB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACvD,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * RESTORMEL GRAPH CONTRACT v0
3
+ * DO NOT MODIFY WITHOUT PLATFORM REVIEW
4
+ *
5
+ * Frozen DTOs for interactive graph rendering. This file is the only graph-core surface
6
+ * that defines cross-platform data shape for Restormel Graph MVP.
7
+ *
8
+ * Rules:
9
+ * - No imports from SOPHIA, `@restormel/contracts`, or any app package.
10
+ * - No runtime logic — types and interfaces only.
11
+ *
12
+ * Portable graph view-model for rendering (Restormel Graph).
13
+ * Hosts map domain snapshots into these shapes; renderers consume `GraphData` + optional semantics.
14
+ */
15
+ export {};
@@ -0,0 +1,88 @@
1
+ export interface WorkspaceGraphLikeNode<NodeKind extends string = string, Phase extends string = string> {
2
+ id: string;
3
+ kind: NodeKind;
4
+ phase?: Phase;
5
+ searchText: string;
6
+ }
7
+ export interface WorkspaceGraphLikeEdge<EdgeKind extends string = string, Phase extends string = string> {
8
+ id: string;
9
+ from: string;
10
+ to: string;
11
+ kind: EdgeKind;
12
+ phase?: Phase;
13
+ }
14
+ export interface WorkspaceGhostLikeNode {
15
+ anchorNodeId?: string;
16
+ }
17
+ export interface WorkspaceGhostLikeEdge {
18
+ from: string;
19
+ to: string;
20
+ }
21
+ export interface WorkspaceGraphLike<Node extends WorkspaceGraphLikeNode = WorkspaceGraphLikeNode, Edge extends WorkspaceGraphLikeEdge = WorkspaceGraphLikeEdge, GhostNode extends WorkspaceGhostLikeNode = WorkspaceGhostLikeNode, GhostEdge extends WorkspaceGhostLikeEdge = WorkspaceGhostLikeEdge> {
22
+ nodes: Node[];
23
+ edges: Edge[];
24
+ ghostNodes: GhostNode[];
25
+ ghostEdges: GhostEdge[];
26
+ }
27
+ export interface WorkspaceGraphFilters<NodeKind extends string, EdgeKind extends string, Phase extends string> {
28
+ search: string;
29
+ phase: 'all' | Phase;
30
+ density: 'comfortable' | 'dense';
31
+ nodeKinds: Set<NodeKind>;
32
+ edgeKinds: Set<EdgeKind>;
33
+ showGhosts: boolean;
34
+ }
35
+ export interface WorkspaceFocusScope {
36
+ nodeIds: string[];
37
+ edgeIds: string[];
38
+ }
39
+ export interface WorkspacePathFocus {
40
+ nodeIds: string[];
41
+ edges: Array<{
42
+ from: string;
43
+ to: string;
44
+ }>;
45
+ }
46
+ export interface WorkspaceScopeSummary {
47
+ active: boolean;
48
+ visibleNodes: number;
49
+ totalNodes: number;
50
+ visibleEdges: number;
51
+ totalEdges: number;
52
+ }
53
+ export declare function reconcileKindSelection<T extends string>(current: Set<T>, available: Set<T>): Set<T>;
54
+ export declare function toggleKindSelection<T extends string>(current: Set<T>, value: T, fallback: Set<T>): Set<T>;
55
+ export declare function collectNodeKinds<Node extends {
56
+ kind: string;
57
+ }>(nodes: Node[]): Set<Node['kind']>;
58
+ export declare function collectEdgeKinds<Edge extends {
59
+ kind: string;
60
+ }>(edges: Edge[]): Set<Edge['kind']>;
61
+ export declare function filterGraph<Node extends WorkspaceGraphLikeNode, Edge extends WorkspaceGraphLikeEdge, GhostNode extends WorkspaceGhostLikeNode, GhostEdge extends WorkspaceGhostLikeEdge, Graph extends WorkspaceGraphLike<Node, Edge, GhostNode, GhostEdge>>(graph: Graph, filters: WorkspaceGraphFilters<Node['kind'], Edge['kind'], NonNullable<Node['phase']>>, selectedNodeId: string | null, options?: {
62
+ comfortableHiddenEdgeKinds?: Edge['kind'][];
63
+ }): Graph;
64
+ export declare function collectNeighborhoodScope<Edge extends WorkspaceGraphLikeEdge>(graph: Pick<WorkspaceGraphLike<WorkspaceGraphLikeNode, Edge>, 'edges'>, centerNodeId: string | null, maxHops: number): WorkspaceFocusScope;
65
+ export declare function isolateGraphToScope<Node extends WorkspaceGraphLikeNode, Edge extends WorkspaceGraphLikeEdge, GhostNode extends WorkspaceGhostLikeNode, GhostEdge extends WorkspaceGhostLikeEdge, Graph extends WorkspaceGraphLike<Node, Edge, GhostNode, GhostEdge>>(graph: Graph, scope: WorkspaceFocusScope): Graph;
66
+ export declare function buildSelectionPathFocus<Node extends WorkspaceGraphLikeNode, Edge extends WorkspaceGraphLikeEdge>(graph: Pick<WorkspaceGraphLike<Node, Edge>, 'edges'>, selectedNodeId: string | null, enabled: boolean, options: {
67
+ highlightEdgeKinds: Edge['kind'][];
68
+ }): WorkspacePathFocus;
69
+ export declare function mergePathFocuses(...paths: WorkspacePathFocus[]): WorkspacePathFocus;
70
+ export declare function buildFocusSummary(params: {
71
+ focusMode: string;
72
+ scopeNodeIds: string[];
73
+ scopeEdgeIds: string[];
74
+ totalNodes: number;
75
+ totalEdges: number;
76
+ }): WorkspaceScopeSummary;
77
+ export declare function buildReadabilityWarnings<Node extends {
78
+ kind: string;
79
+ }, Edge extends {
80
+ kind: string;
81
+ }>(graph: Pick<WorkspaceGraphLike<Node & WorkspaceGraphLikeNode, Edge & WorkspaceGraphLikeEdge>, 'nodes' | 'edges'>, options?: {
82
+ sourceNodeKinds?: Node['kind'][];
83
+ lowSourceThreshold?: number;
84
+ crowdedNodeThreshold?: number;
85
+ denseEdgeFloor?: number;
86
+ structuralEdgeKinds?: Edge['kind'][];
87
+ }): string[];
88
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,202 @@
1
+ function setsEqual(left, right) {
2
+ if (left.size !== right.size)
3
+ return false;
4
+ for (const value of left) {
5
+ if (!right.has(value))
6
+ return false;
7
+ }
8
+ return true;
9
+ }
10
+ export function reconcileKindSelection(current, available) {
11
+ if (available.size === 0) {
12
+ return current.size === 0 ? current : new Set();
13
+ }
14
+ if (current.size === 0) {
15
+ return setsEqual(current, available) ? current : new Set(available);
16
+ }
17
+ const next = new Set();
18
+ for (const value of current) {
19
+ if (available.has(value))
20
+ next.add(value);
21
+ }
22
+ if (next.size > 0) {
23
+ return setsEqual(current, next) ? current : next;
24
+ }
25
+ return setsEqual(current, available) ? current : new Set(available);
26
+ }
27
+ export function toggleKindSelection(current, value, fallback) {
28
+ const next = new Set(current);
29
+ if (next.has(value))
30
+ next.delete(value);
31
+ else
32
+ next.add(value);
33
+ return next.size > 0 ? next : new Set(fallback);
34
+ }
35
+ export function collectNodeKinds(nodes) {
36
+ return new Set(nodes.map((node) => node.kind));
37
+ }
38
+ export function collectEdgeKinds(edges) {
39
+ return new Set(edges.map((edge) => edge.kind));
40
+ }
41
+ export function filterGraph(graph, filters, selectedNodeId, options) {
42
+ const search = filters.search.trim().toLowerCase();
43
+ const nodeMatches = (node) => {
44
+ if (filters.phase !== 'all' && node.phase && node.phase !== filters.phase)
45
+ return false;
46
+ if (!filters.nodeKinds.has(node.kind) && node.id !== selectedNodeId)
47
+ return false;
48
+ if (!search)
49
+ return true;
50
+ return node.searchText.toLowerCase().includes(search);
51
+ };
52
+ const matchedNodeIds = new Set(graph.nodes.filter(nodeMatches).map((node) => node.id));
53
+ if (selectedNodeId)
54
+ matchedNodeIds.add(selectedNodeId);
55
+ let edges = graph.edges.filter((edge) => {
56
+ if (!filters.edgeKinds.has(edge.kind))
57
+ return false;
58
+ if (filters.phase !== 'all' && edge.phase && edge.phase !== filters.phase)
59
+ return false;
60
+ return matchedNodeIds.has(edge.from) && matchedNodeIds.has(edge.to);
61
+ });
62
+ if (filters.density === 'comfortable') {
63
+ const hiddenKinds = new Set(options?.comfortableHiddenEdgeKinds ?? []);
64
+ const logicalEdges = edges.filter((edge) => !hiddenKinds.has(edge.kind));
65
+ if (logicalEdges.length > 0)
66
+ edges = logicalEdges;
67
+ }
68
+ const visibleNodeIds = new Set();
69
+ for (const edge of edges) {
70
+ visibleNodeIds.add(edge.from);
71
+ visibleNodeIds.add(edge.to);
72
+ }
73
+ if (selectedNodeId)
74
+ visibleNodeIds.add(selectedNodeId);
75
+ const nodes = graph.nodes.filter((node) => {
76
+ if (visibleNodeIds.size === 0)
77
+ return nodeMatches(node);
78
+ return visibleNodeIds.has(node.id) && nodeMatches(node);
79
+ });
80
+ const nodeIds = new Set(nodes.map((node) => node.id));
81
+ return {
82
+ ...graph,
83
+ nodes,
84
+ edges,
85
+ ghostNodes: filters.showGhosts
86
+ ? graph.ghostNodes.filter((node) => !node.anchorNodeId || nodeIds.has(node.anchorNodeId))
87
+ : [],
88
+ ghostEdges: filters.showGhosts
89
+ ? graph.ghostEdges.filter((edge) => nodeIds.has(edge.from) && nodeIds.has(edge.to))
90
+ : []
91
+ };
92
+ }
93
+ export function collectNeighborhoodScope(graph, centerNodeId, maxHops) {
94
+ if (!centerNodeId || maxHops < 0) {
95
+ return { nodeIds: [], edgeIds: [] };
96
+ }
97
+ const visited = new Set([centerNodeId]);
98
+ const frontier = new Set([centerNodeId]);
99
+ const edgeIds = new Set();
100
+ for (let hop = 0; hop < maxHops; hop += 1) {
101
+ const nextFrontier = new Set();
102
+ for (const edge of graph.edges) {
103
+ const fromInFrontier = frontier.has(edge.from);
104
+ const toInFrontier = frontier.has(edge.to);
105
+ if (!fromInFrontier && !toInFrontier)
106
+ continue;
107
+ edgeIds.add(edge.id);
108
+ if (fromInFrontier && !visited.has(edge.to)) {
109
+ visited.add(edge.to);
110
+ nextFrontier.add(edge.to);
111
+ }
112
+ if (toInFrontier && !visited.has(edge.from)) {
113
+ visited.add(edge.from);
114
+ nextFrontier.add(edge.from);
115
+ }
116
+ }
117
+ if (nextFrontier.size === 0)
118
+ break;
119
+ frontier.clear();
120
+ for (const nodeId of nextFrontier)
121
+ frontier.add(nodeId);
122
+ }
123
+ return {
124
+ nodeIds: [...visited],
125
+ edgeIds: [...edgeIds]
126
+ };
127
+ }
128
+ export function isolateGraphToScope(graph, scope) {
129
+ if (scope.nodeIds.length === 0)
130
+ return graph;
131
+ const nodeIdSet = new Set(scope.nodeIds);
132
+ const edgeIdSet = new Set(scope.edgeIds);
133
+ return {
134
+ ...graph,
135
+ nodes: graph.nodes.filter((node) => nodeIdSet.has(node.id)),
136
+ edges: graph.edges.filter((edge) => edgeIdSet.has(edge.id)),
137
+ ghostNodes: graph.ghostNodes.filter((node) => !node.anchorNodeId || nodeIdSet.has(node.anchorNodeId)),
138
+ ghostEdges: graph.ghostEdges.filter((edge) => nodeIdSet.has(edge.from) && nodeIdSet.has(edge.to))
139
+ };
140
+ }
141
+ export function buildSelectionPathFocus(graph, selectedNodeId, enabled, options) {
142
+ if (!enabled || !selectedNodeId) {
143
+ return { nodeIds: [], edges: [] };
144
+ }
145
+ const highlightKinds = new Set(options.highlightEdgeKinds);
146
+ const edges = graph.edges
147
+ .filter((edge) => (edge.from === selectedNodeId || edge.to === selectedNodeId) &&
148
+ highlightKinds.has(edge.kind))
149
+ .map((edge) => ({ from: edge.from, to: edge.to }));
150
+ const nodeIds = new Set([selectedNodeId]);
151
+ for (const edge of edges) {
152
+ nodeIds.add(edge.from);
153
+ nodeIds.add(edge.to);
154
+ }
155
+ return { nodeIds: [...nodeIds], edges };
156
+ }
157
+ export function mergePathFocuses(...paths) {
158
+ const nodeIds = new Set();
159
+ const edgeMap = new Map();
160
+ for (const path of paths) {
161
+ for (const nodeId of path.nodeIds)
162
+ nodeIds.add(nodeId);
163
+ for (const edge of path.edges) {
164
+ edgeMap.set(`${edge.from}:${edge.to}`, edge);
165
+ }
166
+ }
167
+ return {
168
+ nodeIds: [...nodeIds],
169
+ edges: [...edgeMap.values()]
170
+ };
171
+ }
172
+ export function buildFocusSummary(params) {
173
+ return {
174
+ active: params.focusMode !== 'global' && params.scopeNodeIds.length > 0,
175
+ visibleNodes: params.scopeNodeIds.length,
176
+ totalNodes: params.totalNodes,
177
+ visibleEdges: params.scopeEdgeIds.length,
178
+ totalEdges: params.totalEdges
179
+ };
180
+ }
181
+ export function buildReadabilityWarnings(graph, options) {
182
+ const warnings = [];
183
+ const nodeCount = graph.nodes.length;
184
+ const edgeCount = graph.edges.length;
185
+ const sourceNodeKinds = new Set(options?.sourceNodeKinds ?? ['source']);
186
+ const structuralEdgeKinds = new Set(options?.structuralEdgeKinds ?? ['contains', 'retrieved-from']);
187
+ const sourceCount = graph.nodes.filter((node) => sourceNodeKinds.has(node.kind)).length;
188
+ const crossSourceEdgeCount = graph.edges.filter((edge) => !structuralEdgeKinds.has(edge.kind)).length;
189
+ if (nodeCount >= (options?.crowdedNodeThreshold ?? 36)) {
190
+ warnings.push('Orbital v1 starts to lose global readability once roughly 36+ visible nodes compete for label space.');
191
+ }
192
+ if (edgeCount >= Math.max(nodeCount * 2, options?.denseEdgeFloor ?? 24)) {
193
+ warnings.push('Dense relation overlays create heavy edge crossings; local focus or isolation is recommended.');
194
+ }
195
+ if (sourceCount <= (options?.lowSourceThreshold ?? 1) && nodeCount >= 18) {
196
+ warnings.push('When many claims share little source structure, the current source-centric layout collapses toward the center.');
197
+ }
198
+ if (crossSourceEdgeCount >= 18) {
199
+ warnings.push('Cross-source reasoning links are only weakly clustered today, so support and contradiction paths become harder to trace.');
200
+ }
201
+ return warnings;
202
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@restormel/graph-core",
3
+ "version": "0.1.0",
4
+ "description": "Restormel Graph Contract v0 DTOs plus contracts-free layout, trace, and workspace helpers.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/Allotment-Technology-Ltd/restormel-keys.git",
9
+ "directory": "packages/graph-core"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./layout": {
20
+ "types": "./dist/layout.d.ts",
21
+ "import": "./dist/layout.js"
22
+ },
23
+ "./trace": {
24
+ "types": "./dist/trace.d.ts",
25
+ "import": "./dist/trace.js"
26
+ },
27
+ "./viewModel": {
28
+ "types": "./dist/viewModel.d.ts",
29
+ "import": "./dist/viewModel.js"
30
+ },
31
+ "./workspace": {
32
+ "types": "./dist/workspace.d.ts",
33
+ "import": "./dist/workspace.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "GRAPH_CORE_V0_SCOPE.md"
39
+ ],
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "^5.7.0",
45
+ "vitest": "^4.1.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -b tsconfig.json",
52
+ "typecheck": "tsc -b tsconfig.json",
53
+ "test": "vitest run"
54
+ }
55
+ }