trellis 1.0.8 → 2.0.6
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/README.md +564 -83
- package/bin/trellis.mjs +2 -0
- package/dist/cli/index.js +4718 -0
- package/dist/core/index.js +12 -0
- package/dist/decisions/index.js +19 -0
- package/dist/embeddings/index.js +43 -0
- package/dist/index-1j1anhmr.js +4038 -0
- package/dist/index-3s0eak0p.js +1556 -0
- package/dist/index-8pce39mh.js +272 -0
- package/dist/index-a76rekgs.js +67 -0
- package/dist/index-cy9k1g6v.js +684 -0
- package/dist/index-fd4e26s4.js +69 -0
- package/dist/{store/eav-store.js → index-gkvhzm9f.js} +4 -6
- package/dist/index-gnw8d7d6.js +51 -0
- package/dist/index-vkpkfwhq.js +817 -0
- package/dist/index.js +118 -2876
- package/dist/links/index.js +55 -0
- package/dist/transformers-m9je15kg.js +32491 -0
- package/dist/vcs/index.js +110 -0
- package/logo.png +0 -0
- package/logo.svg +9 -0
- package/package.json +79 -76
- package/src/cli/index.ts +2340 -0
- package/src/core/index.ts +35 -0
- package/src/core/kernel/middleware.ts +44 -0
- package/src/core/persist/backend.ts +64 -0
- package/src/core/store/eav-store.ts +467 -0
- package/src/decisions/auto-capture.ts +136 -0
- package/src/decisions/hooks.ts +163 -0
- package/src/decisions/index.ts +261 -0
- package/src/decisions/types.ts +103 -0
- package/src/embeddings/chunker.ts +327 -0
- package/src/embeddings/index.ts +41 -0
- package/src/embeddings/model.ts +95 -0
- package/src/embeddings/search.ts +305 -0
- package/src/embeddings/store.ts +313 -0
- package/src/embeddings/types.ts +85 -0
- package/src/engine.ts +1083 -0
- package/src/garden/cluster.ts +330 -0
- package/src/garden/garden.ts +306 -0
- package/src/garden/index.ts +29 -0
- package/src/git/git-exporter.ts +286 -0
- package/src/git/git-importer.ts +329 -0
- package/src/git/git-reader.ts +189 -0
- package/src/git/index.ts +22 -0
- package/src/identity/governance.ts +211 -0
- package/src/identity/identity.ts +224 -0
- package/src/identity/index.ts +30 -0
- package/src/identity/signing-middleware.ts +97 -0
- package/src/index.ts +20 -0
- package/src/links/index.ts +49 -0
- package/src/links/lifecycle.ts +400 -0
- package/src/links/parser.ts +484 -0
- package/src/links/ref-index.ts +186 -0
- package/src/links/resolver.ts +314 -0
- package/src/links/types.ts +108 -0
- package/src/mcp/index.ts +22 -0
- package/src/mcp/server.ts +1278 -0
- package/src/semantic/csharp-parser.ts +493 -0
- package/src/semantic/go-parser.ts +585 -0
- package/src/semantic/index.ts +34 -0
- package/src/semantic/java-parser.ts +456 -0
- package/src/semantic/python-parser.ts +659 -0
- package/src/semantic/ruby-parser.ts +446 -0
- package/src/semantic/rust-parser.ts +784 -0
- package/src/semantic/semantic-merge.ts +210 -0
- package/src/semantic/ts-parser.ts +681 -0
- package/src/semantic/types.ts +175 -0
- package/src/sync/index.ts +32 -0
- package/src/sync/memory-transport.ts +66 -0
- package/src/sync/reconciler.ts +237 -0
- package/src/sync/sync-engine.ts +258 -0
- package/src/sync/types.ts +104 -0
- package/src/vcs/blob-store.ts +124 -0
- package/src/vcs/branch.ts +150 -0
- package/src/vcs/checkpoint.ts +64 -0
- package/src/vcs/decompose.ts +469 -0
- package/src/vcs/diff.ts +409 -0
- package/src/vcs/engine-context.ts +26 -0
- package/src/vcs/index.ts +23 -0
- package/src/vcs/issue.ts +800 -0
- package/src/vcs/merge.ts +425 -0
- package/src/vcs/milestone.ts +124 -0
- package/src/vcs/ops.ts +59 -0
- package/src/vcs/types.ts +213 -0
- package/src/vcs/vcs-middleware.ts +81 -0
- package/src/watcher/fs-watcher.ts +217 -0
- package/src/watcher/index.ts +9 -0
- package/src/watcher/ingestion.ts +116 -0
- package/dist/ai/index.js +0 -688
- package/dist/cli/server.js +0 -3321
- package/dist/cli/tql.js +0 -5282
- package/dist/client/tql-client.js +0 -108
- package/dist/graph/index.js +0 -2248
- package/dist/kernel/logic-middleware.js +0 -179
- package/dist/kernel/middleware.js +0 -0
- package/dist/kernel/operations.js +0 -32
- package/dist/kernel/schema-middleware.js +0 -34
- package/dist/kernel/security-middleware.js +0 -53
- package/dist/kernel/trellis-kernel.js +0 -2239
- package/dist/kernel/workspace.js +0 -91
- package/dist/persist/backend.js +0 -0
- package/dist/persist/sqlite-backend.js +0 -123
- package/dist/query/index.js +0 -1643
- package/dist/server/index.js +0 -3309
- package/dist/workflows/index.js +0 -3160
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrellisVCS Core — EAV Store, Kernel Types, Middleware
|
|
3
|
+
*
|
|
4
|
+
* Inlined from trellis-core for single-package publish.
|
|
5
|
+
* Consumers: `import { EAVStore, Fact } from 'trellis/core'`
|
|
6
|
+
*
|
|
7
|
+
* @module trellis/core
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// EAV Store
|
|
11
|
+
export { EAVStore, flatten, jsonEntityFacts } from './store/eav-store.js';
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
Atom,
|
|
15
|
+
EntityRef,
|
|
16
|
+
Fact,
|
|
17
|
+
Link,
|
|
18
|
+
CatalogEntry,
|
|
19
|
+
QueryTraceEntry,
|
|
20
|
+
QueryResult,
|
|
21
|
+
} from './store/eav-store.js';
|
|
22
|
+
|
|
23
|
+
// Kernel persistence types
|
|
24
|
+
export type {
|
|
25
|
+
KernelOp,
|
|
26
|
+
KernelOpKind,
|
|
27
|
+
KernelBackend,
|
|
28
|
+
} from './persist/backend.js';
|
|
29
|
+
|
|
30
|
+
// Kernel middleware types
|
|
31
|
+
export type {
|
|
32
|
+
KernelMiddleware,
|
|
33
|
+
MiddlewareContext,
|
|
34
|
+
OpMiddlewareNext,
|
|
35
|
+
} from './kernel/middleware.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel middleware types.
|
|
3
|
+
* Slim version inlined from trellis-core — only includes the types
|
|
4
|
+
* used by the VCS layer (op middleware). Query middleware types are
|
|
5
|
+
* omitted to avoid pulling in the full query engine dependency chain.
|
|
6
|
+
*
|
|
7
|
+
* @module trellis/core
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { KernelOp } from '../persist/backend.js';
|
|
11
|
+
|
|
12
|
+
export type MiddlewareContext = {
|
|
13
|
+
agentId?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type OpMiddlewareNext = (
|
|
18
|
+
op: KernelOp,
|
|
19
|
+
ctx: MiddlewareContext,
|
|
20
|
+
) => void | Promise<void>;
|
|
21
|
+
|
|
22
|
+
export interface KernelMiddleware {
|
|
23
|
+
name: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hook into kernel operations (mutations).
|
|
27
|
+
* Can throw to block the operation (e.g. for security).
|
|
28
|
+
*/
|
|
29
|
+
handleOp?: (
|
|
30
|
+
op: KernelOp,
|
|
31
|
+
ctx: MiddlewareContext,
|
|
32
|
+
next: OpMiddlewareNext,
|
|
33
|
+
) => void | Promise<void>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hook into kernel queries.
|
|
37
|
+
* Typed loosely here to avoid importing the full query engine.
|
|
38
|
+
*/
|
|
39
|
+
handleQuery?: (
|
|
40
|
+
query: unknown,
|
|
41
|
+
ctx: MiddlewareContext,
|
|
42
|
+
next: (...args: any[]) => any,
|
|
43
|
+
) => any;
|
|
44
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel persistence backend types.
|
|
3
|
+
* Inlined from trellis-core for single-package publish.
|
|
4
|
+
*
|
|
5
|
+
* @module trellis/core
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Fact, Link } from '../store/eav-store.js';
|
|
9
|
+
|
|
10
|
+
export type KernelOpKind =
|
|
11
|
+
| 'addFacts'
|
|
12
|
+
| 'addLinks'
|
|
13
|
+
| 'deleteFacts'
|
|
14
|
+
| 'deleteLinks';
|
|
15
|
+
|
|
16
|
+
export interface KernelOp {
|
|
17
|
+
/**
|
|
18
|
+
* Content hash of this operation (including previousHash).
|
|
19
|
+
* Format: trellis:op:{hash}
|
|
20
|
+
*/
|
|
21
|
+
hash: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Kind of operation.
|
|
25
|
+
*/
|
|
26
|
+
kind: KernelOpKind;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* ISO timestamp of when the op was created.
|
|
30
|
+
*/
|
|
31
|
+
timestamp: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The ID of the agent that performed the operation.
|
|
35
|
+
*/
|
|
36
|
+
agentId: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hash of the previous operation in the local chain.
|
|
40
|
+
*/
|
|
41
|
+
previousHash?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The actual data payload.
|
|
45
|
+
*/
|
|
46
|
+
facts?: Fact[];
|
|
47
|
+
links?: Link[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface KernelBackend {
|
|
51
|
+
init(): void;
|
|
52
|
+
append(op: KernelOp): void;
|
|
53
|
+
readAll(): KernelOp[];
|
|
54
|
+
readUntil(hash: string): KernelOp[];
|
|
55
|
+
readAfter(hash: string): KernelOp[];
|
|
56
|
+
readUntilTimestamp(isoTimestamp: string): KernelOp[];
|
|
57
|
+
getLastOp(): KernelOp | undefined;
|
|
58
|
+
|
|
59
|
+
// Snapshot support
|
|
60
|
+
saveSnapshot(lastOpHash: string, data: any): void;
|
|
61
|
+
loadLatestSnapshot(): { lastOpHash: string; data: any } | undefined;
|
|
62
|
+
|
|
63
|
+
close?(): void;
|
|
64
|
+
}
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EAV-based Datalog Engine with Path-Aware Ingestor
|
|
3
|
+
*
|
|
4
|
+
* Core types and fact storage for schema-agnostic data processing.
|
|
5
|
+
* Inlined from trellis-core for single-package publish.
|
|
6
|
+
*
|
|
7
|
+
* @module trellis/core
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type Atom = string | number | boolean | Date | EntityRef;
|
|
11
|
+
export type EntityRef = string;
|
|
12
|
+
|
|
13
|
+
export interface Fact {
|
|
14
|
+
e: string; // entity
|
|
15
|
+
a: string; // attribute (JSONPath)
|
|
16
|
+
v: Atom; // value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Link {
|
|
20
|
+
e1: string; // source entity
|
|
21
|
+
a: string; // relationship attribute
|
|
22
|
+
e2: string; // target entity
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CatalogEntry {
|
|
26
|
+
attribute: string;
|
|
27
|
+
type: 'string' | 'number' | 'boolean' | 'date' | 'mixed';
|
|
28
|
+
cardinality: 'one' | 'many';
|
|
29
|
+
distinctCount: number;
|
|
30
|
+
examples: Atom[];
|
|
31
|
+
min?: number;
|
|
32
|
+
max?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface QueryTraceEntry {
|
|
36
|
+
goal: string;
|
|
37
|
+
bindingsCount: number;
|
|
38
|
+
durationMs: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface QueryResult {
|
|
42
|
+
bindings: Record<string, Atom>[];
|
|
43
|
+
executionTime: number;
|
|
44
|
+
plan?: string;
|
|
45
|
+
trace?: QueryTraceEntry[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Path-aware JSON flattener
|
|
50
|
+
* Converts nested JSON into attribute-value pairs with dot notation
|
|
51
|
+
*/
|
|
52
|
+
export function* flatten(obj: any, base = ''): Generator<[string, any]> {
|
|
53
|
+
if (Array.isArray(obj)) {
|
|
54
|
+
for (const v of obj) {
|
|
55
|
+
yield* flatten(v, base);
|
|
56
|
+
}
|
|
57
|
+
} else if (obj && typeof obj === 'object') {
|
|
58
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
59
|
+
yield* flatten(v, base ? `${base}.${k}` : k);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
yield [base, obj];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert JSON entity to EAV facts
|
|
68
|
+
*/
|
|
69
|
+
export function jsonEntityFacts(
|
|
70
|
+
entityId: string,
|
|
71
|
+
root: any,
|
|
72
|
+
type: string,
|
|
73
|
+
): Fact[] {
|
|
74
|
+
const facts: Fact[] = [{ e: entityId, a: 'type', v: type }];
|
|
75
|
+
|
|
76
|
+
for (const [a, v] of flatten(root)) {
|
|
77
|
+
if (v === undefined || v === null) continue;
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(v)) {
|
|
80
|
+
for (const el of v) {
|
|
81
|
+
facts.push({ e: entityId, a, v: el as any });
|
|
82
|
+
}
|
|
83
|
+
} else if (typeof v === 'object') {
|
|
84
|
+
// Handled by flatten recursion
|
|
85
|
+
} else {
|
|
86
|
+
facts.push({ e: entityId, a, v: v as any });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return facts;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* In-memory EAV triple store
|
|
95
|
+
*/
|
|
96
|
+
export class EAVStore {
|
|
97
|
+
private facts: Fact[] = [];
|
|
98
|
+
private links: Link[] = [];
|
|
99
|
+
private catalog: Map<string, CatalogEntry> = new Map();
|
|
100
|
+
|
|
101
|
+
// Indexes for fast lookups
|
|
102
|
+
private eavIndex: Map<string, Map<string, Set<number>>> = new Map();
|
|
103
|
+
private aevIndex: Map<string, Map<string, Set<number>>> = new Map();
|
|
104
|
+
private aveIndex: Map<string, Map<Atom, Set<number>>> = new Map();
|
|
105
|
+
|
|
106
|
+
// Link indexes for graph queries
|
|
107
|
+
private linkIndex: Map<string, Map<string, Set<string>>> = new Map(); // e1 -> a -> e2s
|
|
108
|
+
private linkReverseIndex: Map<string, Map<string, Set<string>>> = new Map(); // e2 -> a -> e1s
|
|
109
|
+
private linkAttrIndex: Map<string, Set<[string, string]>> = new Map(); // a -> [(e1, e2)]
|
|
110
|
+
|
|
111
|
+
// Distinct value tracking
|
|
112
|
+
private distinct = new Map<string, Set<string>>(); // attr -> set of valueKey
|
|
113
|
+
|
|
114
|
+
addFacts(facts: Fact[]): void {
|
|
115
|
+
for (let i = 0; i < facts.length; i++) {
|
|
116
|
+
const fact = facts[i];
|
|
117
|
+
if (fact) {
|
|
118
|
+
this.facts.push(fact);
|
|
119
|
+
this.updateIndexes(fact, this.facts.length - 1);
|
|
120
|
+
this.updateCatalog(fact);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
addLinks(links: Link[]): void {
|
|
126
|
+
for (const link of links) {
|
|
127
|
+
this.links.push(link);
|
|
128
|
+
this.updateLinkIndexes(link);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
deleteFacts(factsToDelete: Fact[]): void {
|
|
133
|
+
for (const fact of factsToDelete) {
|
|
134
|
+
// Find the fact index
|
|
135
|
+
const valueKey = this.valueKey(fact.v);
|
|
136
|
+
const indices = this.aveIndex.get(fact.a)?.get(valueKey);
|
|
137
|
+
if (!indices) continue;
|
|
138
|
+
|
|
139
|
+
let foundIdx = -1;
|
|
140
|
+
for (const idx of indices) {
|
|
141
|
+
const storedFact = this.facts[idx];
|
|
142
|
+
if (storedFact && storedFact.e === fact.e && storedFact.a === fact.a) {
|
|
143
|
+
foundIdx = idx;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (foundIdx !== -1) {
|
|
149
|
+
// Remove from main facts (set to null to maintain indices if needed, or just remove and rebuild)
|
|
150
|
+
// For simplicity and to keep indices valid without shifting, we'll set to undefined
|
|
151
|
+
this.facts[foundIdx] = undefined as any;
|
|
152
|
+
|
|
153
|
+
// Remove from indexes
|
|
154
|
+
this.eavIndex.get(fact.e)?.get(fact.a)?.delete(foundIdx);
|
|
155
|
+
this.aevIndex.get(fact.a)?.get(fact.e)?.delete(foundIdx);
|
|
156
|
+
this.aveIndex.get(fact.a)?.get(valueKey)?.delete(foundIdx);
|
|
157
|
+
|
|
158
|
+
// Update catalog (approximate, since we don't fully rebuild it)
|
|
159
|
+
const entry = this.catalog.get(fact.a);
|
|
160
|
+
if (entry) {
|
|
161
|
+
// If we had a specific count, we'd decrement, but distinctCount needs re-check
|
|
162
|
+
// For now we'll just leave it or let it be refreshed later
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
deleteLinks(linksToDelete: Link[]): void {
|
|
169
|
+
for (const link of linksToDelete) {
|
|
170
|
+
// Find and remove from main links list
|
|
171
|
+
const initialLen = this.links.length;
|
|
172
|
+
this.links = this.links.filter(
|
|
173
|
+
(l) => !(l.e1 === link.e1 && l.a === link.a && l.e2 === link.e2),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (this.links.length < initialLen) {
|
|
177
|
+
// Remove from indexes
|
|
178
|
+
this.linkIndex.get(link.e1)?.get(link.a)?.delete(link.e2);
|
|
179
|
+
this.linkReverseIndex.get(link.e2)?.get(link.a)?.delete(link.e1);
|
|
180
|
+
|
|
181
|
+
const attrPairs = this.linkAttrIndex.get(link.a);
|
|
182
|
+
if (attrPairs) {
|
|
183
|
+
for (const pair of attrPairs) {
|
|
184
|
+
if (pair[0] === link.e1 && pair[1] === link.e2) {
|
|
185
|
+
attrPairs.delete(pair);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private updateIndexes(fact: Fact, index: number): void {
|
|
195
|
+
// EAV index: entity -> attribute -> fact indices
|
|
196
|
+
if (!this.eavIndex.has(fact.e)) {
|
|
197
|
+
this.eavIndex.set(fact.e, new Map());
|
|
198
|
+
}
|
|
199
|
+
if (!this.eavIndex.get(fact.e)!.has(fact.a)) {
|
|
200
|
+
this.eavIndex.get(fact.e)!.set(fact.a, new Set());
|
|
201
|
+
}
|
|
202
|
+
this.eavIndex.get(fact.e)!.get(fact.a)!.add(index);
|
|
203
|
+
|
|
204
|
+
// AEV index: attribute -> entity -> fact indices
|
|
205
|
+
if (!this.aevIndex.has(fact.a)) {
|
|
206
|
+
this.aevIndex.set(fact.a, new Map());
|
|
207
|
+
}
|
|
208
|
+
if (!this.aevIndex.get(fact.a)!.has(fact.e)) {
|
|
209
|
+
this.aevIndex.get(fact.a)!.set(fact.e, new Set());
|
|
210
|
+
}
|
|
211
|
+
this.aevIndex.get(fact.a)!.get(fact.e)!.add(index);
|
|
212
|
+
|
|
213
|
+
// AVE index: attribute -> value -> fact indices
|
|
214
|
+
if (!this.aveIndex.has(fact.a)) {
|
|
215
|
+
this.aveIndex.set(fact.a, new Map());
|
|
216
|
+
}
|
|
217
|
+
const valueKey = this.valueKey(fact.v);
|
|
218
|
+
if (!this.aveIndex.get(fact.a)!.has(valueKey)) {
|
|
219
|
+
this.aveIndex.get(fact.a)!.set(valueKey, new Set());
|
|
220
|
+
}
|
|
221
|
+
this.aveIndex.get(fact.a)!.get(valueKey)!.add(index);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private updateLinkIndexes(link: Link): void {
|
|
225
|
+
// Forward index: e1 -> a -> e2s
|
|
226
|
+
if (!this.linkIndex.has(link.e1)) {
|
|
227
|
+
this.linkIndex.set(link.e1, new Map());
|
|
228
|
+
}
|
|
229
|
+
const e1Attrs = this.linkIndex.get(link.e1)!;
|
|
230
|
+
if (!e1Attrs.has(link.a)) {
|
|
231
|
+
e1Attrs.set(link.a, new Set());
|
|
232
|
+
}
|
|
233
|
+
e1Attrs.get(link.a)!.add(link.e2);
|
|
234
|
+
|
|
235
|
+
// Reverse index: e2 -> a -> e1s
|
|
236
|
+
if (!this.linkReverseIndex.has(link.e2)) {
|
|
237
|
+
this.linkReverseIndex.set(link.e2, new Map());
|
|
238
|
+
}
|
|
239
|
+
const e2Attrs = this.linkReverseIndex.get(link.e2)!;
|
|
240
|
+
if (!e2Attrs.has(link.a)) {
|
|
241
|
+
e2Attrs.set(link.a, new Set());
|
|
242
|
+
}
|
|
243
|
+
e2Attrs.get(link.a)!.add(link.e1);
|
|
244
|
+
|
|
245
|
+
// Attribute index: a -> [(e1, e2)]
|
|
246
|
+
if (!this.linkAttrIndex.has(link.a)) {
|
|
247
|
+
this.linkAttrIndex.set(link.a, new Set());
|
|
248
|
+
}
|
|
249
|
+
this.linkAttrIndex.get(link.a)!.add([link.e1, link.e2]);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private valueKey(v: Atom): string {
|
|
253
|
+
if (v instanceof Date) return `date:${v.toISOString()}`;
|
|
254
|
+
return `${typeof v}:${v}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private updateCatalog(fact: Fact): void {
|
|
258
|
+
const entry = this.catalog.get(fact.a) || {
|
|
259
|
+
attribute: fact.a,
|
|
260
|
+
type: this.inferType(fact.v),
|
|
261
|
+
cardinality: 'one',
|
|
262
|
+
distinctCount: 0,
|
|
263
|
+
examples: [],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Update type (may become 'mixed')
|
|
267
|
+
const factType = this.inferType(fact.v);
|
|
268
|
+
if (entry.type !== factType && entry.type !== 'mixed') {
|
|
269
|
+
entry.type = 'mixed';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Update cardinality (if we see multiple values for same entity+attribute)
|
|
273
|
+
const entityAttrs = this.eavIndex.get(fact.e)?.get(fact.a);
|
|
274
|
+
if (entityAttrs && entityAttrs.size > 1) {
|
|
275
|
+
entry.cardinality = 'many';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Update distinct count
|
|
279
|
+
const k = this.valueKey(fact.v);
|
|
280
|
+
const s =
|
|
281
|
+
this.distinct.get(fact.a) ||
|
|
282
|
+
(this.distinct.set(fact.a, new Set()), this.distinct.get(fact.a)!);
|
|
283
|
+
s.add(k);
|
|
284
|
+
entry.distinctCount = s.size;
|
|
285
|
+
|
|
286
|
+
// Update examples (keep first 5)
|
|
287
|
+
if (entry.examples.length < 5 && !entry.examples.includes(fact.v)) {
|
|
288
|
+
entry.examples.push(fact.v);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Update numeric ranges
|
|
292
|
+
if (typeof fact.v === 'number') {
|
|
293
|
+
entry.min = Math.min(entry.min ?? fact.v, fact.v);
|
|
294
|
+
entry.max = Math.max(entry.max ?? fact.v, fact.v);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.catalog.set(fact.a, entry);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private inferType(
|
|
301
|
+
v: Atom,
|
|
302
|
+
): 'string' | 'number' | 'boolean' | 'date' | 'mixed' {
|
|
303
|
+
if (typeof v === 'string') return 'string';
|
|
304
|
+
if (typeof v === 'number') return 'number';
|
|
305
|
+
if (typeof v === 'boolean') return 'boolean';
|
|
306
|
+
if (v instanceof Date) return 'date';
|
|
307
|
+
return 'mixed';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Query methods
|
|
311
|
+
getFactsByEntity(entity: string): Fact[] {
|
|
312
|
+
const indices = this.eavIndex.get(entity);
|
|
313
|
+
if (!indices) return [];
|
|
314
|
+
|
|
315
|
+
const result: Fact[] = [];
|
|
316
|
+
for (const attrIndices of indices.values()) {
|
|
317
|
+
for (const idx of attrIndices) {
|
|
318
|
+
const fact = this.facts[idx];
|
|
319
|
+
if (fact) {
|
|
320
|
+
result.push(fact);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
getFactsByAttribute(attribute: string): Fact[] {
|
|
328
|
+
const indices = this.aevIndex.get(attribute);
|
|
329
|
+
if (!indices) return [];
|
|
330
|
+
|
|
331
|
+
const result: Fact[] = [];
|
|
332
|
+
for (const entityIndices of indices.values()) {
|
|
333
|
+
for (const idx of entityIndices) {
|
|
334
|
+
const fact = this.facts[idx];
|
|
335
|
+
if (fact) {
|
|
336
|
+
result.push(fact);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getFactsByValue(attribute: string, value: Atom): Fact[] {
|
|
344
|
+
const indices = this.aveIndex.get(attribute)?.get(this.valueKey(value));
|
|
345
|
+
if (!indices) return [];
|
|
346
|
+
|
|
347
|
+
return Array.from(indices)
|
|
348
|
+
.map((idx) => this.facts[idx])
|
|
349
|
+
.filter((fact): fact is Fact => fact !== undefined);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
getCatalog(): CatalogEntry[] {
|
|
353
|
+
return Array.from(this.catalog.values());
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
getCatalogEntry(attribute: string): CatalogEntry | undefined {
|
|
357
|
+
return this.catalog.get(attribute);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Statistics
|
|
361
|
+
getAllFacts(): Fact[] {
|
|
362
|
+
return this.facts.filter((f): f is Fact => f !== undefined);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
getAllLinks(): Link[] {
|
|
366
|
+
return [...this.links];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
getLinksByEntity(entity: string): Link[] {
|
|
370
|
+
const results: Link[] = [];
|
|
371
|
+
const forwardLinks = this.linkIndex.get(entity);
|
|
372
|
+
if (forwardLinks) {
|
|
373
|
+
for (const [attr, targets] of forwardLinks) {
|
|
374
|
+
for (const target of targets) {
|
|
375
|
+
results.push({ e1: entity, a: attr, e2: target });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const reverseLinks = this.linkReverseIndex.get(entity);
|
|
380
|
+
if (reverseLinks) {
|
|
381
|
+
for (const [attr, sources] of reverseLinks) {
|
|
382
|
+
for (const source of sources) {
|
|
383
|
+
results.push({ e1: source, a: attr, e2: entity });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return results;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
getLinksByAttribute(attribute: string): Link[] {
|
|
391
|
+
const results: Link[] = [];
|
|
392
|
+
const links = this.linkAttrIndex.get(attribute);
|
|
393
|
+
if (links) {
|
|
394
|
+
for (const [e1, e2] of links) {
|
|
395
|
+
results.push({ e1, a: attribute, e2 });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return results;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
getLinksByEntityAndAttribute(entity: string, attribute: string): Link[] {
|
|
402
|
+
const results: Link[] = [];
|
|
403
|
+
const attrs = this.linkIndex.get(entity);
|
|
404
|
+
if (attrs) {
|
|
405
|
+
const targets = attrs.get(attribute);
|
|
406
|
+
if (targets) {
|
|
407
|
+
for (const target of targets) {
|
|
408
|
+
results.push({ e1: entity, a: attribute, e2: target });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return results;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
getStats() {
|
|
416
|
+
return {
|
|
417
|
+
totalFacts: this.facts.length,
|
|
418
|
+
totalLinks: this.links.length,
|
|
419
|
+
uniqueEntities: this.eavIndex.size,
|
|
420
|
+
uniqueAttributes: this.aevIndex.size,
|
|
421
|
+
catalogEntries: this.catalog.size,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Creates a serializable snapshot of the current store state.
|
|
427
|
+
*/
|
|
428
|
+
snapshot(): { facts: Fact[]; links: Link[]; catalog: CatalogEntry[] } {
|
|
429
|
+
return {
|
|
430
|
+
facts: this.facts.filter((f) => f !== undefined),
|
|
431
|
+
links: [...this.links],
|
|
432
|
+
catalog: this.getCatalog(),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Restores the store state from a snapshot and rebuilds all indexes.
|
|
438
|
+
*/
|
|
439
|
+
restore(snapshot: {
|
|
440
|
+
facts: Fact[];
|
|
441
|
+
links: Link[];
|
|
442
|
+
catalog: CatalogEntry[];
|
|
443
|
+
}): void {
|
|
444
|
+
// Clear current state
|
|
445
|
+
this.facts = [];
|
|
446
|
+
this.links = [];
|
|
447
|
+
this.catalog.clear();
|
|
448
|
+
this.eavIndex.clear();
|
|
449
|
+
this.aevIndex.clear();
|
|
450
|
+
this.aveIndex.clear();
|
|
451
|
+
this.linkIndex.clear();
|
|
452
|
+
this.linkReverseIndex.clear();
|
|
453
|
+
this.linkAttrIndex.clear();
|
|
454
|
+
this.distinct.clear();
|
|
455
|
+
|
|
456
|
+
// Re-add data
|
|
457
|
+
this.addFacts(snapshot.facts);
|
|
458
|
+
this.addLinks(snapshot.links);
|
|
459
|
+
|
|
460
|
+
// Explicitly restore catalog if provided (though addFacts rebuilds it partially)
|
|
461
|
+
if (snapshot.catalog) {
|
|
462
|
+
for (const entry of snapshot.catalog) {
|
|
463
|
+
this.catalog.set(entry.attribute, entry);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|