@specferret/core 0.1.4 → 0.2.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/dist/extractor/frontmatter.d.ts +4 -0
- package/dist/extractor/frontmatter.d.ts.map +1 -1
- package/dist/extractor/frontmatter.js +5 -0
- package/dist/extractor/frontmatter.js.map +1 -1
- package/dist/extractor/upward-classifier.d.ts +25 -0
- package/dist/extractor/upward-classifier.d.ts.map +1 -0
- package/dist/extractor/upward-classifier.js +48 -0
- package/dist/extractor/upward-classifier.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +1 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +37 -32
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/types.d.ts +6 -2
- package/dist/store/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/extractor/frontmatter.test.ts +74 -0
- package/src/extractor/frontmatter.ts +10 -0
- package/src/extractor/upward-classifier.test.ts +188 -0
- package/src/extractor/upward-classifier.ts +68 -0
- package/src/index.ts +1 -0
- package/src/store/sqlite.test.ts +135 -79
- package/src/store/sqlite.ts +45 -92
- package/src/store/types.ts +7 -6
package/src/store/sqlite.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import { Database } from
|
|
2
|
-
import * as path from
|
|
3
|
-
import * as fs from
|
|
4
|
-
import { randomUUID } from
|
|
5
|
-
import { findProjectRoot } from
|
|
6
|
-
import type {
|
|
7
|
-
DBStore,
|
|
8
|
-
FerretNode,
|
|
9
|
-
FerretContract,
|
|
10
|
-
FerretDependency,
|
|
11
|
-
FerretReconciliationLog,
|
|
12
|
-
FerretPlacementDecision,
|
|
13
|
-
NodeStatus,
|
|
14
|
-
} from "./types.js";
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { findProjectRoot } from '../utils/paths.js';
|
|
6
|
+
import type { DBStore, FerretNode, FerretContract, FerretDependency, FerretReconciliationLog, FerretPlacementDecision, NodeStatus } from './types.js';
|
|
15
7
|
|
|
16
8
|
export class SqliteStore implements DBStore {
|
|
17
9
|
private db: Database | null = null;
|
|
@@ -19,36 +11,27 @@ export class SqliteStore implements DBStore {
|
|
|
19
11
|
|
|
20
12
|
constructor(customPath?: string) {
|
|
21
13
|
const root = findProjectRoot();
|
|
22
|
-
const defaultPath = path.join(root,
|
|
23
|
-
this.dbPath =
|
|
24
|
-
customPath === ":memory:"
|
|
25
|
-
? ":memory:"
|
|
26
|
-
: customPath
|
|
27
|
-
? path.resolve(root, customPath)
|
|
28
|
-
: defaultPath;
|
|
14
|
+
const defaultPath = path.join(root, '.ferret', 'graph.db');
|
|
15
|
+
this.dbPath = customPath === ':memory:' ? ':memory:' : customPath ? path.resolve(root, customPath) : defaultPath;
|
|
29
16
|
}
|
|
30
17
|
|
|
31
18
|
async init(): Promise<void> {
|
|
32
19
|
if (this.db) return;
|
|
33
20
|
|
|
34
|
-
if (this.dbPath !==
|
|
21
|
+
if (this.dbPath !== ':memory:') {
|
|
35
22
|
const ferretDir = path.dirname(this.dbPath);
|
|
36
23
|
if (!fs.existsSync(ferretDir)) {
|
|
37
24
|
fs.mkdirSync(ferretDir, { recursive: true });
|
|
38
25
|
}
|
|
39
|
-
const gitignorePath = path.join(ferretDir,
|
|
26
|
+
const gitignorePath = path.join(ferretDir, '.gitignore');
|
|
40
27
|
try {
|
|
41
|
-
fs.writeFileSync(
|
|
42
|
-
gitignorePath,
|
|
43
|
-
"*\n!.gitignore\n!context.json\n",
|
|
44
|
-
"utf-8",
|
|
45
|
-
);
|
|
28
|
+
fs.writeFileSync(gitignorePath, '*\n!.gitignore\n!context.json\n', 'utf-8');
|
|
46
29
|
} catch {}
|
|
47
30
|
}
|
|
48
31
|
|
|
49
32
|
this.db = new Database(this.dbPath);
|
|
50
|
-
this.db.exec(
|
|
51
|
-
this.db.exec(
|
|
33
|
+
this.db.exec('PRAGMA journal_mode = WAL;');
|
|
34
|
+
this.db.exec('PRAGMA foreign_keys = ON;');
|
|
52
35
|
|
|
53
36
|
this.db.exec(`
|
|
54
37
|
CREATE TABLE IF NOT EXISTS ferret_nodes (
|
|
@@ -95,9 +78,15 @@ export class SqliteStore implements DBStore {
|
|
|
95
78
|
`);
|
|
96
79
|
|
|
97
80
|
try {
|
|
98
|
-
this.db.exec(
|
|
99
|
-
|
|
100
|
-
|
|
81
|
+
this.db.exec(`ALTER TABLE ferret_contracts ADD COLUMN shape_schema TEXT NOT NULL DEFAULT '{}';`);
|
|
82
|
+
} catch {}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
this.db.exec(`ALTER TABLE ferret_contracts ADD COLUMN code_source_file TEXT;`);
|
|
86
|
+
} catch {}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
this.db.exec(`ALTER TABLE ferret_contracts ADD COLUMN code_source_symbol TEXT;`);
|
|
101
90
|
} catch {}
|
|
102
91
|
}
|
|
103
92
|
|
|
@@ -109,9 +98,7 @@ export class SqliteStore implements DBStore {
|
|
|
109
98
|
}
|
|
110
99
|
|
|
111
100
|
async getNodeByFilePath(filePath: string): Promise<FerretNode | null> {
|
|
112
|
-
const row = this.db!.prepare(
|
|
113
|
-
"SELECT * FROM ferret_nodes WHERE file_path = ?",
|
|
114
|
-
).get(filePath) as any;
|
|
101
|
+
const row = this.db!.prepare('SELECT * FROM ferret_nodes WHERE file_path = ?').get(filePath) as any;
|
|
115
102
|
return row ?? null;
|
|
116
103
|
}
|
|
117
104
|
|
|
@@ -130,22 +117,22 @@ export class SqliteStore implements DBStore {
|
|
|
130
117
|
}
|
|
131
118
|
|
|
132
119
|
async getAllContractIds(): Promise<string[]> {
|
|
133
|
-
return (
|
|
134
|
-
this.db!.prepare("SELECT id FROM ferret_contracts").all() as any[]
|
|
135
|
-
).map((r) => r.id);
|
|
120
|
+
return (this.db!.prepare('SELECT id FROM ferret_contracts').all() as any[]).map((r) => r.id);
|
|
136
121
|
}
|
|
137
122
|
|
|
138
123
|
async upsertContract(contract: FerretContract): Promise<void> {
|
|
139
124
|
this.db!.prepare(
|
|
140
125
|
`
|
|
141
|
-
INSERT INTO ferret_contracts (id, node_id, shape_hash, shape_schema, type, status)
|
|
142
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
126
|
+
INSERT INTO ferret_contracts (id, node_id, shape_hash, shape_schema, type, status, code_source_file, code_source_symbol)
|
|
127
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
143
128
|
ON CONFLICT(id) DO UPDATE SET
|
|
144
129
|
node_id = excluded.node_id,
|
|
145
130
|
shape_hash = excluded.shape_hash,
|
|
146
131
|
shape_schema = excluded.shape_schema,
|
|
147
132
|
type = excluded.type,
|
|
148
|
-
status = excluded.status
|
|
133
|
+
status = excluded.status,
|
|
134
|
+
code_source_file = excluded.code_source_file,
|
|
135
|
+
code_source_symbol = excluded.code_source_symbol
|
|
149
136
|
`,
|
|
150
137
|
).run(
|
|
151
138
|
contract.id,
|
|
@@ -154,6 +141,8 @@ export class SqliteStore implements DBStore {
|
|
|
154
141
|
contract.shape_schema,
|
|
155
142
|
contract.type,
|
|
156
143
|
contract.status,
|
|
144
|
+
contract.code_source_file ?? null,
|
|
145
|
+
contract.code_source_symbol ?? null,
|
|
157
146
|
);
|
|
158
147
|
}
|
|
159
148
|
|
|
@@ -164,21 +153,12 @@ export class SqliteStore implements DBStore {
|
|
|
164
153
|
VALUES (?, ?, ?)
|
|
165
154
|
ON CONFLICT(id) DO NOTHING
|
|
166
155
|
`,
|
|
167
|
-
).run(
|
|
168
|
-
dependency.id,
|
|
169
|
-
dependency.source_node_id,
|
|
170
|
-
dependency.target_contract_id,
|
|
171
|
-
);
|
|
156
|
+
).run(dependency.id, dependency.source_node_id, dependency.target_contract_id);
|
|
172
157
|
}
|
|
173
158
|
|
|
174
|
-
async replaceDependenciesForSourceNode(
|
|
175
|
-
sourceNodeId: string,
|
|
176
|
-
targetContractIds: string[],
|
|
177
|
-
): Promise<void> {
|
|
159
|
+
async replaceDependenciesForSourceNode(sourceNodeId: string, targetContractIds: string[]): Promise<void> {
|
|
178
160
|
const uniqueTargets = [...new Set(targetContractIds)].sort();
|
|
179
|
-
const deleteStatement = this.db!.prepare(
|
|
180
|
-
"DELETE FROM ferret_dependencies WHERE source_node_id = ?",
|
|
181
|
-
);
|
|
161
|
+
const deleteStatement = this.db!.prepare('DELETE FROM ferret_dependencies WHERE source_node_id = ?');
|
|
182
162
|
const insertStatement = this.db!.prepare(
|
|
183
163
|
`
|
|
184
164
|
INSERT INTO ferret_dependencies (id, source_node_id, target_contract_id)
|
|
@@ -197,40 +177,28 @@ export class SqliteStore implements DBStore {
|
|
|
197
177
|
}
|
|
198
178
|
|
|
199
179
|
async getNodes(): Promise<FerretNode[]> {
|
|
200
|
-
return this.db!.prepare(
|
|
201
|
-
"SELECT * FROM ferret_nodes",
|
|
202
|
-
).all() as unknown as FerretNode[];
|
|
180
|
+
return this.db!.prepare('SELECT * FROM ferret_nodes').all() as unknown as FerretNode[];
|
|
203
181
|
}
|
|
204
182
|
|
|
205
183
|
async getNodesByStatus(status: NodeStatus): Promise<FerretNode[]> {
|
|
206
|
-
return this.db!.prepare(
|
|
207
|
-
status,
|
|
208
|
-
) as unknown as FerretNode[];
|
|
184
|
+
return this.db!.prepare('SELECT * FROM ferret_nodes WHERE status = ?').all(status) as unknown as FerretNode[];
|
|
209
185
|
}
|
|
210
186
|
|
|
211
187
|
async getContracts(): Promise<FerretContract[]> {
|
|
212
|
-
return this.db!.prepare(
|
|
213
|
-
"SELECT * FROM ferret_contracts",
|
|
214
|
-
).all() as unknown as FerretContract[];
|
|
188
|
+
return this.db!.prepare('SELECT * FROM ferret_contracts').all() as unknown as FerretContract[];
|
|
215
189
|
}
|
|
216
190
|
|
|
217
191
|
async getContract(id: string): Promise<FerretContract | null> {
|
|
218
|
-
const row = this.db!.prepare(
|
|
219
|
-
"SELECT * FROM ferret_contracts WHERE id = ?",
|
|
220
|
-
).get(id) as any;
|
|
192
|
+
const row = this.db!.prepare('SELECT * FROM ferret_contracts WHERE id = ?').get(id) as any;
|
|
221
193
|
return row ?? null;
|
|
222
194
|
}
|
|
223
195
|
|
|
224
196
|
async getDependencies(): Promise<FerretDependency[]> {
|
|
225
|
-
return this.db!.prepare(
|
|
226
|
-
"SELECT * FROM ferret_dependencies",
|
|
227
|
-
).all() as unknown as FerretDependency[];
|
|
197
|
+
return this.db!.prepare('SELECT * FROM ferret_dependencies').all() as unknown as FerretDependency[];
|
|
228
198
|
}
|
|
229
199
|
|
|
230
200
|
async updateNodeStatus(nodeId: string, status: NodeStatus): Promise<void> {
|
|
231
|
-
this.db!.prepare(
|
|
232
|
-
"UPDATE ferret_nodes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
|
233
|
-
).run(status, nodeId);
|
|
201
|
+
this.db!.prepare('UPDATE ferret_nodes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(status, nodeId);
|
|
234
202
|
}
|
|
235
203
|
|
|
236
204
|
async insertReconciliationLog(log: FerretReconciliationLog): Promise<void> {
|
|
@@ -239,40 +207,25 @@ export class SqliteStore implements DBStore {
|
|
|
239
207
|
INSERT INTO ferret_reconciliation_log (id, node_id, triggered_by, resolved_by, resolution_notes)
|
|
240
208
|
VALUES (?, ?, ?, ?, ?)
|
|
241
209
|
`,
|
|
242
|
-
).run(
|
|
243
|
-
log.id,
|
|
244
|
-
log.node_id,
|
|
245
|
-
log.triggered_by,
|
|
246
|
-
log.resolved_by ?? null,
|
|
247
|
-
log.resolution_notes ?? null,
|
|
248
|
-
);
|
|
210
|
+
).run(log.id, log.node_id, log.triggered_by, log.resolved_by ?? null, log.resolution_notes ?? null);
|
|
249
211
|
}
|
|
250
212
|
|
|
251
213
|
async getReconciliationLogs(): Promise<FerretReconciliationLog[]> {
|
|
252
214
|
return this.db!.prepare(
|
|
253
|
-
|
|
215
|
+
'SELECT id, node_id, triggered_by, resolved_by, resolution_notes FROM ferret_reconciliation_log',
|
|
254
216
|
).all() as unknown as FerretReconciliationLog[];
|
|
255
217
|
}
|
|
256
218
|
|
|
257
|
-
async insertPlacementDecision(
|
|
258
|
-
decision: FerretPlacementDecision,
|
|
259
|
-
): Promise<void> {
|
|
219
|
+
async insertPlacementDecision(decision: FerretPlacementDecision): Promise<void> {
|
|
260
220
|
this.db!.prepare(
|
|
261
221
|
`
|
|
262
222
|
INSERT INTO ferret_placement_decisions (id, node_id, placed_by, reasoning)
|
|
263
223
|
VALUES (?, ?, ?, ?)
|
|
264
224
|
`,
|
|
265
|
-
).run(
|
|
266
|
-
decision.id,
|
|
267
|
-
decision.node_id,
|
|
268
|
-
decision.placed_by,
|
|
269
|
-
decision.reasoning ?? null,
|
|
270
|
-
);
|
|
225
|
+
).run(decision.id, decision.node_id, decision.placed_by, decision.reasoning ?? null);
|
|
271
226
|
}
|
|
272
227
|
|
|
273
228
|
async getPlacementDecisions(): Promise<FerretPlacementDecision[]> {
|
|
274
|
-
return this.db!.prepare(
|
|
275
|
-
"SELECT id, node_id, placed_by, reasoning FROM ferret_placement_decisions",
|
|
276
|
-
).all() as unknown as FerretPlacementDecision[];
|
|
229
|
+
return this.db!.prepare('SELECT id, node_id, placed_by, reasoning FROM ferret_placement_decisions').all() as unknown as FerretPlacementDecision[];
|
|
277
230
|
}
|
|
278
231
|
}
|
package/src/store/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type NodeStatus =
|
|
2
|
-
export type ContractStatus =
|
|
1
|
+
export type NodeStatus = 'stable' | 'needs-review' | 'roadmap' | 'blocked';
|
|
2
|
+
export type ContractStatus = 'stable' | 'roadmap' | 'needs-review';
|
|
3
3
|
|
|
4
4
|
export interface FerretNode {
|
|
5
5
|
id: string;
|
|
@@ -16,6 +16,10 @@ export interface FerretContract {
|
|
|
16
16
|
shape_schema: string;
|
|
17
17
|
type: string;
|
|
18
18
|
status: ContractStatus;
|
|
19
|
+
/** Path to the TypeScript file this contract was generated from (code-first contracts only). */
|
|
20
|
+
code_source_file?: string;
|
|
21
|
+
/** TypeScript symbol name this contract was generated from (code-first contracts only). */
|
|
22
|
+
code_source_symbol?: string;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export interface FerretDependency {
|
|
@@ -73,10 +77,7 @@ export interface DBStore {
|
|
|
73
77
|
upsertDependency(dependency: FerretDependency): Promise<void>;
|
|
74
78
|
|
|
75
79
|
/** Replaces all dependency edges for a source node with the provided targets */
|
|
76
|
-
replaceDependenciesForSourceNode(
|
|
77
|
-
sourceNodeId: string,
|
|
78
|
-
targetContractIds: string[],
|
|
79
|
-
): Promise<void>;
|
|
80
|
+
replaceDependenciesForSourceNode(sourceNodeId: string, targetContractIds: string[]): Promise<void>;
|
|
80
81
|
|
|
81
82
|
/** Full graph retrieval methods for the Reconciler engine */
|
|
82
83
|
getNodes(): Promise<FerretNode[]>;
|