@specferret/core 0.1.1 → 0.1.3
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/README.md +62 -25
- package/dist/store/sqlite.js +72 -72
- package/package.json +44 -44
package/README.md
CHANGED
|
@@ -1,25 +1,62 @@
|
|
|
1
|
-
# @specferret/core
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
# @specferret/core
|
|
2
|
+
|
|
3
|
+
Core engine for SpecFerret contract extraction, validation, graph reconciliation, and drift analysis.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@specferret/core)
|
|
6
|
+
[](../../LICENSE)
|
|
7
|
+
|
|
8
|
+
## What this package is for
|
|
9
|
+
|
|
10
|
+
`@specferret/core` is the runtime library behind the CLI. Use it when you want to embed SpecFerret behavior in scripts, custom tooling, or internal workflows.
|
|
11
|
+
|
|
12
|
+
It includes:
|
|
13
|
+
|
|
14
|
+
- Store implementations (`sqlite`, `postgres`)
|
|
15
|
+
- Frontmatter extraction and schema validation
|
|
16
|
+
- Reconciler logic for direct/transitive impact
|
|
17
|
+
- Context graph generation utilities
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm i @specferret/core
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Runtime requirements
|
|
26
|
+
|
|
27
|
+
- Bun 1.0+
|
|
28
|
+
|
|
29
|
+
SpecFerret core is Bun-first and uses Bun-compatible runtime behavior.
|
|
30
|
+
|
|
31
|
+
## Typical consumers
|
|
32
|
+
|
|
33
|
+
- Internal platform tooling that needs drift checks in-process
|
|
34
|
+
- CI wrappers that need machine-readable drift metadata
|
|
35
|
+
- Custom developer workflows on top of SpecFerret reconciliation data
|
|
36
|
+
|
|
37
|
+
## Validation status
|
|
38
|
+
|
|
39
|
+
Core behavior is validated through the Sprint 5 external branch-matrix harness (spec-kit + BMAD), including explicit assertions for:
|
|
40
|
+
|
|
41
|
+
- S40 transitive depth thresholds
|
|
42
|
+
- S41 review proof artifacts
|
|
43
|
+
- S42 extract determinism
|
|
44
|
+
|
|
45
|
+
Latest green runs:
|
|
46
|
+
|
|
47
|
+
- spec-kit: https://github.com/BenGardiner123/spec-ferret-validation-spec-kit/actions/runs/23962649755
|
|
48
|
+
- BMAD: https://github.com/BenGardiner123/specferret-validation-bmad/actions/runs/23962652352
|
|
49
|
+
|
|
50
|
+
## Looking for the CLI?
|
|
51
|
+
|
|
52
|
+
Install `@specferret/cli` if you want the end-user command interface:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bun install -g @specferret/cli
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Links
|
|
59
|
+
|
|
60
|
+
- Source: https://github.com/BenGardiner123/spec-ferret
|
|
61
|
+
- Docs: https://specferret.dev
|
|
62
|
+
- CLI package: https://www.npmjs.com/package/@specferret/cli
|
package/dist/store/sqlite.js
CHANGED
|
@@ -33,48 +33,48 @@ export class SqliteStore {
|
|
|
33
33
|
this.db = new Database(this.dbPath);
|
|
34
34
|
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
35
35
|
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
36
|
-
this.db.exec(`
|
|
37
|
-
CREATE TABLE IF NOT EXISTS ferret_nodes (
|
|
38
|
-
id TEXT PRIMARY KEY,
|
|
39
|
-
file_path TEXT NOT NULL,
|
|
40
|
-
hash TEXT NOT NULL,
|
|
41
|
-
status TEXT NOT NULL,
|
|
42
|
-
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
CREATE TABLE IF NOT EXISTS ferret_contracts (
|
|
46
|
-
id TEXT PRIMARY KEY,
|
|
47
|
-
node_id TEXT NOT NULL,
|
|
48
|
-
shape_hash TEXT NOT NULL,
|
|
49
|
-
shape_schema TEXT NOT NULL DEFAULT '{}',
|
|
50
|
-
type TEXT NOT NULL,
|
|
51
|
-
status TEXT NOT NULL,
|
|
52
|
-
FOREIGN KEY (node_id) REFERENCES ferret_nodes(id)
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
CREATE TABLE IF NOT EXISTS ferret_dependencies (
|
|
56
|
-
id TEXT PRIMARY KEY,
|
|
57
|
-
source_node_id TEXT NOT NULL,
|
|
58
|
-
target_contract_id TEXT NOT NULL,
|
|
59
|
-
FOREIGN KEY (source_node_id) REFERENCES ferret_nodes(id)
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
CREATE TABLE IF NOT EXISTS ferret_reconciliation_log (
|
|
63
|
-
id TEXT PRIMARY KEY,
|
|
64
|
-
node_id TEXT NOT NULL,
|
|
65
|
-
triggered_by TEXT NOT NULL,
|
|
66
|
-
resolved_by TEXT,
|
|
67
|
-
resolution_notes TEXT,
|
|
68
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
CREATE TABLE IF NOT EXISTS ferret_placement_decisions (
|
|
72
|
-
id TEXT PRIMARY KEY,
|
|
73
|
-
node_id TEXT NOT NULL,
|
|
74
|
-
placed_by TEXT NOT NULL,
|
|
75
|
-
reasoning TEXT,
|
|
76
|
-
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
77
|
-
);
|
|
36
|
+
this.db.exec(`
|
|
37
|
+
CREATE TABLE IF NOT EXISTS ferret_nodes (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
file_path TEXT NOT NULL,
|
|
40
|
+
hash TEXT NOT NULL,
|
|
41
|
+
status TEXT NOT NULL,
|
|
42
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS ferret_contracts (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
node_id TEXT NOT NULL,
|
|
48
|
+
shape_hash TEXT NOT NULL,
|
|
49
|
+
shape_schema TEXT NOT NULL DEFAULT '{}',
|
|
50
|
+
type TEXT NOT NULL,
|
|
51
|
+
status TEXT NOT NULL,
|
|
52
|
+
FOREIGN KEY (node_id) REFERENCES ferret_nodes(id)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS ferret_dependencies (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
source_node_id TEXT NOT NULL,
|
|
58
|
+
target_contract_id TEXT NOT NULL,
|
|
59
|
+
FOREIGN KEY (source_node_id) REFERENCES ferret_nodes(id)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS ferret_reconciliation_log (
|
|
63
|
+
id TEXT PRIMARY KEY,
|
|
64
|
+
node_id TEXT NOT NULL,
|
|
65
|
+
triggered_by TEXT NOT NULL,
|
|
66
|
+
resolved_by TEXT,
|
|
67
|
+
resolution_notes TEXT,
|
|
68
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS ferret_placement_decisions (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
node_id TEXT NOT NULL,
|
|
74
|
+
placed_by TEXT NOT NULL,
|
|
75
|
+
reasoning TEXT,
|
|
76
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
77
|
+
);
|
|
78
78
|
`);
|
|
79
79
|
try {
|
|
80
80
|
this.db.exec(`ALTER TABLE ferret_contracts ADD COLUMN shape_schema TEXT NOT NULL DEFAULT '{}';`);
|
|
@@ -92,44 +92,44 @@ export class SqliteStore {
|
|
|
92
92
|
return row ?? null;
|
|
93
93
|
}
|
|
94
94
|
async upsertNode(node) {
|
|
95
|
-
this.db.prepare(`
|
|
96
|
-
INSERT INTO ferret_nodes (id, file_path, hash, status)
|
|
97
|
-
VALUES (?, ?, ?, ?)
|
|
98
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
99
|
-
file_path = excluded.file_path,
|
|
100
|
-
hash = excluded.hash,
|
|
101
|
-
status = excluded.status,
|
|
102
|
-
updated_at = CURRENT_TIMESTAMP
|
|
95
|
+
this.db.prepare(`
|
|
96
|
+
INSERT INTO ferret_nodes (id, file_path, hash, status)
|
|
97
|
+
VALUES (?, ?, ?, ?)
|
|
98
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
99
|
+
file_path = excluded.file_path,
|
|
100
|
+
hash = excluded.hash,
|
|
101
|
+
status = excluded.status,
|
|
102
|
+
updated_at = CURRENT_TIMESTAMP
|
|
103
103
|
`).run(node.id, node.file_path, node.hash, node.status);
|
|
104
104
|
}
|
|
105
105
|
async getAllContractIds() {
|
|
106
106
|
return this.db.prepare("SELECT id FROM ferret_contracts").all().map((r) => r.id);
|
|
107
107
|
}
|
|
108
108
|
async upsertContract(contract) {
|
|
109
|
-
this.db.prepare(`
|
|
110
|
-
INSERT INTO ferret_contracts (id, node_id, shape_hash, shape_schema, type, status)
|
|
111
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
112
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
113
|
-
node_id = excluded.node_id,
|
|
114
|
-
shape_hash = excluded.shape_hash,
|
|
115
|
-
shape_schema = excluded.shape_schema,
|
|
116
|
-
type = excluded.type,
|
|
117
|
-
status = excluded.status
|
|
109
|
+
this.db.prepare(`
|
|
110
|
+
INSERT INTO ferret_contracts (id, node_id, shape_hash, shape_schema, type, status)
|
|
111
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
112
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
113
|
+
node_id = excluded.node_id,
|
|
114
|
+
shape_hash = excluded.shape_hash,
|
|
115
|
+
shape_schema = excluded.shape_schema,
|
|
116
|
+
type = excluded.type,
|
|
117
|
+
status = excluded.status
|
|
118
118
|
`).run(contract.id, contract.node_id, contract.shape_hash, contract.shape_schema, contract.type, contract.status);
|
|
119
119
|
}
|
|
120
120
|
async upsertDependency(dependency) {
|
|
121
|
-
this.db.prepare(`
|
|
122
|
-
INSERT INTO ferret_dependencies (id, source_node_id, target_contract_id)
|
|
123
|
-
VALUES (?, ?, ?)
|
|
124
|
-
ON CONFLICT(id) DO NOTHING
|
|
121
|
+
this.db.prepare(`
|
|
122
|
+
INSERT INTO ferret_dependencies (id, source_node_id, target_contract_id)
|
|
123
|
+
VALUES (?, ?, ?)
|
|
124
|
+
ON CONFLICT(id) DO NOTHING
|
|
125
125
|
`).run(dependency.id, dependency.source_node_id, dependency.target_contract_id);
|
|
126
126
|
}
|
|
127
127
|
async replaceDependenciesForSourceNode(sourceNodeId, targetContractIds) {
|
|
128
128
|
const uniqueTargets = [...new Set(targetContractIds)].sort();
|
|
129
129
|
const deleteStatement = this.db.prepare("DELETE FROM ferret_dependencies WHERE source_node_id = ?");
|
|
130
|
-
const insertStatement = this.db.prepare(`
|
|
131
|
-
INSERT INTO ferret_dependencies (id, source_node_id, target_contract_id)
|
|
132
|
-
VALUES (?, ?, ?)
|
|
130
|
+
const insertStatement = this.db.prepare(`
|
|
131
|
+
INSERT INTO ferret_dependencies (id, source_node_id, target_contract_id)
|
|
132
|
+
VALUES (?, ?, ?)
|
|
133
133
|
`);
|
|
134
134
|
const transaction = this.db.transaction((sourceId) => {
|
|
135
135
|
deleteStatement.run(sourceId);
|
|
@@ -159,18 +159,18 @@ export class SqliteStore {
|
|
|
159
159
|
this.db.prepare("UPDATE ferret_nodes SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(status, nodeId);
|
|
160
160
|
}
|
|
161
161
|
async insertReconciliationLog(log) {
|
|
162
|
-
this.db.prepare(`
|
|
163
|
-
INSERT INTO ferret_reconciliation_log (id, node_id, triggered_by, resolved_by, resolution_notes)
|
|
164
|
-
VALUES (?, ?, ?, ?, ?)
|
|
162
|
+
this.db.prepare(`
|
|
163
|
+
INSERT INTO ferret_reconciliation_log (id, node_id, triggered_by, resolved_by, resolution_notes)
|
|
164
|
+
VALUES (?, ?, ?, ?, ?)
|
|
165
165
|
`).run(log.id, log.node_id, log.triggered_by, log.resolved_by ?? null, log.resolution_notes ?? null);
|
|
166
166
|
}
|
|
167
167
|
async getReconciliationLogs() {
|
|
168
168
|
return this.db.prepare("SELECT id, node_id, triggered_by, resolved_by, resolution_notes FROM ferret_reconciliation_log").all();
|
|
169
169
|
}
|
|
170
170
|
async insertPlacementDecision(decision) {
|
|
171
|
-
this.db.prepare(`
|
|
172
|
-
INSERT INTO ferret_placement_decisions (id, node_id, placed_by, reasoning)
|
|
173
|
-
VALUES (?, ?, ?, ?)
|
|
171
|
+
this.db.prepare(`
|
|
172
|
+
INSERT INTO ferret_placement_decisions (id, node_id, placed_by, reasoning)
|
|
173
|
+
VALUES (?, ?, ?, ?)
|
|
174
174
|
`).run(decision.id, decision.node_id, decision.placed_by, decision.reasoning ?? null);
|
|
175
175
|
}
|
|
176
176
|
async getPlacementDecisions() {
|
package/package.json
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@specferret/core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "SpecFerret core engine — spec drift detection.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"bun": "./
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"types": "./dist/index.d.ts"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist/",
|
|
17
|
-
"README.md",
|
|
18
|
-
"LICENSE"
|
|
19
|
-
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "bun ../../scripts/clean-dist.ts dist && tsc --project tsconfig.build.json",
|
|
22
|
-
"test": "bun test"
|
|
23
|
-
},
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"ajv": "^8.12.0",
|
|
26
|
-
"ajv-formats": "^2.1.1",
|
|
27
|
-
"glob": "^13.0.6",
|
|
28
|
-
"gray-matter": "^4.0.3",
|
|
29
|
-
"zod": "^3.22.4"
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"@types/bun": "^1.3.11",
|
|
33
|
-
"typescript": "^6.0.2"
|
|
34
|
-
},
|
|
35
|
-
"engines": {
|
|
36
|
-
"bun": ">=1.0.0"
|
|
37
|
-
},
|
|
38
|
-
"license": "MIT",
|
|
39
|
-
"repository": {
|
|
40
|
-
"type": "git",
|
|
41
|
-
"url": "git+https://github.com/BenGardiner123/spec-ferret.git"
|
|
42
|
-
},
|
|
43
|
-
"homepage": "https://specferret.dev"
|
|
44
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@specferret/core",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "SpecFerret core engine — spec drift detection.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"bun": "./dist/index.js",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "bun ../../scripts/clean-dist.ts dist && tsc --project tsconfig.build.json",
|
|
22
|
+
"test": "bun test"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"ajv": "^8.12.0",
|
|
26
|
+
"ajv-formats": "^2.1.1",
|
|
27
|
+
"glob": "^13.0.6",
|
|
28
|
+
"gray-matter": "^4.0.3",
|
|
29
|
+
"zod": "^3.22.4"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/bun": "^1.3.11",
|
|
33
|
+
"typescript": "^6.0.2"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"bun": ">=1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/BenGardiner123/spec-ferret.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://specferret.dev"
|
|
44
|
+
}
|