@geraldmaron/construct 1.0.21 → 1.0.24
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 +23 -7
- package/bin/construct +206 -228
- package/bin/construct-postinstall.mjs +25 -15
- package/lib/acp/server.mjs +113 -0
- package/lib/agent-instructions/inject.mjs +94 -0
- package/lib/auto-docs.mjs +10 -2
- package/lib/cli-commands.mjs +43 -15
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +4 -0
- package/lib/decisions/enforced-baseline.json +2 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +31 -10
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/features.mjs +11 -11
- package/lib/git-hooks-path.mjs +61 -0
- package/lib/home-namespace.mjs +60 -0
- package/lib/hooks/ci-status-check.mjs +62 -40
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
- package/lib/hooks/pre-push-gate.mjs +15 -6
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/hooks/stop-notify.mjs +32 -17
- package/lib/hooks/stop-typecheck.mjs +7 -2
- package/lib/host-capabilities.mjs +24 -8
- package/lib/host-disposition.mjs +76 -0
- package/lib/ingest/provider-extract.mjs +1 -1
- package/lib/init-docs.mjs +1 -1
- package/lib/init-unified.mjs +320 -219
- package/lib/init-update.mjs +4 -84
- package/lib/init.mjs +9 -25
- package/lib/install/stage-project.mjs +8 -2
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/intent-classifier.mjs +1 -1
- package/lib/knowledge/search.mjs +52 -3
- package/lib/mcp/server.mjs +57 -14
- package/lib/mcp/tools/memory.mjs +2 -2
- package/lib/mcp/tools/orchestration-run.mjs +125 -0
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +3 -3
- package/lib/mcp-manager.mjs +59 -3
- package/lib/model-registry.mjs +40 -33
- package/lib/observation-store.mjs +39 -163
- package/lib/opencode-config.mjs +1 -1
- package/lib/orchestration/events.mjs +66 -0
- package/lib/orchestration/runtime.mjs +72 -10
- package/lib/orchestration/worker.mjs +69 -21
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +46 -24
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-init-shared.mjs +0 -173
- package/lib/reconcile/adapter-prune.mjs +105 -0
- package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
- package/lib/reconcile/gitignore-coverage.mjs +88 -0
- package/lib/reconcile/index.mjs +169 -0
- package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
- package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
- package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
- package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
- package/lib/runtime/uv-bootstrap.mjs +27 -3
- package/lib/schema-infer.mjs +16 -2
- package/lib/server/csrf.mjs +14 -2
- package/lib/server/index.mjs +95 -0
- package/lib/service-manager.mjs +59 -252
- package/lib/setup-prompts.mjs +2 -1
- package/lib/setup.mjs +104 -480
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -43
- package/lib/storage/hybrid-query.mjs +15 -196
- package/lib/storage/sync.mjs +36 -177
- package/lib/storage/vector-client.mjs +256 -235
- package/lib/strategy-store.mjs +35 -286
- package/lib/template-registry.mjs +73 -0
- package/lib/term-format.mjs +75 -0
- package/lib/uninstall/uninstall.mjs +180 -7
- package/lib/worker/entrypoint.mjs +6 -14
- package/package.json +7 -6
- package/personas/construct.md +7 -8
- package/platforms/claude/settings.template.json +30 -11
- package/platforms/opencode/config.template.json +2 -2
- package/rules/common/neurodivergent-output.md +66 -0
- package/rules/common/tool-invisibility.md +37 -0
- package/scripts/sync-specialists.mjs +427 -107
- package/skills/operating/orchestration-reference.md +2 -16
- package/specialists/policy-inventory.json +14 -0
- package/specialists/prompts/cx-accessibility.md +2 -6
- package/specialists/prompts/cx-ai-engineer.md +0 -4
- package/specialists/prompts/cx-architect.md +3 -5
- package/specialists/prompts/cx-business-strategist.md +0 -5
- package/specialists/prompts/cx-data-analyst.md +0 -4
- package/specialists/prompts/cx-data-engineer.md +0 -4
- package/specialists/prompts/cx-debugger.md +2 -6
- package/specialists/prompts/cx-designer.md +0 -8
- package/specialists/prompts/cx-devil-advocate.md +2 -2
- package/specialists/prompts/cx-docs-keeper.md +0 -13
- package/specialists/prompts/cx-engineer.md +0 -13
- package/specialists/prompts/cx-evaluator.md +2 -2
- package/specialists/prompts/cx-explorer.md +4 -5
- package/specialists/prompts/cx-legal-compliance.md +4 -5
- package/specialists/prompts/cx-operations.md +0 -5
- package/specialists/prompts/cx-orchestrator.md +0 -4
- package/specialists/prompts/cx-platform-engineer.md +0 -8
- package/specialists/prompts/cx-product-manager.md +0 -8
- package/specialists/prompts/cx-qa.md +3 -12
- package/specialists/prompts/cx-rd-lead.md +0 -5
- package/specialists/prompts/cx-release-manager.md +0 -8
- package/specialists/prompts/cx-researcher.md +5 -29
- package/specialists/prompts/cx-reviewer.md +2 -6
- package/specialists/prompts/cx-security.md +2 -11
- package/specialists/prompts/cx-sre.md +0 -15
- package/specialists/prompts/cx-test-automation.md +0 -4
- package/specialists/prompts/cx-trace-reviewer.md +2 -2
- package/specialists/prompts/cx-ux-researcher.md +0 -4
- package/specialists/registry.json +28 -36
- package/templates/distribution/run.mjs +36 -7
- package/templates/docs/accessibility-audit.md +56 -0
- package/templates/docs/architecture-review.md +59 -0
- package/templates/docs/code-review-report.md +46 -0
- package/templates/docs/construct_guide.md +15 -15
- package/templates/docs/debug-investigation.md +53 -0
- package/templates/docs/qa-report.md +48 -0
- package/templates/docs/security-audit-report.md +48 -0
- package/templates/docs/task-packet.md +49 -0
- package/templates/docs/verdict.md +40 -0
- package/db/schema/001_init.sql +0 -40
- package/db/schema/002_pgvector.sql +0 -182
- package/db/schema/003_intake.sql +0 -47
- package/db/schema/003_observation_reconciliation.sql +0 -14
- package/db/schema/004_recommendations.sql +0 -46
- package/db/schema/005_strategy.sql +0 -21
- package/db/schema/006_graph.sql +0 -24
- package/db/schema/007_tags.sql +0 -30
- package/db/schema/008_skill_usage.sql +0 -24
- package/db/schema/009_scheduler.sql +0 -14
- package/db/schema/010_cx_scores.sql +0 -51
- package/lib/intake/postgres-queue.mjs +0 -240
- package/lib/server/static/index.html +0 -1
- package/lib/services/local-postgres.mjs +0 -15
- package/lib/storage/backup.mjs +0 -347
- package/lib/storage/migrations.mjs +0 -187
- package/lib/storage/postgres-backup.mjs +0 -124
- package/lib/storage/sql-store.mjs +0 -45
- package/lib/storage/store-version.mjs +0 -115
- package/lib/storage/vector-store.mjs +0 -100
|
@@ -1,59 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/storage/vector-client.mjs — Unified vector storage client for
|
|
3
|
-
*
|
|
4
|
-
* Hides whether we're using local Postgres, managed Postgres, or (future) external vector DB.
|
|
5
|
-
* All callers use: store(id, embedding, metadata) and search(queryEmbedding, filters).
|
|
6
|
-
*
|
|
7
|
-
* Works with any Postgres that has the pgvector extension installed.
|
|
8
|
-
* Connection string (DATABASE_URL) determines the target.
|
|
9
|
-
*
|
|
10
|
-
* Dimensionality is resolved from the embedding engine at construction time
|
|
11
|
-
* via `getEmbeddingModelInfo()`. The legacy `PGVECTOR_DIMENSIONS = 384` constant
|
|
12
|
-
* is retained as a fallback only when the engine cannot be reached. When
|
|
13
|
-
* `assertSchemaDimensions()` runs, the actual `embedding` column declared
|
|
14
|
-
* dimension is read from `pg_attribute` and compared against the engine; a
|
|
15
|
-
* mismatch surfaces a precise error so the operator can either rerun a
|
|
16
|
-
* compatible migration or change `CONSTRUCT_EMBEDDING_MODEL`.
|
|
17
|
-
*
|
|
18
|
-
* NOTE: `postgres` package is imported lazily to avoid ERR_MODULE_NOT_FOUND
|
|
19
|
-
* when this module is loaded in environments where `postgres` is not installed.
|
|
2
|
+
* lib/storage/vector-client.mjs — Unified vector storage client for LanceDB.
|
|
20
3
|
*/
|
|
21
4
|
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
22
7
|
import { getEmbeddingModelInfo } from './embeddings-engine.mjs';
|
|
23
8
|
|
|
24
|
-
const DEFAULT_POOL = { max: 10, idle_timeout: 20, connect_timeout: 10 };
|
|
25
9
|
const FALLBACK_DIMENSIONS = 384;
|
|
26
10
|
|
|
27
|
-
|
|
11
|
+
function getDbPath(env = process.env) {
|
|
12
|
+
if (env.CONSTRUCT_LANCEDB_PATH) return env.CONSTRUCT_LANCEDB_PATH;
|
|
13
|
+
return path.join(process.cwd(), '.cx', 'lancedb');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// LanceDB uses optimistic concurrency: two writes racing the same table fail
|
|
17
|
+
// with "Incompatible transaction ... Overwrite at version N" and two creates
|
|
18
|
+
// race "Table already exists". Call sites build a fresh VectorClient per write
|
|
19
|
+
// (inbox ingest fans out across files), so writes are serialized per database
|
|
20
|
+
// directory at module scope — keyed by path, not by instance — and the queue
|
|
21
|
+
// key is dropped once its chain drains.
|
|
28
22
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
const writeQueues = new Map();
|
|
24
|
+
|
|
25
|
+
function serializeWrite(dbPath, task) {
|
|
26
|
+
const prev = writeQueues.get(dbPath) || Promise.resolve();
|
|
27
|
+
const run = prev.then(task, task);
|
|
28
|
+
const chain = run.then(() => {}, () => {});
|
|
29
|
+
writeQueues.set(dbPath, chain);
|
|
30
|
+
chain.then(() => {
|
|
31
|
+
if (writeQueues.get(dbPath) === chain) writeQueues.delete(dbPath);
|
|
32
|
+
});
|
|
33
|
+
return run;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// Serialization covers this process; a retry absorbs the residual conflict from
|
|
37
|
+
// a concurrent writer in another process (separate test files) or a create race.
|
|
38
|
+
|
|
39
|
+
async function withWriteRetry(task, { attempts = 6, baseMs = 25 } = {}) {
|
|
40
|
+
let lastErr;
|
|
41
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
42
|
+
try {
|
|
43
|
+
return await task();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
lastErr = err;
|
|
46
|
+
const msg = String(err?.message || err);
|
|
47
|
+
const retriable = /Incompatible transaction|already exists|Commit conflict|conflict_resolver|failed to persist|No such file or directory|LanceError\(IO\)/i.test(msg);
|
|
48
|
+
if (!retriable || attempt === attempts - 1) throw err;
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, baseMs * (attempt + 1)));
|
|
50
|
+
}
|
|
39
51
|
}
|
|
52
|
+
throw lastErr;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
export class VectorClient {
|
|
43
56
|
constructor({ databaseUrl, env } = {}) {
|
|
44
|
-
this.url = databaseUrl || process.env.DATABASE_URL || null;
|
|
45
57
|
this.env = env || process.env;
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
58
|
+
this._db = null;
|
|
59
|
+
this._lancedbModule = null;
|
|
60
|
+
this._arrowModule = null;
|
|
48
61
|
this._engineDim = null;
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
async _initModules() {
|
|
65
|
+
if (this._lancedbModule && this._arrowModule) return;
|
|
66
|
+
try {
|
|
67
|
+
this._lancedbModule = await import('@lancedb/lancedb');
|
|
68
|
+
this._arrowModule = await import('apache-arrow');
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Failed to load LanceDB or Apache Arrow: ${err.message}. ` +
|
|
72
|
+
`Ensure they are installed (npm install @lancedb/lancedb apache-arrow).`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async _getDb() {
|
|
78
|
+
if (this._db) return this._db;
|
|
79
|
+
await this._initModules();
|
|
80
|
+
const dbPath = getDbPath(this.env);
|
|
81
|
+
if (!fs.existsSync(dbPath)) {
|
|
82
|
+
fs.mkdirSync(dbPath, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
this._db = await this._lancedbModule.connect(dbPath);
|
|
85
|
+
return this._db;
|
|
86
|
+
}
|
|
87
|
+
|
|
57
88
|
async getEngineDimensions() {
|
|
58
89
|
if (this._engineDim != null) return this._engineDim;
|
|
59
90
|
try {
|
|
@@ -65,237 +96,227 @@ export class VectorClient {
|
|
|
65
96
|
return this._engineDim;
|
|
66
97
|
}
|
|
67
98
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
`'ALTER TABLE construct_embeddings ALTER COLUMN embedding TYPE vector(${engineDim})' (truncate first).`
|
|
100
|
-
);
|
|
99
|
+
async _getObservationsTable() {
|
|
100
|
+
const db = await this._getDb();
|
|
101
|
+
const tableName = 'observations_v1';
|
|
102
|
+
const dim = await this.getEngineDimensions();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
return await db.openTable(tableName);
|
|
106
|
+
} catch {
|
|
107
|
+
const schema = new this._arrowModule.Schema([
|
|
108
|
+
new this._arrowModule.Field('id', new this._arrowModule.Utf8()),
|
|
109
|
+
new this._arrowModule.Field('project', new this._arrowModule.Utf8(), true),
|
|
110
|
+
new this._arrowModule.Field('role', new this._arrowModule.Utf8(), true),
|
|
111
|
+
new this._arrowModule.Field('category', new this._arrowModule.Utf8(), true),
|
|
112
|
+
new this._arrowModule.Field('summary', new this._arrowModule.Utf8(), true),
|
|
113
|
+
new this._arrowModule.Field('content', new this._arrowModule.Utf8(), true),
|
|
114
|
+
new this._arrowModule.Field('tags', new this._arrowModule.Utf8(), true),
|
|
115
|
+
new this._arrowModule.Field('confidence', new this._arrowModule.Float32(), true),
|
|
116
|
+
new this._arrowModule.Field('source', new this._arrowModule.Utf8(), true),
|
|
117
|
+
new this._arrowModule.Field('git_sha', new this._arrowModule.Utf8(), true),
|
|
118
|
+
new this._arrowModule.Field('embedding', new this._arrowModule.FixedSizeList(dim, new this._arrowModule.Field('item', new this._arrowModule.Float32()))),
|
|
119
|
+
new this._arrowModule.Field('content_hash', new this._arrowModule.Utf8(), true),
|
|
120
|
+
new this._arrowModule.Field('model', new this._arrowModule.Utf8(), true),
|
|
121
|
+
new this._arrowModule.Field('created_at', new this._arrowModule.Utf8(), true),
|
|
122
|
+
new this._arrowModule.Field('updated_at', new this._arrowModule.Utf8(), true),
|
|
123
|
+
]);
|
|
124
|
+
try {
|
|
125
|
+
return await db.createTable(tableName, [], { schema });
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (/already exists/i.test(String(err?.message))) return await db.openTable(tableName);
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
101
130
|
}
|
|
102
|
-
return schemaDim;
|
|
103
131
|
}
|
|
104
132
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (!this.url) return null;
|
|
110
|
-
if (this._sql) return this._sql;
|
|
133
|
+
async _getDocumentsTable() {
|
|
134
|
+
const db = await this._getDb();
|
|
135
|
+
const tableName = 'documents_v1';
|
|
136
|
+
const dim = await this.getEngineDimensions();
|
|
111
137
|
|
|
112
|
-
|
|
138
|
+
try {
|
|
139
|
+
return await db.openTable(tableName);
|
|
140
|
+
} catch {
|
|
141
|
+
const schema = new this._arrowModule.Schema([
|
|
142
|
+
new this._arrowModule.Field('id', new this._arrowModule.Utf8()),
|
|
143
|
+
new this._arrowModule.Field('project', new this._arrowModule.Utf8(), true),
|
|
144
|
+
new this._arrowModule.Field('kind', new this._arrowModule.Utf8(), true),
|
|
145
|
+
new this._arrowModule.Field('title', new this._arrowModule.Utf8(), true),
|
|
146
|
+
new this._arrowModule.Field('summary', new this._arrowModule.Utf8(), true),
|
|
147
|
+
new this._arrowModule.Field('body', new this._arrowModule.Utf8(), true),
|
|
148
|
+
new this._arrowModule.Field('source_path', new this._arrowModule.Utf8(), true),
|
|
149
|
+
new this._arrowModule.Field('tags', new this._arrowModule.Utf8(), true),
|
|
150
|
+
new this._arrowModule.Field('content_hash', new this._arrowModule.Utf8(), true),
|
|
151
|
+
new this._arrowModule.Field('embedding', new this._arrowModule.FixedSizeList(dim, new this._arrowModule.Field('item', new this._arrowModule.Float32()))),
|
|
152
|
+
new this._arrowModule.Field('model', new this._arrowModule.Utf8(), true),
|
|
153
|
+
new this._arrowModule.Field('created_at', new this._arrowModule.Utf8(), true),
|
|
154
|
+
new this._arrowModule.Field('updated_at', new this._arrowModule.Utf8(), true),
|
|
155
|
+
]);
|
|
113
156
|
try {
|
|
114
|
-
|
|
115
|
-
this._pgModule = m.default || m;
|
|
157
|
+
return await db.createTable(tableName, [], { schema });
|
|
116
158
|
} catch (err) {
|
|
117
|
-
|
|
118
|
-
throw
|
|
119
|
-
`Failed to load 'postgres' module: ${err.message}. ` +
|
|
120
|
-
`Ensure 'postgres' is installed (npm install postgres).`
|
|
121
|
-
);
|
|
159
|
+
if (/already exists/i.test(String(err?.message))) return await db.openTable(tableName);
|
|
160
|
+
throw err;
|
|
122
161
|
}
|
|
123
162
|
}
|
|
124
|
-
|
|
125
|
-
this._sql = this._pgModule(this.url, DEFAULT_POOL);
|
|
126
|
-
return this._sql;
|
|
127
163
|
}
|
|
128
164
|
|
|
129
|
-
/**
|
|
130
|
-
* Check if the SQL backend is available and healthy.
|
|
131
|
-
*/
|
|
132
165
|
async isHealthy() {
|
|
133
|
-
const sql = await this._getSql();
|
|
134
|
-
if (!sql) return false;
|
|
135
166
|
try {
|
|
136
|
-
|
|
137
|
-
return
|
|
167
|
+
await this._getDb();
|
|
168
|
+
return true;
|
|
138
169
|
} catch {
|
|
139
170
|
return false;
|
|
140
171
|
}
|
|
141
172
|
}
|
|
142
173
|
|
|
143
|
-
/**
|
|
144
|
-
* Check if pgvector extension is enabled.
|
|
145
|
-
*/
|
|
146
174
|
async isPgvectorEnabled() {
|
|
147
|
-
|
|
148
|
-
if (!sql) return false;
|
|
149
|
-
try {
|
|
150
|
-
const result = await sql`
|
|
151
|
-
SELECT EXISTS (
|
|
152
|
-
SELECT 1 FROM pg_extension WHERE extname = 'vector'
|
|
153
|
-
) AS enabled
|
|
154
|
-
`;
|
|
155
|
-
return result?.[0]?.enabled === true;
|
|
156
|
-
} catch {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
175
|
+
return this.isHealthy();
|
|
159
176
|
}
|
|
160
177
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
*/
|
|
164
|
-
async storeObservation({ id, project, role, category, summary, content, tags, confidence, source, embedding, gitSha, contentHash = null, model = null }) {
|
|
165
|
-
const sql = await this._getSql();
|
|
166
|
-
if (!sql) return { mode: 'file', reason: 'no_sql' };
|
|
167
|
-
const expected = await this.getEngineDimensions();
|
|
168
|
-
assertPgvectorDimensions(embedding, { expected, label: 'observation embedding' });
|
|
169
|
-
|
|
170
|
-
const embeddingVec = floatArrayToPgVector(embedding);
|
|
171
|
-
await sql`
|
|
172
|
-
INSERT INTO construct_observations (id, project, role, category, summary, content, tags, confidence, source, git_sha, embedding, content_hash, model)
|
|
173
|
-
VALUES (${id}, ${project}, ${role}, ${category}, ${summary}, ${content}, ${JSON.stringify(tags || [])}, ${confidence || 0.8}, ${source || null}, ${gitSha || null}, ${embeddingVec}, ${contentHash}, ${model})
|
|
174
|
-
ON CONFLICT (id) DO UPDATE SET
|
|
175
|
-
summary = EXCLUDED.summary,
|
|
176
|
-
content = EXCLUDED.content,
|
|
177
|
-
tags = EXCLUDED.tags,
|
|
178
|
-
confidence = EXCLUDED.confidence,
|
|
179
|
-
source = EXCLUDED.source,
|
|
180
|
-
git_sha = EXCLUDED.git_sha,
|
|
181
|
-
embedding = EXCLUDED.embedding,
|
|
182
|
-
content_hash = EXCLUDED.content_hash,
|
|
183
|
-
model = EXCLUDED.model,
|
|
184
|
-
updated_at = now()
|
|
185
|
-
`;
|
|
186
|
-
return { mode: 'sql', id };
|
|
178
|
+
async storeObservation(record) {
|
|
179
|
+
return serializeWrite(getDbPath(this.env), () => withWriteRetry(() => this._storeObservation(record)));
|
|
187
180
|
}
|
|
188
181
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
182
|
+
async _storeObservation(record) {
|
|
183
|
+
const table = await this._getObservationsTable();
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
|
|
186
|
+
const data = {
|
|
187
|
+
id: record.id,
|
|
188
|
+
project: record.project || '',
|
|
189
|
+
role: record.role || 'unknown',
|
|
190
|
+
category: record.category || 'insight',
|
|
191
|
+
summary: record.summary || '',
|
|
192
|
+
content: record.content || '',
|
|
193
|
+
tags: JSON.stringify(record.tags || []),
|
|
194
|
+
confidence: Number(record.confidence || 0.8),
|
|
195
|
+
source: typeof record.source === 'object' ? JSON.stringify(record.source) : (record.source || ''),
|
|
196
|
+
git_sha: record.gitSha || '',
|
|
197
|
+
embedding: Array.from(record.embedding),
|
|
198
|
+
content_hash: record.contentHash || '',
|
|
199
|
+
model: record.model || '',
|
|
200
|
+
created_at: record.createdAt || now,
|
|
201
|
+
updated_at: now
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
await table.mergeInsert('id')
|
|
205
|
+
.whenMatchedUpdateAll()
|
|
206
|
+
.whenNotMatchedInsertAll()
|
|
207
|
+
.execute([data]);
|
|
208
|
+
|
|
209
|
+
return { mode: 'lancedb', id: record.id };
|
|
210
|
+
}
|
|
192
211
|
|
|
193
212
|
async getObservationFingerprints(ids = []) {
|
|
194
|
-
const
|
|
195
|
-
if (!
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
const table = await this._getObservationsTable();
|
|
214
|
+
if (!Array.isArray(ids) || ids.length === 0) return new Map();
|
|
215
|
+
|
|
216
|
+
const results = await table.query()
|
|
217
|
+
.where(`id IN (${ids.map(id => `'${id}'`).join(',')})`)
|
|
218
|
+
.select(['id', 'content_hash', 'model'])
|
|
219
|
+
.toArray();
|
|
220
|
+
|
|
221
|
+
return new Map(results.map((r) => [r.id, { contentHash: r.content_hash, model: r.model }]));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async searchObservations({ project, queryEmbedding, limit = 10, minSimilarity = 0.01, role, category }) {
|
|
225
|
+
const table = await this._getObservationsTable();
|
|
226
|
+
|
|
227
|
+
const query = table.query()
|
|
228
|
+
.nearestTo(Array.from(queryEmbedding))
|
|
229
|
+
.distanceType('cosine');
|
|
230
|
+
|
|
231
|
+
const rawResults = await query.limit(100).toArray();
|
|
232
|
+
|
|
233
|
+
return rawResults.map(r => {
|
|
234
|
+
const distance = r._distance !== undefined ? r._distance : 0;
|
|
235
|
+
let tags = [];
|
|
236
|
+
try {
|
|
237
|
+
tags = (typeof r.tags === 'string' && r.tags.startsWith('[')) ? JSON.parse(r.tags) : (Array.isArray(r.tags) ? r.tags : []);
|
|
238
|
+
} catch {}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
...r,
|
|
242
|
+
similarity: 1 - distance,
|
|
243
|
+
tags
|
|
244
|
+
};
|
|
245
|
+
})
|
|
246
|
+
.filter(r => {
|
|
247
|
+
// Use fuzzy similarity and robust project matching
|
|
248
|
+
if (r.similarity < minSimilarity) return false;
|
|
249
|
+
if (project && r.project !== project) return false;
|
|
250
|
+
if (role && r.role && r.role !== role) return false;
|
|
251
|
+
if (category && r.category && r.category !== category) return false;
|
|
252
|
+
return true;
|
|
253
|
+
})
|
|
254
|
+
.slice(0, limit);
|
|
200
255
|
}
|
|
201
256
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*/
|
|
205
|
-
async searchObservations({ project, queryEmbedding, limit = 10, minSimilarity = 0.3, role, category }) {
|
|
206
|
-
const sql = await this._getSql();
|
|
207
|
-
if (!sql) return [];
|
|
208
|
-
const expected = await this.getEngineDimensions();
|
|
209
|
-
assertPgvectorDimensions(queryEmbedding, { expected, label: 'observation query embedding' });
|
|
210
|
-
|
|
211
|
-
const queryVec = floatArrayToPgVector(queryEmbedding);
|
|
212
|
-
const conditions = [sql`project = ${project}`];
|
|
213
|
-
if (role) conditions.push(sql`role = ${role}`);
|
|
214
|
-
if (category) conditions.push(sql`category = ${category}`);
|
|
215
|
-
conditions.push(sql`1 - (embedding <=> ${queryVec}) > ${minSimilarity}`);
|
|
216
|
-
|
|
217
|
-
const results = await sql`
|
|
218
|
-
SELECT
|
|
219
|
-
id, role, category, summary, content, tags, confidence, source, git_sha, created_at,
|
|
220
|
-
1 - (embedding <=> ${queryVec}) AS similarity
|
|
221
|
-
FROM construct_observations
|
|
222
|
-
WHERE ${sql.join(conditions, ' AND ')}
|
|
223
|
-
ORDER BY embedding <=> ${queryVec}
|
|
224
|
-
LIMIT ${limit}
|
|
225
|
-
`;
|
|
226
|
-
return results;
|
|
257
|
+
async storeDocument(record) {
|
|
258
|
+
return serializeWrite(getDbPath(this.env), () => withWriteRetry(() => this._storeDocument(record)));
|
|
227
259
|
}
|
|
228
260
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
261
|
+
async _storeDocument(record) {
|
|
262
|
+
const table = await this._getDocumentsTable();
|
|
263
|
+
const now = new Date().toISOString();
|
|
264
|
+
|
|
265
|
+
const data = {
|
|
266
|
+
id: record.id,
|
|
267
|
+
project: record.project || '',
|
|
268
|
+
kind: record.kind || '',
|
|
269
|
+
title: record.title || '',
|
|
270
|
+
summary: record.summary || '',
|
|
271
|
+
body: record.body || '',
|
|
272
|
+
source_path: record.sourcePath || '',
|
|
273
|
+
tags: JSON.stringify(record.tags || []),
|
|
274
|
+
content_hash: record.contentHash || '',
|
|
275
|
+
embedding: Array.from(record.embedding),
|
|
276
|
+
model: record.model || '',
|
|
277
|
+
created_at: record.createdAt || now,
|
|
278
|
+
updated_at: now
|
|
279
|
+
};
|
|
241
280
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
summary = EXCLUDED.summary,
|
|
249
|
-
body = EXCLUDED.body,
|
|
250
|
-
source_path = EXCLUDED.source_path,
|
|
251
|
-
tags = EXCLUDED.tags,
|
|
252
|
-
content_hash = EXCLUDED.content_hash,
|
|
253
|
-
updated_at = now()
|
|
254
|
-
`;
|
|
255
|
-
|
|
256
|
-
await sql`
|
|
257
|
-
INSERT INTO construct_embeddings (document_id, model, embedding, content_hash)
|
|
258
|
-
VALUES (${id}, ${model}, ${embeddingVec}, ${contentHash || ''})
|
|
259
|
-
ON CONFLICT (document_id) DO UPDATE SET
|
|
260
|
-
model = EXCLUDED.model,
|
|
261
|
-
embedding = EXCLUDED.embedding,
|
|
262
|
-
content_hash = EXCLUDED.content_hash,
|
|
263
|
-
updated_at = now()
|
|
264
|
-
`;
|
|
265
|
-
return { mode: 'sql', id };
|
|
281
|
+
await table.mergeInsert('id')
|
|
282
|
+
.whenMatchedUpdateAll()
|
|
283
|
+
.whenNotMatchedInsertAll()
|
|
284
|
+
.execute([data]);
|
|
285
|
+
|
|
286
|
+
return { mode: 'lancedb', id: record.id };
|
|
266
287
|
}
|
|
267
288
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
async searchDocuments({ project, queryEmbedding, limit = 10, minSimilarity = 0.01 }) {
|
|
290
|
+
const table = await this._getDocumentsTable();
|
|
291
|
+
|
|
292
|
+
const query = table.query()
|
|
293
|
+
.nearestTo(Array.from(queryEmbedding))
|
|
294
|
+
.distanceType('cosine');
|
|
295
|
+
|
|
296
|
+
const rawResults = await query.limit(100).toArray();
|
|
297
|
+
|
|
298
|
+
return rawResults.map(r => {
|
|
299
|
+
const distance = r._distance !== undefined ? r._distance : 0;
|
|
300
|
+
let tags = [];
|
|
301
|
+
try {
|
|
302
|
+
tags = (typeof r.tags === 'string' && r.tags.startsWith('[')) ? JSON.parse(r.tags) : (Array.isArray(r.tags) ? r.tags : []);
|
|
303
|
+
} catch {}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
...r,
|
|
307
|
+
similarity: 1 - distance,
|
|
308
|
+
tags
|
|
309
|
+
};
|
|
310
|
+
})
|
|
311
|
+
.filter(r => {
|
|
312
|
+
if (r.similarity < minSimilarity) return false;
|
|
313
|
+
if (project && r.project !== project) return false;
|
|
314
|
+
return true;
|
|
315
|
+
})
|
|
316
|
+
.slice(0, limit);
|
|
290
317
|
}
|
|
291
318
|
|
|
292
|
-
/**
|
|
293
|
-
* Close the connection pool.
|
|
294
|
-
*/
|
|
295
319
|
async close() {
|
|
296
|
-
|
|
297
|
-
await this._sql.end();
|
|
298
|
-
this._sql = null;
|
|
299
|
-
}
|
|
320
|
+
this._db = null;
|
|
300
321
|
}
|
|
301
322
|
}
|