@opennodes/registry 0.1.0 → 0.1.2
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 +21 -21
- package/bin.js +25 -24
- package/package.json +49 -18
- package/src/importers.js +346 -346
- package/src/mcp.js +182 -182
- package/src/reimport.js +55 -55
- package/src/search.js +123 -118
- package/src/server.js +392 -391
- package/src/stageb.js +19 -8
- package/src/store-pg.js +127 -127
- package/src/store.js +126 -126
- package/src/web/index.html +1794 -1778
package/src/stageb.js
CHANGED
|
@@ -12,7 +12,7 @@ export const FINGERPRINT_PROMPTS = [
|
|
|
12
12
|
'ONP identity probe 3: repeat exactly: basalt-7301',
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
-
async function chatOnce(safeFetch, card, offering, content, maxTokens = 64) {
|
|
15
|
+
async function chatOnce(safeFetch, card, offering, content, maxTokens = 64, timeoutMs = 60_000) {
|
|
16
16
|
const t0 = performance.now();
|
|
17
17
|
const { status, body } = await fetchJsonCapped(safeFetch, `${card.endpoints.openai}/chat/completions`, {
|
|
18
18
|
method: 'POST',
|
|
@@ -27,7 +27,7 @@ async function chatOnce(safeFetch, card, offering, content, maxTokens = 64) {
|
|
|
27
27
|
temperature: 0,
|
|
28
28
|
max_tokens: maxTokens,
|
|
29
29
|
}),
|
|
30
|
-
},
|
|
30
|
+
}, timeoutMs);
|
|
31
31
|
return {
|
|
32
32
|
status,
|
|
33
33
|
text: body?.choices?.[0]?.message?.content ?? null,
|
|
@@ -61,6 +61,14 @@ async function probeIdentity(safeFetch, card, offering, refs) {
|
|
|
61
61
|
return { ok: matches === refs.length, matches, total: refs.length };
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** A needle counts as retrieved if the reply carries the full token or just its digits (small models drop the prefix). */
|
|
65
|
+
export function needleFound(text, needle) {
|
|
66
|
+
if (typeof text !== 'string') return false;
|
|
67
|
+
if (text.includes(needle)) return true;
|
|
68
|
+
const digits = needle.split(':')[1];
|
|
69
|
+
return new RegExp(`(^|[^0-9])${digits}([^0-9]|$)`).test(text);
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
/** DS-ADM-03: needle retrieval at 25/50/90% of the claimed window; returns the verified cap. */
|
|
65
73
|
async function probeContext(safeFetch, card, offering) {
|
|
66
74
|
const claimed = offering.serving?.context_window ?? 4096;
|
|
@@ -76,12 +84,14 @@ async function probeContext(safeFetch, card, offering) {
|
|
|
76
84
|
+ ` ${needle} `
|
|
77
85
|
+ fillerUnit.repeat(Math.ceil(afterNeedle / fillerUnit.length)).slice(0, afterNeedle)
|
|
78
86
|
+ ' Return the NEEDLE token from this text.';
|
|
79
|
-
let found = false;
|
|
87
|
+
let found = false, why = null;
|
|
80
88
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
// Prefill on modest hardware is slow: allow ~30 ms per claimed token on top of the base timeout.
|
|
90
|
+
const { status, text } = await chatOnce(safeFetch, card, offering, prompt, 64, 60_000 + tokens * 30);
|
|
91
|
+
found = status === 200 && needleFound(text, needle);
|
|
92
|
+
if (!found) why = status !== 200 ? `status ${status}` : 'needle not in reply';
|
|
93
|
+
} catch (err) { found = false; why = /abort|timeout/i.test(String(err?.message)) ? 'timeout' : String(err?.message ?? err).slice(0, 80); }
|
|
94
|
+
results.push({ fraction, tokens, found, ...(why ? { why } : {}) });
|
|
85
95
|
if (found) cap = Math.max(cap, tokens);
|
|
86
96
|
}
|
|
87
97
|
const allPassed = results.every((r) => r.found);
|
|
@@ -145,7 +155,8 @@ export async function runStageB(store, nodeId, card, { safeFetch, capacity = {}
|
|
|
145
155
|
await store.recordProbe(nodeId, offering.offering_id, 'B', 'context', verdict.context.ok,
|
|
146
156
|
`claimed ${verdict.context.claimed}, verified ${verdict.context.verified_cap}`,
|
|
147
157
|
verdict.context);
|
|
148
|
-
if (!verdict.context.ok
|
|
158
|
+
if (!verdict.context.ok) {
|
|
159
|
+
// 0 = nothing verified: search projects the window as unverified rather than trusting the claim.
|
|
149
160
|
await store.setObservation(nodeId, offering.offering_id, 'context_cap', verdict.context.verified_cap);
|
|
150
161
|
}
|
|
151
162
|
|
package/src/store-pg.js
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
// Postgres storage driver (production). Same async contract as the sqlite driver.
|
|
2
|
-
// Accepts a connection string, or an injected pg-compatible Pool (DI for tests: pg-mem).
|
|
3
|
-
import pg from 'pg';
|
|
4
|
-
|
|
5
|
-
const SCHEMA = `
|
|
6
|
-
CREATE TABLE IF NOT EXISTS nodes (
|
|
7
|
-
id TEXT PRIMARY KEY, origin TEXT NOT NULL, card_url TEXT NOT NULL,
|
|
8
|
-
state TEXT NOT NULL, institutional INTEGER DEFAULT 0,
|
|
9
|
-
source TEXT NOT NULL DEFAULT 'registration',
|
|
10
|
-
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
11
|
-
);
|
|
12
|
-
CREATE TABLE IF NOT EXISTS card_revisions (
|
|
13
|
-
node_id TEXT NOT NULL, revision TEXT NOT NULL, raw TEXT NOT NULL,
|
|
14
|
-
sig_ok INTEGER NOT NULL, fetched_at TEXT NOT NULL,
|
|
15
|
-
PRIMARY KEY (node_id, revision)
|
|
16
|
-
);
|
|
17
|
-
CREATE TABLE IF NOT EXISTS challenges (
|
|
18
|
-
node_id TEXT PRIMARY KEY, token TEXT NOT NULL, expires_at TEXT NOT NULL
|
|
19
|
-
);
|
|
20
|
-
CREATE TABLE IF NOT EXISTS probes (
|
|
21
|
-
id SERIAL PRIMARY KEY, node_id TEXT NOT NULL, offering_id TEXT,
|
|
22
|
-
stage TEXT NOT NULL, kind TEXT NOT NULL, ok INTEGER NOT NULL,
|
|
23
|
-
detail TEXT, measured TEXT, at TEXT NOT NULL
|
|
24
|
-
);
|
|
25
|
-
CREATE TABLE IF NOT EXISTS transitions (
|
|
26
|
-
id SERIAL PRIMARY KEY, node_id TEXT NOT NULL,
|
|
27
|
-
from_state TEXT, to_state TEXT NOT NULL, reason TEXT, at TEXT NOT NULL
|
|
28
|
-
);
|
|
29
|
-
CREATE TABLE IF NOT EXISTS fingerprints (
|
|
30
|
-
artifact TEXT NOT NULL, prompt TEXT NOT NULL, expected_hash TEXT NOT NULL,
|
|
31
|
-
PRIMARY KEY (artifact, prompt)
|
|
32
|
-
);
|
|
33
|
-
CREATE TABLE IF NOT EXISTS observations (
|
|
34
|
-
node_id TEXT NOT NULL, offering_id TEXT NOT NULL, key TEXT NOT NULL,
|
|
35
|
-
value TEXT NOT NULL, at TEXT NOT NULL,
|
|
36
|
-
PRIMARY KEY (node_id, offering_id, key)
|
|
37
|
-
);
|
|
38
|
-
`;
|
|
39
|
-
|
|
40
|
-
export async function createPgStore({ connectionString, pool } = {}) {
|
|
41
|
-
const p = pool ?? new pg.Pool({ connectionString, max: 10 });
|
|
42
|
-
for (const stmt of SCHEMA.split(';').map((s) => s.trim()).filter(Boolean)) {
|
|
43
|
-
await p.query(stmt);
|
|
44
|
-
}
|
|
45
|
-
const now = () => new Date().toISOString();
|
|
46
|
-
const q = async (text, params = []) => (await p.query(text, params)).rows;
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
driver: 'postgres',
|
|
50
|
-
async upsertNode(id, origin, cardUrl, state, source = 'registration') {
|
|
51
|
-
await q(`INSERT INTO nodes (id, origin, card_url, state, source, created_at, updated_at)
|
|
52
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
53
|
-
ON CONFLICT (id) DO UPDATE SET origin=excluded.origin, card_url=excluded.card_url,
|
|
54
|
-
source=excluded.source, updated_at=excluded.updated_at`,
|
|
55
|
-
[id, origin, cardUrl, state, source, now(), now()]);
|
|
56
|
-
},
|
|
57
|
-
async getNode(id) {
|
|
58
|
-
return (await q('SELECT * FROM nodes WHERE id = $1', [id]))[0];
|
|
59
|
-
},
|
|
60
|
-
async setState(id, toState, reason = null) {
|
|
61
|
-
const cur = await this.getNode(id);
|
|
62
|
-
await q('UPDATE nodes SET state = $1, updated_at = $2 WHERE id = $3', [toState, now(), id]);
|
|
63
|
-
await q('INSERT INTO transitions (node_id, from_state, to_state, reason, at) VALUES ($1, $2, $3, $4, $5)',
|
|
64
|
-
[id, cur?.state ?? null, toState, reason, now()]);
|
|
65
|
-
},
|
|
66
|
-
async saveRevision(nodeId, revision, card, sigOk) {
|
|
67
|
-
await q(`INSERT INTO card_revisions (node_id, revision, raw, sig_ok, fetched_at)
|
|
68
|
-
VALUES ($1, $2, $3, $4, $5)
|
|
69
|
-
ON CONFLICT (node_id, revision) DO UPDATE SET raw=excluded.raw,
|
|
70
|
-
sig_ok=excluded.sig_ok, fetched_at=excluded.fetched_at`,
|
|
71
|
-
[nodeId, revision, JSON.stringify(card), sigOk ? 1 : 0, now()]);
|
|
72
|
-
},
|
|
73
|
-
async latestCard(nodeId) {
|
|
74
|
-
const rows = await q('SELECT raw FROM card_revisions WHERE node_id = $1 ORDER BY revision DESC LIMIT 1', [nodeId]);
|
|
75
|
-
return rows[0] ? JSON.parse(rows[0].raw) : null;
|
|
76
|
-
},
|
|
77
|
-
async setChallenge(nodeId, token, ttlMs = 72 * 3600 * 1000) {
|
|
78
|
-
await q(`INSERT INTO challenges (node_id, token, expires_at) VALUES ($1, $2, $3)
|
|
79
|
-
ON CONFLICT (node_id) DO UPDATE SET token=excluded.token, expires_at=excluded.expires_at`,
|
|
80
|
-
[nodeId, token, new Date(Date.now() + ttlMs).toISOString()]);
|
|
81
|
-
},
|
|
82
|
-
async getChallenge(nodeId) {
|
|
83
|
-
const row = (await q('SELECT * FROM challenges WHERE node_id = $1', [nodeId]))[0];
|
|
84
|
-
if (!row || row.expires_at < now()) return null;
|
|
85
|
-
return row;
|
|
86
|
-
},
|
|
87
|
-
async recordProbe(nodeId, offeringId, stage, kind, ok, detail = null, measured = null) {
|
|
88
|
-
await q('INSERT INTO probes (node_id, offering_id, stage, kind, ok, detail, measured, at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)',
|
|
89
|
-
[nodeId, offeringId, stage, kind, ok ? 1 : 0, detail, measured ? JSON.stringify(measured) : null, now()]);
|
|
90
|
-
},
|
|
91
|
-
async probesFor(nodeId) {
|
|
92
|
-
return q('SELECT * FROM probes WHERE node_id = $1 ORDER BY id DESC LIMIT 100', [nodeId]);
|
|
93
|
-
},
|
|
94
|
-
async transitionsFor(nodeId) {
|
|
95
|
-
return q('SELECT * FROM transitions WHERE node_id = $1 ORDER BY id ASC', [nodeId]);
|
|
96
|
-
},
|
|
97
|
-
async allNodes() { return q('SELECT * FROM nodes'); },
|
|
98
|
-
async recentTransitions(limit = 50) {
|
|
99
|
-
return q(`SELECT t.*, n.source FROM transitions t
|
|
100
|
-
LEFT JOIN nodes n ON n.id = t.node_id ORDER BY t.id DESC LIMIT $1`, [limit]);
|
|
101
|
-
},
|
|
102
|
-
async addFingerprints(artifact, refs) {
|
|
103
|
-
for (const ref of refs) {
|
|
104
|
-
await q(`INSERT INTO fingerprints (artifact, prompt, expected_hash) VALUES ($1, $2, $3)
|
|
105
|
-
ON CONFLICT (artifact, prompt) DO UPDATE SET expected_hash=excluded.expected_hash`,
|
|
106
|
-
[artifact, ref.prompt, ref.expected_hash]);
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
async getFingerprints(artifact) {
|
|
110
|
-
return q('SELECT prompt, expected_hash FROM fingerprints WHERE artifact = $1', [artifact]);
|
|
111
|
-
},
|
|
112
|
-
async setObservation(nodeId, offeringId, key, value) {
|
|
113
|
-
await q(`INSERT INTO observations (node_id, offering_id, key, value, at) VALUES ($1, $2, $3, $4, $5)
|
|
114
|
-
ON CONFLICT (node_id, offering_id, key) DO UPDATE SET value=excluded.value, at=excluded.at`,
|
|
115
|
-
[nodeId, offeringId, key, JSON.stringify(value), now()]);
|
|
116
|
-
},
|
|
117
|
-
async getObservations(nodeId, offeringId) {
|
|
118
|
-
const rows = await q('SELECT key, value FROM observations WHERE node_id = $1 AND offering_id = $2', [nodeId, offeringId]);
|
|
119
|
-
return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
|
|
120
|
-
},
|
|
121
|
-
async observationsForNode(nodeId) {
|
|
122
|
-
const rows = await q('SELECT offering_id, key, value FROM observations WHERE node_id = $1', [nodeId]);
|
|
123
|
-
return rows.map((r) => ({ offering_id: r.offering_id, key: r.key, value: JSON.parse(r.value) }));
|
|
124
|
-
},
|
|
125
|
-
async close() { await p.end(); },
|
|
126
|
-
};
|
|
127
|
-
}
|
|
1
|
+
// Postgres storage driver (production). Same async contract as the sqlite driver.
|
|
2
|
+
// Accepts a connection string, or an injected pg-compatible Pool (DI for tests: pg-mem).
|
|
3
|
+
import pg from 'pg';
|
|
4
|
+
|
|
5
|
+
const SCHEMA = `
|
|
6
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
7
|
+
id TEXT PRIMARY KEY, origin TEXT NOT NULL, card_url TEXT NOT NULL,
|
|
8
|
+
state TEXT NOT NULL, institutional INTEGER DEFAULT 0,
|
|
9
|
+
source TEXT NOT NULL DEFAULT 'registration',
|
|
10
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
11
|
+
);
|
|
12
|
+
CREATE TABLE IF NOT EXISTS card_revisions (
|
|
13
|
+
node_id TEXT NOT NULL, revision TEXT NOT NULL, raw TEXT NOT NULL,
|
|
14
|
+
sig_ok INTEGER NOT NULL, fetched_at TEXT NOT NULL,
|
|
15
|
+
PRIMARY KEY (node_id, revision)
|
|
16
|
+
);
|
|
17
|
+
CREATE TABLE IF NOT EXISTS challenges (
|
|
18
|
+
node_id TEXT PRIMARY KEY, token TEXT NOT NULL, expires_at TEXT NOT NULL
|
|
19
|
+
);
|
|
20
|
+
CREATE TABLE IF NOT EXISTS probes (
|
|
21
|
+
id SERIAL PRIMARY KEY, node_id TEXT NOT NULL, offering_id TEXT,
|
|
22
|
+
stage TEXT NOT NULL, kind TEXT NOT NULL, ok INTEGER NOT NULL,
|
|
23
|
+
detail TEXT, measured TEXT, at TEXT NOT NULL
|
|
24
|
+
);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS transitions (
|
|
26
|
+
id SERIAL PRIMARY KEY, node_id TEXT NOT NULL,
|
|
27
|
+
from_state TEXT, to_state TEXT NOT NULL, reason TEXT, at TEXT NOT NULL
|
|
28
|
+
);
|
|
29
|
+
CREATE TABLE IF NOT EXISTS fingerprints (
|
|
30
|
+
artifact TEXT NOT NULL, prompt TEXT NOT NULL, expected_hash TEXT NOT NULL,
|
|
31
|
+
PRIMARY KEY (artifact, prompt)
|
|
32
|
+
);
|
|
33
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
34
|
+
node_id TEXT NOT NULL, offering_id TEXT NOT NULL, key TEXT NOT NULL,
|
|
35
|
+
value TEXT NOT NULL, at TEXT NOT NULL,
|
|
36
|
+
PRIMARY KEY (node_id, offering_id, key)
|
|
37
|
+
);
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export async function createPgStore({ connectionString, pool } = {}) {
|
|
41
|
+
const p = pool ?? new pg.Pool({ connectionString, max: 10 });
|
|
42
|
+
for (const stmt of SCHEMA.split(';').map((s) => s.trim()).filter(Boolean)) {
|
|
43
|
+
await p.query(stmt);
|
|
44
|
+
}
|
|
45
|
+
const now = () => new Date().toISOString();
|
|
46
|
+
const q = async (text, params = []) => (await p.query(text, params)).rows;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
driver: 'postgres',
|
|
50
|
+
async upsertNode(id, origin, cardUrl, state, source = 'registration') {
|
|
51
|
+
await q(`INSERT INTO nodes (id, origin, card_url, state, source, created_at, updated_at)
|
|
52
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
53
|
+
ON CONFLICT (id) DO UPDATE SET origin=excluded.origin, card_url=excluded.card_url,
|
|
54
|
+
source=excluded.source, updated_at=excluded.updated_at`,
|
|
55
|
+
[id, origin, cardUrl, state, source, now(), now()]);
|
|
56
|
+
},
|
|
57
|
+
async getNode(id) {
|
|
58
|
+
return (await q('SELECT * FROM nodes WHERE id = $1', [id]))[0];
|
|
59
|
+
},
|
|
60
|
+
async setState(id, toState, reason = null) {
|
|
61
|
+
const cur = await this.getNode(id);
|
|
62
|
+
await q('UPDATE nodes SET state = $1, updated_at = $2 WHERE id = $3', [toState, now(), id]);
|
|
63
|
+
await q('INSERT INTO transitions (node_id, from_state, to_state, reason, at) VALUES ($1, $2, $3, $4, $5)',
|
|
64
|
+
[id, cur?.state ?? null, toState, reason, now()]);
|
|
65
|
+
},
|
|
66
|
+
async saveRevision(nodeId, revision, card, sigOk) {
|
|
67
|
+
await q(`INSERT INTO card_revisions (node_id, revision, raw, sig_ok, fetched_at)
|
|
68
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
69
|
+
ON CONFLICT (node_id, revision) DO UPDATE SET raw=excluded.raw,
|
|
70
|
+
sig_ok=excluded.sig_ok, fetched_at=excluded.fetched_at`,
|
|
71
|
+
[nodeId, revision, JSON.stringify(card), sigOk ? 1 : 0, now()]);
|
|
72
|
+
},
|
|
73
|
+
async latestCard(nodeId) {
|
|
74
|
+
const rows = await q('SELECT raw FROM card_revisions WHERE node_id = $1 ORDER BY revision DESC LIMIT 1', [nodeId]);
|
|
75
|
+
return rows[0] ? JSON.parse(rows[0].raw) : null;
|
|
76
|
+
},
|
|
77
|
+
async setChallenge(nodeId, token, ttlMs = 72 * 3600 * 1000) {
|
|
78
|
+
await q(`INSERT INTO challenges (node_id, token, expires_at) VALUES ($1, $2, $3)
|
|
79
|
+
ON CONFLICT (node_id) DO UPDATE SET token=excluded.token, expires_at=excluded.expires_at`,
|
|
80
|
+
[nodeId, token, new Date(Date.now() + ttlMs).toISOString()]);
|
|
81
|
+
},
|
|
82
|
+
async getChallenge(nodeId) {
|
|
83
|
+
const row = (await q('SELECT * FROM challenges WHERE node_id = $1', [nodeId]))[0];
|
|
84
|
+
if (!row || row.expires_at < now()) return null;
|
|
85
|
+
return row;
|
|
86
|
+
},
|
|
87
|
+
async recordProbe(nodeId, offeringId, stage, kind, ok, detail = null, measured = null) {
|
|
88
|
+
await q('INSERT INTO probes (node_id, offering_id, stage, kind, ok, detail, measured, at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)',
|
|
89
|
+
[nodeId, offeringId, stage, kind, ok ? 1 : 0, detail, measured ? JSON.stringify(measured) : null, now()]);
|
|
90
|
+
},
|
|
91
|
+
async probesFor(nodeId) {
|
|
92
|
+
return q('SELECT * FROM probes WHERE node_id = $1 ORDER BY id DESC LIMIT 100', [nodeId]);
|
|
93
|
+
},
|
|
94
|
+
async transitionsFor(nodeId) {
|
|
95
|
+
return q('SELECT * FROM transitions WHERE node_id = $1 ORDER BY id ASC', [nodeId]);
|
|
96
|
+
},
|
|
97
|
+
async allNodes() { return q('SELECT * FROM nodes'); },
|
|
98
|
+
async recentTransitions(limit = 50) {
|
|
99
|
+
return q(`SELECT t.*, n.source FROM transitions t
|
|
100
|
+
LEFT JOIN nodes n ON n.id = t.node_id ORDER BY t.id DESC LIMIT $1`, [limit]);
|
|
101
|
+
},
|
|
102
|
+
async addFingerprints(artifact, refs) {
|
|
103
|
+
for (const ref of refs) {
|
|
104
|
+
await q(`INSERT INTO fingerprints (artifact, prompt, expected_hash) VALUES ($1, $2, $3)
|
|
105
|
+
ON CONFLICT (artifact, prompt) DO UPDATE SET expected_hash=excluded.expected_hash`,
|
|
106
|
+
[artifact, ref.prompt, ref.expected_hash]);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
async getFingerprints(artifact) {
|
|
110
|
+
return q('SELECT prompt, expected_hash FROM fingerprints WHERE artifact = $1', [artifact]);
|
|
111
|
+
},
|
|
112
|
+
async setObservation(nodeId, offeringId, key, value) {
|
|
113
|
+
await q(`INSERT INTO observations (node_id, offering_id, key, value, at) VALUES ($1, $2, $3, $4, $5)
|
|
114
|
+
ON CONFLICT (node_id, offering_id, key) DO UPDATE SET value=excluded.value, at=excluded.at`,
|
|
115
|
+
[nodeId, offeringId, key, JSON.stringify(value), now()]);
|
|
116
|
+
},
|
|
117
|
+
async getObservations(nodeId, offeringId) {
|
|
118
|
+
const rows = await q('SELECT key, value FROM observations WHERE node_id = $1 AND offering_id = $2', [nodeId, offeringId]);
|
|
119
|
+
return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
|
|
120
|
+
},
|
|
121
|
+
async observationsForNode(nodeId) {
|
|
122
|
+
const rows = await q('SELECT offering_id, key, value FROM observations WHERE node_id = $1', [nodeId]);
|
|
123
|
+
return rows.map((r) => ({ offering_id: r.offering_id, key: r.key, value: JSON.parse(r.value) }));
|
|
124
|
+
},
|
|
125
|
+
async close() { await p.end(); },
|
|
126
|
+
};
|
|
127
|
+
}
|
package/src/store.js
CHANGED
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
// Registry storage. The async store API is the seam between drivers:
|
|
2
|
-
// - sqlite (node:sqlite): zero-setup dev/tests, file or :memory:
|
|
3
|
-
// - postgres (store-pg.js): production, selected by a postgres:// URL
|
|
4
|
-
// Every method is async so both drivers present the identical contract.
|
|
5
|
-
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
-
|
|
7
|
-
/** config: ':memory:' | sqlite file path | postgres:// URL */
|
|
8
|
-
export async function createStore(config = ':memory:') {
|
|
9
|
-
if (typeof config === 'string' && /^postgres(ql)?:\/\//.test(config)) {
|
|
10
|
-
const { createPgStore } = await import('./store-pg.js');
|
|
11
|
-
return createPgStore({ connectionString: config });
|
|
12
|
-
}
|
|
13
|
-
return createSqliteStore(config);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function createSqliteStore(path = ':memory:') {
|
|
17
|
-
const db = new DatabaseSync(path);
|
|
18
|
-
db.exec(`
|
|
19
|
-
CREATE TABLE IF NOT EXISTS nodes (
|
|
20
|
-
id TEXT PRIMARY KEY, origin TEXT NOT NULL, card_url TEXT NOT NULL,
|
|
21
|
-
state TEXT NOT NULL, institutional INTEGER DEFAULT 0,
|
|
22
|
-
source TEXT NOT NULL DEFAULT 'registration',
|
|
23
|
-
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
24
|
-
);
|
|
25
|
-
CREATE TABLE IF NOT EXISTS card_revisions (
|
|
26
|
-
node_id TEXT NOT NULL, revision TEXT NOT NULL, raw TEXT NOT NULL,
|
|
27
|
-
sig_ok INTEGER NOT NULL, fetched_at TEXT NOT NULL,
|
|
28
|
-
PRIMARY KEY (node_id, revision)
|
|
29
|
-
);
|
|
30
|
-
CREATE TABLE IF NOT EXISTS challenges (
|
|
31
|
-
node_id TEXT PRIMARY KEY, token TEXT NOT NULL, expires_at TEXT NOT NULL
|
|
32
|
-
);
|
|
33
|
-
CREATE TABLE IF NOT EXISTS probes (
|
|
34
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, node_id TEXT NOT NULL, offering_id TEXT,
|
|
35
|
-
stage TEXT NOT NULL, kind TEXT NOT NULL, ok INTEGER NOT NULL,
|
|
36
|
-
detail TEXT, measured TEXT, at TEXT NOT NULL
|
|
37
|
-
);
|
|
38
|
-
CREATE TABLE IF NOT EXISTS transitions (
|
|
39
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, node_id TEXT NOT NULL,
|
|
40
|
-
from_state TEXT, to_state TEXT NOT NULL, reason TEXT, at TEXT NOT NULL
|
|
41
|
-
);
|
|
42
|
-
CREATE TABLE IF NOT EXISTS fingerprints (
|
|
43
|
-
artifact TEXT NOT NULL, prompt TEXT NOT NULL, expected_hash TEXT NOT NULL,
|
|
44
|
-
PRIMARY KEY (artifact, prompt)
|
|
45
|
-
);
|
|
46
|
-
CREATE TABLE IF NOT EXISTS observations (
|
|
47
|
-
node_id TEXT NOT NULL, offering_id TEXT NOT NULL, key TEXT NOT NULL,
|
|
48
|
-
value TEXT NOT NULL, at TEXT NOT NULL,
|
|
49
|
-
PRIMARY KEY (node_id, offering_id, key)
|
|
50
|
-
);
|
|
51
|
-
`);
|
|
52
|
-
|
|
53
|
-
const now = () => new Date().toISOString();
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
driver: 'sqlite',
|
|
57
|
-
async upsertNode(id, origin, cardUrl, state, source = 'registration') {
|
|
58
|
-
db.prepare(`INSERT INTO nodes (id, origin, card_url, state, source, created_at, updated_at)
|
|
59
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
60
|
-
ON CONFLICT(id) DO UPDATE SET origin=excluded.origin, card_url=excluded.card_url,
|
|
61
|
-
source=excluded.source, updated_at=excluded.updated_at`)
|
|
62
|
-
.run(id, origin, cardUrl, state, source, now(), now());
|
|
63
|
-
},
|
|
64
|
-
async getNode(id) { return db.prepare('SELECT * FROM nodes WHERE id = ?').get(id); },
|
|
65
|
-
async setState(id, toState, reason = null) {
|
|
66
|
-
const cur = await this.getNode(id);
|
|
67
|
-
db.prepare('UPDATE nodes SET state = ?, updated_at = ? WHERE id = ?').run(toState, now(), id);
|
|
68
|
-
db.prepare('INSERT INTO transitions (node_id, from_state, to_state, reason, at) VALUES (?, ?, ?, ?, ?)')
|
|
69
|
-
.run(id, cur?.state ?? null, toState, reason, now());
|
|
70
|
-
},
|
|
71
|
-
async saveRevision(nodeId, revision, card, sigOk) {
|
|
72
|
-
db.prepare(`INSERT OR REPLACE INTO card_revisions (node_id, revision, raw, sig_ok, fetched_at)
|
|
73
|
-
VALUES (?, ?, ?, ?, ?)`).run(nodeId, revision, JSON.stringify(card), sigOk ? 1 : 0, now());
|
|
74
|
-
},
|
|
75
|
-
async latestCard(nodeId) {
|
|
76
|
-
const row = db.prepare('SELECT raw FROM card_revisions WHERE node_id = ? ORDER BY revision DESC LIMIT 1').get(nodeId);
|
|
77
|
-
return row ? JSON.parse(row.raw) : null;
|
|
78
|
-
},
|
|
79
|
-
async setChallenge(nodeId, token, ttlMs = 72 * 3600 * 1000) {
|
|
80
|
-
db.prepare('INSERT OR REPLACE INTO challenges (node_id, token, expires_at) VALUES (?, ?, ?)')
|
|
81
|
-
.run(nodeId, token, new Date(Date.now() + ttlMs).toISOString());
|
|
82
|
-
},
|
|
83
|
-
async getChallenge(nodeId) {
|
|
84
|
-
const row = db.prepare('SELECT * FROM challenges WHERE node_id = ?').get(nodeId);
|
|
85
|
-
if (!row || row.expires_at < now()) return null;
|
|
86
|
-
return row;
|
|
87
|
-
},
|
|
88
|
-
async recordProbe(nodeId, offeringId, stage, kind, ok, detail = null, measured = null) {
|
|
89
|
-
db.prepare('INSERT INTO probes (node_id, offering_id, stage, kind, ok, detail, measured, at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)')
|
|
90
|
-
.run(nodeId, offeringId, stage, kind, ok ? 1 : 0, detail, measured ? JSON.stringify(measured) : null, now());
|
|
91
|
-
},
|
|
92
|
-
async probesFor(nodeId) {
|
|
93
|
-
return db.prepare('SELECT * FROM probes WHERE node_id = ? ORDER BY id DESC LIMIT 100').all(nodeId);
|
|
94
|
-
},
|
|
95
|
-
async transitionsFor(nodeId) {
|
|
96
|
-
return db.prepare('SELECT * FROM transitions WHERE node_id = ? ORDER BY id ASC').all(nodeId);
|
|
97
|
-
},
|
|
98
|
-
async allNodes() { return db.prepare('SELECT * FROM nodes').all(); },
|
|
99
|
-
async recentTransitions(limit = 50) {
|
|
100
|
-
return db.prepare(`SELECT t.*, n.source FROM transitions t
|
|
101
|
-
LEFT JOIN nodes n ON n.id = t.node_id ORDER BY t.id DESC LIMIT ?`).all(limit);
|
|
102
|
-
},
|
|
103
|
-
async addFingerprints(artifact, refs) {
|
|
104
|
-
for (const ref of refs) {
|
|
105
|
-
db.prepare('INSERT OR REPLACE INTO fingerprints (artifact, prompt, expected_hash) VALUES (?, ?, ?)')
|
|
106
|
-
.run(artifact, ref.prompt, ref.expected_hash);
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
async getFingerprints(artifact) {
|
|
110
|
-
return db.prepare('SELECT prompt, expected_hash FROM fingerprints WHERE artifact = ?').all(artifact);
|
|
111
|
-
},
|
|
112
|
-
async setObservation(nodeId, offeringId, key, value) {
|
|
113
|
-
db.prepare('INSERT OR REPLACE INTO observations (node_id, offering_id, key, value, at) VALUES (?, ?, ?, ?, ?)')
|
|
114
|
-
.run(nodeId, offeringId, key, JSON.stringify(value), now());
|
|
115
|
-
},
|
|
116
|
-
async getObservations(nodeId, offeringId) {
|
|
117
|
-
const rows = db.prepare('SELECT key, value FROM observations WHERE node_id = ? AND offering_id = ?').all(nodeId, offeringId);
|
|
118
|
-
return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
|
|
119
|
-
},
|
|
120
|
-
async observationsForNode(nodeId) {
|
|
121
|
-
const rows = db.prepare('SELECT offering_id, key, value FROM observations WHERE node_id = ?').all(nodeId);
|
|
122
|
-
return rows.map((r) => ({ offering_id: r.offering_id, key: r.key, value: JSON.parse(r.value) }));
|
|
123
|
-
},
|
|
124
|
-
async close() { db.close(); },
|
|
125
|
-
};
|
|
126
|
-
}
|
|
1
|
+
// Registry storage. The async store API is the seam between drivers:
|
|
2
|
+
// - sqlite (node:sqlite): zero-setup dev/tests, file or :memory:
|
|
3
|
+
// - postgres (store-pg.js): production, selected by a postgres:// URL
|
|
4
|
+
// Every method is async so both drivers present the identical contract.
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
|
|
7
|
+
/** config: ':memory:' | sqlite file path | postgres:// URL */
|
|
8
|
+
export async function createStore(config = ':memory:') {
|
|
9
|
+
if (typeof config === 'string' && /^postgres(ql)?:\/\//.test(config)) {
|
|
10
|
+
const { createPgStore } = await import('./store-pg.js');
|
|
11
|
+
return createPgStore({ connectionString: config });
|
|
12
|
+
}
|
|
13
|
+
return createSqliteStore(config);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function createSqliteStore(path = ':memory:') {
|
|
17
|
+
const db = new DatabaseSync(path);
|
|
18
|
+
db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
20
|
+
id TEXT PRIMARY KEY, origin TEXT NOT NULL, card_url TEXT NOT NULL,
|
|
21
|
+
state TEXT NOT NULL, institutional INTEGER DEFAULT 0,
|
|
22
|
+
source TEXT NOT NULL DEFAULT 'registration',
|
|
23
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
24
|
+
);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS card_revisions (
|
|
26
|
+
node_id TEXT NOT NULL, revision TEXT NOT NULL, raw TEXT NOT NULL,
|
|
27
|
+
sig_ok INTEGER NOT NULL, fetched_at TEXT NOT NULL,
|
|
28
|
+
PRIMARY KEY (node_id, revision)
|
|
29
|
+
);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS challenges (
|
|
31
|
+
node_id TEXT PRIMARY KEY, token TEXT NOT NULL, expires_at TEXT NOT NULL
|
|
32
|
+
);
|
|
33
|
+
CREATE TABLE IF NOT EXISTS probes (
|
|
34
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, node_id TEXT NOT NULL, offering_id TEXT,
|
|
35
|
+
stage TEXT NOT NULL, kind TEXT NOT NULL, ok INTEGER NOT NULL,
|
|
36
|
+
detail TEXT, measured TEXT, at TEXT NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS transitions (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, node_id TEXT NOT NULL,
|
|
40
|
+
from_state TEXT, to_state TEXT NOT NULL, reason TEXT, at TEXT NOT NULL
|
|
41
|
+
);
|
|
42
|
+
CREATE TABLE IF NOT EXISTS fingerprints (
|
|
43
|
+
artifact TEXT NOT NULL, prompt TEXT NOT NULL, expected_hash TEXT NOT NULL,
|
|
44
|
+
PRIMARY KEY (artifact, prompt)
|
|
45
|
+
);
|
|
46
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
47
|
+
node_id TEXT NOT NULL, offering_id TEXT NOT NULL, key TEXT NOT NULL,
|
|
48
|
+
value TEXT NOT NULL, at TEXT NOT NULL,
|
|
49
|
+
PRIMARY KEY (node_id, offering_id, key)
|
|
50
|
+
);
|
|
51
|
+
`);
|
|
52
|
+
|
|
53
|
+
const now = () => new Date().toISOString();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
driver: 'sqlite',
|
|
57
|
+
async upsertNode(id, origin, cardUrl, state, source = 'registration') {
|
|
58
|
+
db.prepare(`INSERT INTO nodes (id, origin, card_url, state, source, created_at, updated_at)
|
|
59
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
60
|
+
ON CONFLICT(id) DO UPDATE SET origin=excluded.origin, card_url=excluded.card_url,
|
|
61
|
+
source=excluded.source, updated_at=excluded.updated_at`)
|
|
62
|
+
.run(id, origin, cardUrl, state, source, now(), now());
|
|
63
|
+
},
|
|
64
|
+
async getNode(id) { return db.prepare('SELECT * FROM nodes WHERE id = ?').get(id); },
|
|
65
|
+
async setState(id, toState, reason = null) {
|
|
66
|
+
const cur = await this.getNode(id);
|
|
67
|
+
db.prepare('UPDATE nodes SET state = ?, updated_at = ? WHERE id = ?').run(toState, now(), id);
|
|
68
|
+
db.prepare('INSERT INTO transitions (node_id, from_state, to_state, reason, at) VALUES (?, ?, ?, ?, ?)')
|
|
69
|
+
.run(id, cur?.state ?? null, toState, reason, now());
|
|
70
|
+
},
|
|
71
|
+
async saveRevision(nodeId, revision, card, sigOk) {
|
|
72
|
+
db.prepare(`INSERT OR REPLACE INTO card_revisions (node_id, revision, raw, sig_ok, fetched_at)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?)`).run(nodeId, revision, JSON.stringify(card), sigOk ? 1 : 0, now());
|
|
74
|
+
},
|
|
75
|
+
async latestCard(nodeId) {
|
|
76
|
+
const row = db.prepare('SELECT raw FROM card_revisions WHERE node_id = ? ORDER BY revision DESC LIMIT 1').get(nodeId);
|
|
77
|
+
return row ? JSON.parse(row.raw) : null;
|
|
78
|
+
},
|
|
79
|
+
async setChallenge(nodeId, token, ttlMs = 72 * 3600 * 1000) {
|
|
80
|
+
db.prepare('INSERT OR REPLACE INTO challenges (node_id, token, expires_at) VALUES (?, ?, ?)')
|
|
81
|
+
.run(nodeId, token, new Date(Date.now() + ttlMs).toISOString());
|
|
82
|
+
},
|
|
83
|
+
async getChallenge(nodeId) {
|
|
84
|
+
const row = db.prepare('SELECT * FROM challenges WHERE node_id = ?').get(nodeId);
|
|
85
|
+
if (!row || row.expires_at < now()) return null;
|
|
86
|
+
return row;
|
|
87
|
+
},
|
|
88
|
+
async recordProbe(nodeId, offeringId, stage, kind, ok, detail = null, measured = null) {
|
|
89
|
+
db.prepare('INSERT INTO probes (node_id, offering_id, stage, kind, ok, detail, measured, at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)')
|
|
90
|
+
.run(nodeId, offeringId, stage, kind, ok ? 1 : 0, detail, measured ? JSON.stringify(measured) : null, now());
|
|
91
|
+
},
|
|
92
|
+
async probesFor(nodeId) {
|
|
93
|
+
return db.prepare('SELECT * FROM probes WHERE node_id = ? ORDER BY id DESC LIMIT 100').all(nodeId);
|
|
94
|
+
},
|
|
95
|
+
async transitionsFor(nodeId) {
|
|
96
|
+
return db.prepare('SELECT * FROM transitions WHERE node_id = ? ORDER BY id ASC').all(nodeId);
|
|
97
|
+
},
|
|
98
|
+
async allNodes() { return db.prepare('SELECT * FROM nodes').all(); },
|
|
99
|
+
async recentTransitions(limit = 50) {
|
|
100
|
+
return db.prepare(`SELECT t.*, n.source FROM transitions t
|
|
101
|
+
LEFT JOIN nodes n ON n.id = t.node_id ORDER BY t.id DESC LIMIT ?`).all(limit);
|
|
102
|
+
},
|
|
103
|
+
async addFingerprints(artifact, refs) {
|
|
104
|
+
for (const ref of refs) {
|
|
105
|
+
db.prepare('INSERT OR REPLACE INTO fingerprints (artifact, prompt, expected_hash) VALUES (?, ?, ?)')
|
|
106
|
+
.run(artifact, ref.prompt, ref.expected_hash);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
async getFingerprints(artifact) {
|
|
110
|
+
return db.prepare('SELECT prompt, expected_hash FROM fingerprints WHERE artifact = ?').all(artifact);
|
|
111
|
+
},
|
|
112
|
+
async setObservation(nodeId, offeringId, key, value) {
|
|
113
|
+
db.prepare('INSERT OR REPLACE INTO observations (node_id, offering_id, key, value, at) VALUES (?, ?, ?, ?, ?)')
|
|
114
|
+
.run(nodeId, offeringId, key, JSON.stringify(value), now());
|
|
115
|
+
},
|
|
116
|
+
async getObservations(nodeId, offeringId) {
|
|
117
|
+
const rows = db.prepare('SELECT key, value FROM observations WHERE node_id = ? AND offering_id = ?').all(nodeId, offeringId);
|
|
118
|
+
return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
|
|
119
|
+
},
|
|
120
|
+
async observationsForNode(nodeId) {
|
|
121
|
+
const rows = db.prepare('SELECT offering_id, key, value FROM observations WHERE node_id = ?').all(nodeId);
|
|
122
|
+
return rows.map((r) => ({ offering_id: r.offering_id, key: r.key, value: JSON.parse(r.value) }));
|
|
123
|
+
},
|
|
124
|
+
async close() { db.close(); },
|
|
125
|
+
};
|
|
126
|
+
}
|