@sigloch/graph-api-core 0.4.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.
- package/LICENSE +21 -0
- package/dist/audit.d.ts +103 -0
- package/dist/audit.js +145 -0
- package/dist/browser.d.ts +21 -0
- package/dist/browser.js +17 -0
- package/dist/edge-ops.d.ts +42 -0
- package/dist/edge-ops.js +66 -0
- package/dist/factory.d.ts +26 -0
- package/dist/factory.js +23 -0
- package/dist/find-root.d.ts +34 -0
- package/dist/find-root.js +10 -0
- package/dist/format-e-codec.d.ts +23 -0
- package/dist/format-e-codec.js +296 -0
- package/dist/graph-service.d.ts +37 -0
- package/dist/graph-service.js +597 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +27 -0
- package/dist/memory-adapter.d.ts +28 -0
- package/dist/memory-adapter.js +72 -0
- package/dist/rule-engine.d.ts +33 -0
- package/dist/rule-engine.js +17 -0
- package/dist/schemas.d.ts +97 -0
- package/dist/schemas.js +58 -0
- package/dist/se-descriptor.d.ts +24 -0
- package/dist/se-descriptor.js +84 -0
- package/dist/storage-adapter.d.ts +32 -0
- package/dist/storage-adapter.js +1 -0
- package/dist/test-fixtures.d.ts +42 -0
- package/dist/test-fixtures.js +151 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/storage-contract-tests.d.ts +2 -0
- package/dist/testing/storage-contract-tests.js +113 -0
- package/dist/transport-adapter.d.ts +13 -0
- package/dist/transport-adapter.js +1 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.js +39 -0
- package/package.json +45 -0
- package/test-fixtures/cr-007-features.format-e.md +22 -0
- package/test-fixtures/empty.format-e.md +6 -0
- package/test-fixtures/rasentraktor.format-e.md +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sigloch Consulting
|
|
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/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { FormatEDiff } from './types.js';
|
|
2
|
+
import type { RuleViolation } from './rule-engine.js';
|
|
3
|
+
import type { MutateCommand } from '@sigloch/contracts/harness';
|
|
4
|
+
export interface AuditEntry {
|
|
5
|
+
id: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
consumerId: string;
|
|
8
|
+
consumerType: 'frontend' | 'agent' | 'automation';
|
|
9
|
+
operation: 'mutate' | 'batch' | 'validate' | 'export';
|
|
10
|
+
diff?: FormatEDiff;
|
|
11
|
+
/**
|
|
12
|
+
* The mutate commands that produced this entry (CR-207, formerly the graphcode-
|
|
13
|
+
* local `GraphcodeAuditEntry.commands`). Optional: a `validate`/`export` entry
|
|
14
|
+
* carries none, and pre-CR-207 records predate the field. Replay-merge reads
|
|
15
|
+
* these in log order; without them replay is impossible.
|
|
16
|
+
*/
|
|
17
|
+
commands?: MutateCommand[];
|
|
18
|
+
result: 'applied' | 'rejected' | 'partial';
|
|
19
|
+
violations?: RuleViolation[];
|
|
20
|
+
graphVersion: number;
|
|
21
|
+
}
|
|
22
|
+
export interface AuditLog {
|
|
23
|
+
record(entry: AuditEntry): Promise<void>;
|
|
24
|
+
query(filter: {
|
|
25
|
+
consumerId?: string;
|
|
26
|
+
since?: string;
|
|
27
|
+
limit?: number;
|
|
28
|
+
}): Promise<AuditEntry[]>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The durable operations-log surface: append/query (AuditLog) plus the event-
|
|
32
|
+
* sourcing primitives replay-merge and OCC build on — version continuity across
|
|
33
|
+
* sessions and compactions, and compaction itself. `compact()` writes a CHECKPOINT
|
|
34
|
+
* line that anchors the version so it is identical before and after; replay of the
|
|
35
|
+
* command batches after a fork point is a consumer concern (it needs the gate), so
|
|
36
|
+
* it is not on this interface — the log only guarantees the batches are durable and
|
|
37
|
+
* ordered.
|
|
38
|
+
*/
|
|
39
|
+
export interface OperationsLog extends AuditLog {
|
|
40
|
+
/** The compaction anchor: checkpoint version, or 0 on a never-compacted log. */
|
|
41
|
+
baseVersion(): number;
|
|
42
|
+
/** Highest known graph version — checkpoint anchor or max recorded entry version. */
|
|
43
|
+
latestVersion(): number;
|
|
44
|
+
/** Archive the active log and restart it anchored by a checkpoint line (version-safe). */
|
|
45
|
+
compact(reason?: string): {
|
|
46
|
+
archivedTo: string | null;
|
|
47
|
+
checkpointVersion: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export declare class InMemoryAuditLog implements AuditLog {
|
|
51
|
+
private entries;
|
|
52
|
+
record(entry: AuditEntry): Promise<void>;
|
|
53
|
+
query(filter: {
|
|
54
|
+
consumerId?: string;
|
|
55
|
+
since?: string;
|
|
56
|
+
limit?: number;
|
|
57
|
+
}): Promise<AuditEntry[]>;
|
|
58
|
+
}
|
|
59
|
+
/** Auto-compaction size threshold (bytes). */
|
|
60
|
+
export declare const DEFAULT_COMPACT_BYTES: number;
|
|
61
|
+
/** Log filename inside the store dir — the log lives BESIDE the store it describes. */
|
|
62
|
+
export declare const AUDIT_BASENAME = "audit.jsonl";
|
|
63
|
+
/** Standard-layout location relative to the repo root (store dir = `.graphcode`). */
|
|
64
|
+
export declare const AUDIT_FILE: string;
|
|
65
|
+
export declare class FileOperationsLog implements OperationsLog {
|
|
66
|
+
private readonly path;
|
|
67
|
+
private readonly maxBytes;
|
|
68
|
+
/**
|
|
69
|
+
* `storeDir` = the directory of the store this log describes (same anchoring rule
|
|
70
|
+
* as the ownership lock: per store, never per repo).
|
|
71
|
+
*/
|
|
72
|
+
constructor(storeDir: string, opts?: {
|
|
73
|
+
maxBytes?: number;
|
|
74
|
+
});
|
|
75
|
+
record(entry: AuditEntry): Promise<void>;
|
|
76
|
+
/** Mirrors InMemoryAuditLog semantics: `since` inclusive, `limit` = last N. */
|
|
77
|
+
query(filter: {
|
|
78
|
+
consumerId?: string;
|
|
79
|
+
since?: string;
|
|
80
|
+
limit?: number;
|
|
81
|
+
}): Promise<AuditEntry[]>;
|
|
82
|
+
/** The compaction anchor: checkpoint version, or 0 on a never-compacted log. */
|
|
83
|
+
baseVersion(): number;
|
|
84
|
+
/**
|
|
85
|
+
* Highest known graph version — checkpoint anchor or the max recorded entry
|
|
86
|
+
* version, whichever is higher. Seeds the tool-layer counter so versions run on
|
|
87
|
+
* across sessions AND across compactions.
|
|
88
|
+
*/
|
|
89
|
+
latestVersion(): number;
|
|
90
|
+
/**
|
|
91
|
+
* Archive the active log and start a fresh one anchored by a checkpoint line.
|
|
92
|
+
* Version-safe by construction: `latestVersion()` is identical before and after.
|
|
93
|
+
* No-op on a missing/empty log.
|
|
94
|
+
*/
|
|
95
|
+
compact(reason?: string): {
|
|
96
|
+
archivedTo: string | null;
|
|
97
|
+
checkpointVersion: number;
|
|
98
|
+
};
|
|
99
|
+
private maybeCompact;
|
|
100
|
+
/** Read all parseable lines; skip a torn tail (crash mid-append) with a warning. */
|
|
101
|
+
private readLines;
|
|
102
|
+
private readEntries;
|
|
103
|
+
}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operations log for tracking all graph mutations (CR-207).
|
|
3
|
+
*
|
|
4
|
+
* The operations log is a SUBSTRATE capability, not a graphcode-local one: it is
|
|
5
|
+
* the single source for OCC deltas, replay-merge, the learning feed, doc export,
|
|
6
|
+
* and rollback across the AiSE family (bok/.../02-operations-log-and-seams.md).
|
|
7
|
+
* It therefore lives here in graph-api-core with exactly one durable File
|
|
8
|
+
* implementation — the former graphcode `FileAuditLog` fork is lifted here (CR-207),
|
|
9
|
+
* no parallel implementation per consumer.
|
|
10
|
+
*
|
|
11
|
+
* `AuditLog` (record/query) is the minimal append/read surface — an in-memory
|
|
12
|
+
* variant is enough for tests. `OperationsLog` adds the durability surface the
|
|
13
|
+
* event-sourcing ladder needs: version continuity (`latestVersion`/`baseVersion`)
|
|
14
|
+
* and compaction (`compact`). `FileOperationsLog` persists an append-only JSONL
|
|
15
|
+
* stream, one file per store, checkpoint-anchored so the monotonic graph version
|
|
16
|
+
* survives every compaction, and torn-tail tolerant so a crash mid-append is a
|
|
17
|
+
* skipped line, never a read failure.
|
|
18
|
+
*/
|
|
19
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
export class InMemoryAuditLog {
|
|
22
|
+
entries = [];
|
|
23
|
+
async record(entry) {
|
|
24
|
+
this.entries.push(entry);
|
|
25
|
+
}
|
|
26
|
+
async query(filter) {
|
|
27
|
+
let result = this.entries;
|
|
28
|
+
if (filter.consumerId) {
|
|
29
|
+
result = result.filter(e => e.consumerId === filter.consumerId);
|
|
30
|
+
}
|
|
31
|
+
if (filter.since) {
|
|
32
|
+
result = result.filter(e => e.timestamp >= filter.since);
|
|
33
|
+
}
|
|
34
|
+
if (filter.limit) {
|
|
35
|
+
result = result.slice(-filter.limit);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const isCheckpoint = (l) => l.checkpoint === true;
|
|
41
|
+
/** Auto-compaction size threshold (bytes). */
|
|
42
|
+
export const DEFAULT_COMPACT_BYTES = 10 * 1024 * 1024;
|
|
43
|
+
/** Log filename inside the store dir — the log lives BESIDE the store it describes. */
|
|
44
|
+
export const AUDIT_BASENAME = 'audit.jsonl';
|
|
45
|
+
/** Standard-layout location relative to the repo root (store dir = `.graphcode`). */
|
|
46
|
+
export const AUDIT_FILE = join('.graphcode', AUDIT_BASENAME);
|
|
47
|
+
export class FileOperationsLog {
|
|
48
|
+
path;
|
|
49
|
+
maxBytes;
|
|
50
|
+
/**
|
|
51
|
+
* `storeDir` = the directory of the store this log describes (same anchoring rule
|
|
52
|
+
* as the ownership lock: per store, never per repo).
|
|
53
|
+
*/
|
|
54
|
+
constructor(storeDir, opts) {
|
|
55
|
+
this.path = join(storeDir, AUDIT_BASENAME);
|
|
56
|
+
this.maxBytes = opts?.maxBytes ?? DEFAULT_COMPACT_BYTES;
|
|
57
|
+
// Session start is the safe compaction moment: no reader is mid-delta.
|
|
58
|
+
this.maybeCompact();
|
|
59
|
+
}
|
|
60
|
+
async record(entry) {
|
|
61
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
62
|
+
appendFileSync(this.path, JSON.stringify(entry) + '\n', 'utf8');
|
|
63
|
+
}
|
|
64
|
+
/** Mirrors InMemoryAuditLog semantics: `since` inclusive, `limit` = last N. */
|
|
65
|
+
async query(filter) {
|
|
66
|
+
let result = this.readEntries();
|
|
67
|
+
if (filter.consumerId)
|
|
68
|
+
result = result.filter(e => e.consumerId === filter.consumerId);
|
|
69
|
+
if (filter.since)
|
|
70
|
+
result = result.filter(e => e.timestamp >= filter.since);
|
|
71
|
+
if (filter.limit)
|
|
72
|
+
result = result.slice(-filter.limit);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/** The compaction anchor: checkpoint version, or 0 on a never-compacted log. */
|
|
76
|
+
baseVersion() {
|
|
77
|
+
const cp = this.readLines().find(isCheckpoint);
|
|
78
|
+
return cp?.version ?? 0;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Highest known graph version — checkpoint anchor or the max recorded entry
|
|
82
|
+
* version, whichever is higher. Seeds the tool-layer counter so versions run on
|
|
83
|
+
* across sessions AND across compactions.
|
|
84
|
+
*/
|
|
85
|
+
latestVersion() {
|
|
86
|
+
let version = 0;
|
|
87
|
+
for (const l of this.readLines()) {
|
|
88
|
+
if (isCheckpoint(l))
|
|
89
|
+
version = Math.max(version, l.version);
|
|
90
|
+
else if (typeof l.graphVersion === 'number')
|
|
91
|
+
version = Math.max(version, l.graphVersion);
|
|
92
|
+
}
|
|
93
|
+
return version;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Archive the active log and start a fresh one anchored by a checkpoint line.
|
|
97
|
+
* Version-safe by construction: `latestVersion()` is identical before and after.
|
|
98
|
+
* No-op on a missing/empty log.
|
|
99
|
+
*/
|
|
100
|
+
compact(reason = 'manual') {
|
|
101
|
+
if (!existsSync(this.path))
|
|
102
|
+
return { archivedTo: null, checkpointVersion: 0 };
|
|
103
|
+
const version = this.latestVersion();
|
|
104
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
105
|
+
const archive = this.path.replace(/\.jsonl$/, `-${stamp}.jsonl`);
|
|
106
|
+
renameSync(this.path, archive);
|
|
107
|
+
const cp = { checkpoint: true, version, timestamp: new Date().toISOString(), reason };
|
|
108
|
+
writeFileSync(this.path, JSON.stringify(cp) + '\n', 'utf8');
|
|
109
|
+
return { archivedTo: archive, checkpointVersion: version };
|
|
110
|
+
}
|
|
111
|
+
maybeCompact() {
|
|
112
|
+
try {
|
|
113
|
+
if (existsSync(this.path) && statSync(this.path).size > this.maxBytes)
|
|
114
|
+
this.compact('auto-size');
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// A stat/rename race only defers compaction to the next bind — never fatal.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Read all parseable lines; skip a torn tail (crash mid-append) with a warning. */
|
|
121
|
+
readLines() {
|
|
122
|
+
if (!existsSync(this.path))
|
|
123
|
+
return [];
|
|
124
|
+
const raw = readFileSync(this.path, 'utf8');
|
|
125
|
+
const lines = [];
|
|
126
|
+
let skipped = 0;
|
|
127
|
+
for (const line of raw.split('\n')) {
|
|
128
|
+
if (!line.trim())
|
|
129
|
+
continue;
|
|
130
|
+
try {
|
|
131
|
+
lines.push(JSON.parse(line));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
skipped += 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (skipped > 0) {
|
|
138
|
+
process.stderr.write(`[graph-api-core] WARN: ${this.path}: skipped ${skipped} unparseable line(s) (torn tail after a crash?).\n`);
|
|
139
|
+
}
|
|
140
|
+
return lines;
|
|
141
|
+
}
|
|
142
|
+
readEntries() {
|
|
143
|
+
return this.readLines().filter((l) => !isCheckpoint(l));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sigloch/graph-api-core/browser — browser-safe entry point.
|
|
3
|
+
*
|
|
4
|
+
* The main barrel (`./index.js`) re-exports `./audit.js`, whose durable
|
|
5
|
+
* `FileOperationsLog` statically imports `node:fs`/`node:path` (CR-207).
|
|
6
|
+
* A browser bundler externalizes those to a stub that THROWS on property
|
|
7
|
+
* access, so importing the barrel client-side dies at module-eval before a
|
|
8
|
+
* single React component mounts. Browser consumers (e.g. graph-view-edit's
|
|
9
|
+
* renderer/edit-surface) import ONLY the framework-agnostic, node-free
|
|
10
|
+
* surface from here instead — no operations-log, no store, no adapters.
|
|
11
|
+
*
|
|
12
|
+
* @author andreas@siglochconsulting
|
|
13
|
+
*/
|
|
14
|
+
export { findRoot } from './find-root.js';
|
|
15
|
+
export type { RootQueryGraph } from './find-root.js';
|
|
16
|
+
export { SE_DESCRIPTOR, projectToOntologyGraph } from './se-descriptor.js';
|
|
17
|
+
export { FormatECodec } from './format-e-codec.js';
|
|
18
|
+
export { isValidTrace, tracePatternsOf } from './types.js';
|
|
19
|
+
export type { GraphNode, GraphEdge, Graph } from './types.js';
|
|
20
|
+
export type { OntologyDescriptor, NodeTypeDescriptor, EdgeTypeDescriptor, TracePattern } from './types.js';
|
|
21
|
+
export type { FormatEOperation, FormatEDiff } from './types.js';
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sigloch/graph-api-core/browser — browser-safe entry point.
|
|
3
|
+
*
|
|
4
|
+
* The main barrel (`./index.js`) re-exports `./audit.js`, whose durable
|
|
5
|
+
* `FileOperationsLog` statically imports `node:fs`/`node:path` (CR-207).
|
|
6
|
+
* A browser bundler externalizes those to a stub that THROWS on property
|
|
7
|
+
* access, so importing the barrel client-side dies at module-eval before a
|
|
8
|
+
* single React component mounts. Browser consumers (e.g. graph-view-edit's
|
|
9
|
+
* renderer/edit-surface) import ONLY the framework-agnostic, node-free
|
|
10
|
+
* surface from here instead — no operations-log, no store, no adapters.
|
|
11
|
+
*
|
|
12
|
+
* @author andreas@siglochconsulting
|
|
13
|
+
*/
|
|
14
|
+
export { findRoot } from './find-root.js';
|
|
15
|
+
export { SE_DESCRIPTOR, projectToOntologyGraph } from './se-descriptor.js';
|
|
16
|
+
export { FormatECodec } from './format-e-codec.js';
|
|
17
|
+
export { isValidTrace, tracePatternsOf } from './types.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure edge-rewiring semantics for update-edge / merge-nodes (CR-198).
|
|
3
|
+
* No storage access — GraphService.mutate() and the graphcode Apply-Gate
|
|
4
|
+
* both call this so flip/merge logic exists in exactly one place.
|
|
5
|
+
*/
|
|
6
|
+
import type { Graph, GraphEdge } from './types.js';
|
|
7
|
+
export interface EdgeIdentity {
|
|
8
|
+
sourceId: string;
|
|
9
|
+
targetId: string;
|
|
10
|
+
edgeType: string;
|
|
11
|
+
}
|
|
12
|
+
export interface UpdateEdgeSet {
|
|
13
|
+
edgeType?: string;
|
|
14
|
+
flip?: boolean;
|
|
15
|
+
attributes?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface UpdateEdgeResult {
|
|
18
|
+
graph: Graph;
|
|
19
|
+
removed: GraphEdge;
|
|
20
|
+
added: GraphEdge;
|
|
21
|
+
}
|
|
22
|
+
export interface MergeNodesResult {
|
|
23
|
+
graph: Graph;
|
|
24
|
+
removedNode: string;
|
|
25
|
+
removedEdges: GraphEdge[];
|
|
26
|
+
addedEdges: GraphEdge[];
|
|
27
|
+
}
|
|
28
|
+
export type EdgeOp = {
|
|
29
|
+
op: 'update-edge';
|
|
30
|
+
edge: EdgeIdentity;
|
|
31
|
+
set: UpdateEdgeSet;
|
|
32
|
+
} | {
|
|
33
|
+
op: 'merge-nodes';
|
|
34
|
+
sourceUid: string;
|
|
35
|
+
targetUid: string;
|
|
36
|
+
};
|
|
37
|
+
/** Flip and/or retype an existing edge as one semantic op (old edge gone, new edge in). */
|
|
38
|
+
export declare function updateEdge(graph: Graph, edge: EdgeIdentity, set: UpdateEdgeSet): UpdateEdgeResult;
|
|
39
|
+
/** Absorb source into target: rewire all incident edges, drop source node, discard self-edges. */
|
|
40
|
+
export declare function mergeNodes(graph: Graph, sourceUid: string, targetUid: string): MergeNodesResult;
|
|
41
|
+
/** Dispatcher matching the contracts MutateCommand('update-edge'|'merge-nodes') shape. */
|
|
42
|
+
export declare function applyEdgeOps(graph: Graph, op: EdgeOp): UpdateEdgeResult | MergeNodesResult;
|
package/dist/edge-ops.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function sameEdge(a, b) {
|
|
2
|
+
return a.sourceId === b.sourceId && a.targetId === b.targetId && a.edgeType === b.edgeType;
|
|
3
|
+
}
|
|
4
|
+
/** Flip and/or retype an existing edge as one semantic op (old edge gone, new edge in). */
|
|
5
|
+
export function updateEdge(graph, edge, set) {
|
|
6
|
+
const idx = graph.edges.findIndex(e => sameEdge(e, edge));
|
|
7
|
+
if (idx === -1) {
|
|
8
|
+
throw new Error(`update-edge: edge not found: ${edge.sourceId} -${edge.edgeType}-> ${edge.targetId}`);
|
|
9
|
+
}
|
|
10
|
+
const existing = graph.edges[idx];
|
|
11
|
+
const added = {
|
|
12
|
+
sourceId: set.flip ? existing.targetId : existing.sourceId,
|
|
13
|
+
targetId: set.flip ? existing.sourceId : existing.targetId,
|
|
14
|
+
edgeType: set.edgeType ?? existing.edgeType,
|
|
15
|
+
attributes: set.attributes ? { ...existing.attributes, ...set.attributes } : existing.attributes,
|
|
16
|
+
};
|
|
17
|
+
const edges = graph.edges.slice();
|
|
18
|
+
edges.splice(idx, 1);
|
|
19
|
+
// A flip/retype can collide with an edge that already exists under the new
|
|
20
|
+
// identity (e.g. both directions of a relation are already present) — the
|
|
21
|
+
// result graph keeps one edge, not two identical ones.
|
|
22
|
+
if (!edges.some(e => sameEdge(e, added)))
|
|
23
|
+
edges.push(added);
|
|
24
|
+
return { graph: { ...graph, edges }, removed: existing, added };
|
|
25
|
+
}
|
|
26
|
+
/** Absorb source into target: rewire all incident edges, drop source node, discard self-edges. */
|
|
27
|
+
export function mergeNodes(graph, sourceUid, targetUid) {
|
|
28
|
+
if (sourceUid === targetUid) {
|
|
29
|
+
throw new Error(`merge-nodes: source and target must differ (${sourceUid})`);
|
|
30
|
+
}
|
|
31
|
+
if (!graph.nodes.some(n => n.uid === sourceUid)) {
|
|
32
|
+
throw new Error(`merge-nodes: source node not found: ${sourceUid}`);
|
|
33
|
+
}
|
|
34
|
+
const edges = [];
|
|
35
|
+
const removedEdges = [];
|
|
36
|
+
const addedEdges = [];
|
|
37
|
+
for (const e of graph.edges) {
|
|
38
|
+
const touchesSource = e.sourceId === sourceUid || e.targetId === sourceUid;
|
|
39
|
+
if (!touchesSource) {
|
|
40
|
+
edges.push(e);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
removedEdges.push(e);
|
|
44
|
+
const rewired = {
|
|
45
|
+
...e,
|
|
46
|
+
sourceId: e.sourceId === sourceUid ? targetUid : e.sourceId,
|
|
47
|
+
targetId: e.targetId === sourceUid ? targetUid : e.targetId,
|
|
48
|
+
};
|
|
49
|
+
if (rewired.sourceId === rewired.targetId) {
|
|
50
|
+
continue; // self-edge created by rewiring — discarded
|
|
51
|
+
}
|
|
52
|
+
addedEdges.push(rewired);
|
|
53
|
+
// Two incident edges (or one incident + one pre-existing) can rewire onto
|
|
54
|
+
// the same (source,target,type) — keep the result graph free of duplicates.
|
|
55
|
+
if (!edges.some(x => sameEdge(x, rewired)))
|
|
56
|
+
edges.push(rewired);
|
|
57
|
+
}
|
|
58
|
+
const nodes = graph.nodes.filter(n => n.uid !== sourceUid);
|
|
59
|
+
return { graph: { nodes, edges }, removedNode: sourceUid, removedEdges, addedEdges };
|
|
60
|
+
}
|
|
61
|
+
/** Dispatcher matching the contracts MutateCommand('update-edge'|'merge-nodes') shape. */
|
|
62
|
+
export function applyEdgeOps(graph, op) {
|
|
63
|
+
if (op.op === 'update-edge')
|
|
64
|
+
return updateEdge(graph, op.edge, op.set);
|
|
65
|
+
return mergeNodes(graph, op.sourceUid, op.targetUid);
|
|
66
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for quick bootstrap of GraphService with adapters.
|
|
3
|
+
*/
|
|
4
|
+
import type { OntologyDescriptor } from './types.js';
|
|
5
|
+
import { GraphService } from './graph-service.js';
|
|
6
|
+
import type { StorageAdapter } from './storage-adapter.js';
|
|
7
|
+
import type { TransportAdapter, TransportConfig } from './transport-adapter.js';
|
|
8
|
+
export interface GraphApiConfig {
|
|
9
|
+
ontology: OntologyDescriptor;
|
|
10
|
+
storage?: StorageAdapter;
|
|
11
|
+
transport?: TransportAdapter;
|
|
12
|
+
transportConfig?: TransportConfig;
|
|
13
|
+
}
|
|
14
|
+
export interface GraphApi {
|
|
15
|
+
service: GraphService;
|
|
16
|
+
transport?: TransportAdapter;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create and start a GraphService with optional transport.
|
|
20
|
+
*
|
|
21
|
+
* Minimal usage:
|
|
22
|
+
* ```ts
|
|
23
|
+
* const api = await createGraphApi({ ontology: myOntology })
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function createGraphApi(config: GraphApiConfig): Promise<GraphApi>;
|
package/dist/factory.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { GraphService } from './graph-service.js';
|
|
2
|
+
import { MemoryAdapter } from './memory-adapter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create and start a GraphService with optional transport.
|
|
5
|
+
*
|
|
6
|
+
* Minimal usage:
|
|
7
|
+
* ```ts
|
|
8
|
+
* const api = await createGraphApi({ ontology: myOntology })
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export async function createGraphApi(config) {
|
|
12
|
+
const storage = config.storage ?? new MemoryAdapter();
|
|
13
|
+
const service = new GraphService({
|
|
14
|
+
ontology: config.ontology,
|
|
15
|
+
storage,
|
|
16
|
+
});
|
|
17
|
+
await service.initialize();
|
|
18
|
+
if (config.transport) {
|
|
19
|
+
config.transport.mount(service, config.transportConfig ?? {});
|
|
20
|
+
await config.transport.start();
|
|
21
|
+
}
|
|
22
|
+
return { service, transport: config.transport };
|
|
23
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* findRoot — strukturelle Wurzel-Suche im SE-Ontologiegraph.
|
|
3
|
+
*
|
|
4
|
+
* Der Root ist der `SYS`-Knoten ("System (Wurzel)"), der KEINE eingehende
|
|
5
|
+
* `compose`-Kante hat — also nicht selbst als Sub-System unter einem anderen
|
|
6
|
+
* SYS hängt. Damit muss keine Wurzel-UID hardcodiert werden (vgl. das
|
|
7
|
+
* verdrahtete `SYS-gve` in graph-view-edit) und keine per Pfad-Hash abgeleitet
|
|
8
|
+
* werden (vgl. `projectSysId` in aimprove); die Wurzel wird aus der Struktur
|
|
9
|
+
* gefunden.
|
|
10
|
+
*
|
|
11
|
+
* Reihenfolge:
|
|
12
|
+
* 1. SYS-Knoten ohne eingehende compose-Kante → der eigentliche Root.
|
|
13
|
+
* 2. sonst der erste SYS-Knoten → Fallback (Zyklus / defekter
|
|
14
|
+
* Graph), damit ein Konsument nie ohne Anker dasteht.
|
|
15
|
+
* 3. sonst null → kein SYS (leerer/neuer Graph).
|
|
16
|
+
*
|
|
17
|
+
* Strukturell typisiert (nur `elements`/`traces` mit den nötigen Feldern), damit
|
|
18
|
+
* sowohl `OntologyGraph` als auch der rohe Graph-JSON-Shape der Renderer passen.
|
|
19
|
+
*
|
|
20
|
+
* @author andreas@siglochconsulting
|
|
21
|
+
*/
|
|
22
|
+
export interface RootQueryGraph {
|
|
23
|
+
elements?: ReadonlyArray<{
|
|
24
|
+
id: string;
|
|
25
|
+
type: string;
|
|
26
|
+
}>;
|
|
27
|
+
traces?: ReadonlyArray<{
|
|
28
|
+
source: string;
|
|
29
|
+
target: string;
|
|
30
|
+
type: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
/** Liefert die UID der Graph-Wurzel (SYS ohne eingehende compose) oder null. */
|
|
34
|
+
export declare function findRoot(graph: RootQueryGraph | null | undefined): string | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Liefert die UID der Graph-Wurzel (SYS ohne eingehende compose) oder null. */
|
|
2
|
+
export function findRoot(graph) {
|
|
3
|
+
const elements = graph?.elements ?? [];
|
|
4
|
+
const systems = elements.filter((e) => e.type === 'SYS');
|
|
5
|
+
if (systems.length === 0)
|
|
6
|
+
return null;
|
|
7
|
+
const composeTargets = new Set((graph?.traces ?? []).filter((t) => t.type === 'compose').map((t) => t.target));
|
|
8
|
+
const rootSys = systems.find((s) => !composeTargets.has(s.id));
|
|
9
|
+
return (rootSys ?? systems[0]).id;
|
|
10
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OntologyDescriptor, FormatEDiff, Graph } from './types.js';
|
|
2
|
+
export declare class FormatECodec {
|
|
3
|
+
private readonly ontology;
|
|
4
|
+
private readonly edgeArrowToType;
|
|
5
|
+
private readonly validNodeTypes;
|
|
6
|
+
/** Trace-legality patterns (CR-GC-247) — the SSOT the parser validates edges against. */
|
|
7
|
+
private readonly patterns;
|
|
8
|
+
constructor(ontology: OntologyDescriptor);
|
|
9
|
+
/** Extract a ```format-e block from LLM output. Returns null if not found. */
|
|
10
|
+
extractFromLlm(llmOutput: string): string | null;
|
|
11
|
+
/** Parse Format E text into validated operations. */
|
|
12
|
+
parse(input: string): FormatEDiff;
|
|
13
|
+
/** Serialize a Graph to Format E text. */
|
|
14
|
+
serialize(graph: Graph): string;
|
|
15
|
+
private looksLikeNode;
|
|
16
|
+
private parseNodeLine;
|
|
17
|
+
private parseEdgeLine;
|
|
18
|
+
private parseMerge;
|
|
19
|
+
private extractNodeType;
|
|
20
|
+
private parseInlineAttrs;
|
|
21
|
+
private serializeAttrs;
|
|
22
|
+
private edgeTypeToArrow;
|
|
23
|
+
}
|