@geraldmaron/construct 1.0.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/.env.example +69 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/agents/prompts/cx-accessibility.md +29 -0
- package/agents/prompts/cx-ai-engineer.md +31 -0
- package/agents/prompts/cx-architect.md +40 -0
- package/agents/prompts/cx-business-strategist.md +25 -0
- package/agents/prompts/cx-data-analyst.md +30 -0
- package/agents/prompts/cx-data-engineer.md +32 -0
- package/agents/prompts/cx-debugger.md +27 -0
- package/agents/prompts/cx-designer.md +33 -0
- package/agents/prompts/cx-devil-advocate.md +36 -0
- package/agents/prompts/cx-docs-keeper.md +50 -0
- package/agents/prompts/cx-engineer.md +36 -0
- package/agents/prompts/cx-evaluator.md +26 -0
- package/agents/prompts/cx-explorer.md +38 -0
- package/agents/prompts/cx-legal-compliance.md +25 -0
- package/agents/prompts/cx-operations.md +28 -0
- package/agents/prompts/cx-orchestrator.md +30 -0
- package/agents/prompts/cx-platform-engineer.md +25 -0
- package/agents/prompts/cx-product-manager.md +30 -0
- package/agents/prompts/cx-qa.md +42 -0
- package/agents/prompts/cx-rd-lead.md +25 -0
- package/agents/prompts/cx-release-manager.md +31 -0
- package/agents/prompts/cx-researcher.md +31 -0
- package/agents/prompts/cx-reviewer.md +36 -0
- package/agents/prompts/cx-security.md +37 -0
- package/agents/prompts/cx-sre.md +26 -0
- package/agents/prompts/cx-test-automation.md +35 -0
- package/agents/prompts/cx-trace-reviewer.md +76 -0
- package/agents/prompts/cx-ux-researcher.md +30 -0
- package/agents/registry.json +879 -0
- package/agents/teams.json +94 -0
- package/bin/construct +613 -0
- package/commands/build/feature.md +21 -0
- package/commands/build/fix.md +20 -0
- package/commands/design/access.md +19 -0
- package/commands/design/flow.md +16 -0
- package/commands/design/ui.md +18 -0
- package/commands/measure/experiment.md +18 -0
- package/commands/measure/metrics.md +18 -0
- package/commands/measure/results.md +18 -0
- package/commands/plan/api.md +19 -0
- package/commands/plan/challenge.md +18 -0
- package/commands/plan/decide.md +20 -0
- package/commands/plan/feature.md +49 -0
- package/commands/plan/requirements.md +24 -0
- package/commands/remember/context.md +26 -0
- package/commands/remember/handoff.md +19 -0
- package/commands/remember/runbook.md +20 -0
- package/commands/review/code.md +21 -0
- package/commands/review/quality.md +19 -0
- package/commands/review/security.md +16 -0
- package/commands/ship/ready.md +19 -0
- package/commands/ship/release.md +19 -0
- package/commands/ship/status.md +18 -0
- package/commands/understand/docs.md +22 -0
- package/commands/understand/research.md +22 -0
- package/commands/understand/this.md +23 -0
- package/commands/understand/why.md +18 -0
- package/commands/work/clean.md +32 -0
- package/commands/work/drive.md +69 -0
- package/commands/work/optimize-prompts.md +46 -0
- package/commands/work/parallel-review.md +40 -0
- package/db/migrations/001_init.sql +38 -0
- package/langfuse/docker-compose.yml +82 -0
- package/lib/audit-skills.mjs +118 -0
- package/lib/auto-docs.mjs +293 -0
- package/lib/cli-commands.mjs +409 -0
- package/lib/codex-config.mjs +114 -0
- package/lib/comment-lint.mjs +191 -0
- package/lib/completions.mjs +170 -0
- package/lib/context-state.mjs +99 -0
- package/lib/cost.mjs +213 -0
- package/lib/distill.mjs +403 -0
- package/lib/efficiency.mjs +139 -0
- package/lib/env-config.mjs +53 -0
- package/lib/eval-harness.mjs +59 -0
- package/lib/features.mjs +209 -0
- package/lib/headhunt.mjs +597 -0
- package/lib/hooks/adaptive-lint.mjs +112 -0
- package/lib/hooks/agent-tracker.mjs +50 -0
- package/lib/hooks/bootstrap-guard.mjs +90 -0
- package/lib/hooks/comment-lint.mjs +26 -0
- package/lib/hooks/config-protection.mjs +52 -0
- package/lib/hooks/console-warn.mjs +43 -0
- package/lib/hooks/context-window-recovery.mjs +90 -0
- package/lib/hooks/continuation-enforcer.mjs +72 -0
- package/lib/hooks/dep-audit.mjs +155 -0
- package/lib/hooks/drive-guard.mjs +89 -0
- package/lib/hooks/edit-accumulator.mjs +36 -0
- package/lib/hooks/edit-error-recovery.mjs +46 -0
- package/lib/hooks/edit-guard.mjs +109 -0
- package/lib/hooks/env-check.mjs +80 -0
- package/lib/hooks/guard-bash.mjs +83 -0
- package/lib/hooks/mcp-audit.mjs +57 -0
- package/lib/hooks/mcp-health-check.mjs +51 -0
- package/lib/hooks/mcp-task-scope.mjs +47 -0
- package/lib/hooks/model-fallback.mjs +105 -0
- package/lib/hooks/pre-compact.mjs +212 -0
- package/lib/hooks/pre-push-gate.mjs +129 -0
- package/lib/hooks/read-tracker.mjs +129 -0
- package/lib/hooks/registry-sync.mjs +23 -0
- package/lib/hooks/scan-secrets.mjs +76 -0
- package/lib/hooks/session-start.mjs +95 -0
- package/lib/hooks/stop-notify.mjs +191 -0
- package/lib/hooks/stop-typecheck.mjs +83 -0
- package/lib/hooks/task-completed-guard.mjs +43 -0
- package/lib/hooks/teammate-idle-guard.mjs +54 -0
- package/lib/hooks/workflow-guard.mjs +62 -0
- package/lib/host-capabilities.mjs +123 -0
- package/lib/init-docs.mjs +382 -0
- package/lib/mcp/server.mjs +1123 -0
- package/lib/mcp-catalog.json +243 -0
- package/lib/mcp-manager.mjs +433 -0
- package/lib/mcp-platform-config.mjs +104 -0
- package/lib/model-router.mjs +810 -0
- package/lib/opencode-config.mjs +44 -0
- package/lib/opencode-runtime-plugin.mjs +909 -0
- package/lib/opencode-telemetry.mjs +433 -0
- package/lib/orchestration-policy.mjs +258 -0
- package/lib/prompt-composer.mjs +196 -0
- package/lib/prompt-metadata.mjs +68 -0
- package/lib/review.mjs +429 -0
- package/lib/role-preload.mjs +52 -0
- package/lib/schemas/decision.json +50 -0
- package/lib/schemas/implementation.json +51 -0
- package/lib/schemas/review-report.json +32 -0
- package/lib/schemas/test-report.json +43 -0
- package/lib/server/index.mjs +334 -0
- package/lib/server/static/app.js +841 -0
- package/lib/server/static/index.html +820 -0
- package/lib/service-manager.mjs +225 -0
- package/lib/setup.mjs +319 -0
- package/lib/status.mjs +707 -0
- package/lib/storage/backend.mjs +44 -0
- package/lib/storage/embeddings.mjs +70 -0
- package/lib/storage/hybrid-query.mjs +184 -0
- package/lib/storage/sql-store.mjs +55 -0
- package/lib/storage/state-source.mjs +101 -0
- package/lib/storage/sync.mjs +164 -0
- package/lib/storage/vector-store.mjs +59 -0
- package/lib/telemetry/backends/langfuse.mjs +58 -0
- package/lib/telemetry/backends/noop.mjs +16 -0
- package/lib/telemetry/langfuse-ingest.mjs +108 -0
- package/lib/telemetry/langfuse-model-sync.mjs +270 -0
- package/lib/telemetry/team-rollup.mjs +143 -0
- package/lib/toolkit-env.mjs +30 -0
- package/lib/validator.mjs +181 -0
- package/lib/workflow-state.mjs +794 -0
- package/package.json +53 -0
- package/personas/construct.md +94 -0
- package/platforms/claude/CLAUDE.md +30 -0
- package/platforms/claude/settings.template.json +472 -0
- package/platforms/opencode/config.template.json +65 -0
- package/platforms/opencode/plugins/construct-fallback.js +5 -0
- package/platforms/opencode/sync-config.mjs +69 -0
- package/rules/common/agents.md +55 -0
- package/rules/common/code-review.md +129 -0
- package/rules/common/coding-style.md +95 -0
- package/rules/common/comments.md +139 -0
- package/rules/common/cx-agent-routing.md +61 -0
- package/rules/common/cx-skill-routing.md +11 -0
- package/rules/common/development-workflow.md +49 -0
- package/rules/common/git-workflow.md +29 -0
- package/rules/common/hooks.md +35 -0
- package/rules/common/patterns.md +36 -0
- package/rules/common/performance.md +71 -0
- package/rules/common/security.md +34 -0
- package/rules/common/testing.md +62 -0
- package/rules/golang/coding-style.md +37 -0
- package/rules/golang/hooks.md +22 -0
- package/rules/golang/patterns.md +50 -0
- package/rules/golang/security.md +39 -0
- package/rules/golang/testing.md +36 -0
- package/rules/python/coding-style.md +47 -0
- package/rules/python/hooks.md +24 -0
- package/rules/python/patterns.md +44 -0
- package/rules/python/security.md +35 -0
- package/rules/python/testing.md +43 -0
- package/rules/swift/coding-style.md +52 -0
- package/rules/swift/hooks.md +25 -0
- package/rules/swift/patterns.md +71 -0
- package/rules/swift/security.md +38 -0
- package/rules/swift/testing.md +50 -0
- package/rules/typescript/coding-style.md +204 -0
- package/rules/typescript/hooks.md +27 -0
- package/rules/typescript/patterns.md +57 -0
- package/rules/typescript/security.md +33 -0
- package/rules/typescript/testing.md +23 -0
- package/rules/web/coding-style.md +101 -0
- package/rules/web/design-quality.md +68 -0
- package/rules/web/hooks.md +125 -0
- package/rules/web/patterns.md +84 -0
- package/rules/web/performance.md +69 -0
- package/rules/web/security.md +62 -0
- package/rules/web/testing.md +60 -0
- package/rules/zh/README.md +113 -0
- package/rules/zh/agents.md +55 -0
- package/rules/zh/code-review.md +129 -0
- package/rules/zh/coding-style.md +53 -0
- package/rules/zh/development-workflow.md +49 -0
- package/rules/zh/git-workflow.md +29 -0
- package/rules/zh/hooks.md +35 -0
- package/rules/zh/patterns.md +36 -0
- package/rules/zh/performance.md +60 -0
- package/rules/zh/security.md +34 -0
- package/rules/zh/testing.md +34 -0
- package/skills/ai/agent-dev.md +102 -0
- package/skills/ai/llm-security.md +105 -0
- package/skills/ai/ml-ops.md +169 -0
- package/skills/ai/orchestration-workflow.md +87 -0
- package/skills/ai/prompt-and-eval.md +114 -0
- package/skills/ai/prompt-optimizer.md +114 -0
- package/skills/ai/rag-system.md +104 -0
- package/skills/architecture/api-design.md +100 -0
- package/skills/architecture/caching.md +101 -0
- package/skills/architecture/cloud-native.md +97 -0
- package/skills/architecture/message-queue.md +100 -0
- package/skills/architecture/security-arch.md +105 -0
- package/skills/development/cpp.md +127 -0
- package/skills/development/go.md +129 -0
- package/skills/development/java.md +134 -0
- package/skills/development/kotlin.md +140 -0
- package/skills/development/mobile-crossplatform.md +144 -0
- package/skills/development/python.md +109 -0
- package/skills/development/rust.md +127 -0
- package/skills/development/shell.md +127 -0
- package/skills/development/swift.md +129 -0
- package/skills/development/typescript.md +129 -0
- package/skills/devops/ci-cd.md +108 -0
- package/skills/devops/containerization.md +106 -0
- package/skills/devops/cost-optimization.md +105 -0
- package/skills/devops/data-engineering.md +181 -0
- package/skills/devops/database.md +95 -0
- package/skills/devops/dependency-management.md +91 -0
- package/skills/devops/devsecops.md +96 -0
- package/skills/devops/git-workflow.md +100 -0
- package/skills/devops/incident-response.md +167 -0
- package/skills/devops/monorepo.md +87 -0
- package/skills/devops/observability.md +103 -0
- package/skills/devops/performance.md +104 -0
- package/skills/devops/testing.md +104 -0
- package/skills/docs/adr-workflow.md +32 -0
- package/skills/docs/backlog-proposal-workflow.md +19 -0
- package/skills/docs/customer-profile-workflow.md +22 -0
- package/skills/docs/evidence-ingest-workflow.md +23 -0
- package/skills/docs/init-docs.md +160 -0
- package/skills/docs/init-project.md +50 -0
- package/skills/docs/prd-workflow.md +57 -0
- package/skills/docs/prfaq-workflow.md +25 -0
- package/skills/docs/product-intelligence-review.md +22 -0
- package/skills/docs/product-intelligence-workflow.md +62 -0
- package/skills/docs/product-signal-workflow.md +27 -0
- package/skills/docs/research-workflow.md +28 -0
- package/skills/docs/runbook-workflow.md +24 -0
- package/skills/exploration/repo-map.md +297 -0
- package/skills/frameworks/django.md +159 -0
- package/skills/frameworks/nextjs.md +148 -0
- package/skills/frameworks/react.md +132 -0
- package/skills/frameworks/spring-boot.md +165 -0
- package/skills/frontend-design/accessibility.md +153 -0
- package/skills/frontend-design/component-patterns.md +111 -0
- package/skills/frontend-design/engineering.md +122 -0
- package/skills/frontend-design/state-management.md +118 -0
- package/skills/frontend-design/ui-aesthetics.md +102 -0
- package/skills/frontend-design/ux-principles.md +126 -0
- package/skills/quality-gates/review-work.md +90 -0
- package/skills/quality-gates/verify-change.md +94 -0
- package/skills/quality-gates/verify-module.md +96 -0
- package/skills/quality-gates/verify-quality.md +93 -0
- package/skills/quality-gates/verify-security.md +95 -0
- package/skills/roles/architect.ai-systems.md +37 -0
- package/skills/roles/architect.data.md +37 -0
- package/skills/roles/architect.enterprise.md +37 -0
- package/skills/roles/architect.integration.md +37 -0
- package/skills/roles/architect.md +68 -0
- package/skills/roles/architect.platform.md +37 -0
- package/skills/roles/data-analyst.experiment.md +37 -0
- package/skills/roles/data-analyst.md +68 -0
- package/skills/roles/data-analyst.product-intelligence.md +37 -0
- package/skills/roles/data-analyst.product.md +37 -0
- package/skills/roles/data-analyst.telemetry.md +37 -0
- package/skills/roles/data-engineer.pipeline.md +37 -0
- package/skills/roles/data-engineer.vector-retrieval.md +37 -0
- package/skills/roles/data-engineer.warehouse.md +37 -0
- package/skills/roles/debugger.md +68 -0
- package/skills/roles/designer.accessibility.md +43 -0
- package/skills/roles/designer.md +68 -0
- package/skills/roles/engineer.ai.md +49 -0
- package/skills/roles/engineer.data.md +49 -0
- package/skills/roles/engineer.md +85 -0
- package/skills/roles/engineer.platform.md +49 -0
- package/skills/roles/operator.docs.md +43 -0
- package/skills/roles/operator.md +68 -0
- package/skills/roles/operator.release.md +43 -0
- package/skills/roles/operator.sre.md +43 -0
- package/skills/roles/orchestrator.md +66 -0
- package/skills/roles/product-manager.ai-product.md +37 -0
- package/skills/roles/product-manager.business-strategy.md +43 -0
- package/skills/roles/product-manager.enterprise.md +37 -0
- package/skills/roles/product-manager.growth.md +37 -0
- package/skills/roles/product-manager.md +67 -0
- package/skills/roles/product-manager.platform.md +37 -0
- package/skills/roles/product-manager.product.md +37 -0
- package/skills/roles/qa.ai-eval.md +37 -0
- package/skills/roles/qa.api-contract.md +37 -0
- package/skills/roles/qa.data-pipeline.md +37 -0
- package/skills/roles/qa.md +68 -0
- package/skills/roles/qa.test-automation.md +49 -0
- package/skills/roles/qa.web-ui.md +37 -0
- package/skills/roles/researcher.explorer.md +43 -0
- package/skills/roles/researcher.md +68 -0
- package/skills/roles/researcher.ux.md +43 -0
- package/skills/roles/reviewer.devil-advocate.md +43 -0
- package/skills/roles/reviewer.evaluator.md +43 -0
- package/skills/roles/reviewer.md +68 -0
- package/skills/roles/reviewer.trace.md +43 -0
- package/skills/roles/security.ai.md +37 -0
- package/skills/roles/security.appsec.md +37 -0
- package/skills/roles/security.cloud.md +37 -0
- package/skills/roles/security.legal-compliance.md +49 -0
- package/skills/roles/security.md +68 -0
- package/skills/roles/security.privacy.md +37 -0
- package/skills/roles/security.supply-chain.md +37 -0
- package/skills/routing.md +139 -0
- package/skills/security/blue-team.md +83 -0
- package/skills/security/code-audit.md +94 -0
- package/skills/security/pentest.md +84 -0
- package/skills/security/red-team.md +81 -0
- package/skills/security/threat-intel.md +89 -0
- package/skills/security/vuln-research.md +87 -0
- package/skills/utility/clean-code.md +165 -0
- package/sync-agents.mjs +899 -0
- package/templates/docs/adr.md +27 -0
- package/templates/docs/backlog-proposal.md +26 -0
- package/templates/docs/customer-profile.md +34 -0
- package/templates/docs/evidence-brief.md +37 -0
- package/templates/docs/incident-report.md +46 -0
- package/templates/docs/memo.md +27 -0
- package/templates/docs/meta-prd.md +56 -0
- package/templates/docs/one-pager.md +24 -0
- package/templates/docs/prd-business.md +61 -0
- package/templates/docs/prd-platform.md +62 -0
- package/templates/docs/prd.md +47 -0
- package/templates/docs/prfaq.md +31 -0
- package/templates/docs/product-intelligence-report.md +31 -0
- package/templates/docs/research-brief.md +29 -0
- package/templates/docs/rfc-platform.md +60 -0
- package/templates/docs/rfc.md +46 -0
- package/templates/docs/runbook.md +33 -0
- package/templates/docs/signal-brief.md +25 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/storage/backend.mjs — shared storage backend helpers for SQL/vector wiring.
|
|
4
|
+
*/
|
|
5
|
+
import postgres from 'postgres';
|
|
6
|
+
|
|
7
|
+
function cleanUrl(url) {
|
|
8
|
+
return String(url || '').trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createSqlClient(env = process.env) {
|
|
12
|
+
const databaseUrl = cleanUrl(env.DATABASE_URL);
|
|
13
|
+
if (!databaseUrl) return null;
|
|
14
|
+
return postgres(databaseUrl, {
|
|
15
|
+
max: Number.parseInt(env.CONSTRUCT_DB_POOL_SIZE || '5', 10),
|
|
16
|
+
idle_timeout: Number.parseInt(env.CONSTRUCT_DB_IDLE_TIMEOUT_MS || '30000', 10),
|
|
17
|
+
connect_timeout: Number.parseInt(env.CONSTRUCT_DB_CONNECT_TIMEOUT_MS || '5000', 10),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function probeSqlClient(client) {
|
|
22
|
+
if (!client) return { status: 'unavailable', message: 'No SQL client configured' };
|
|
23
|
+
try {
|
|
24
|
+
await client`select 1 as ok`;
|
|
25
|
+
return { status: 'healthy', message: 'Shared Postgres store reachable' };
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return { status: 'degraded', message: `SQL store unreachable: ${error?.message || 'unknown error'}` };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function closeSqlClient(client) {
|
|
32
|
+
if (!client) return;
|
|
33
|
+
await client.end({ timeout: 5 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readVectorConfig(env = process.env) {
|
|
37
|
+
const endpoint = cleanUrl(env.CONSTRUCT_VECTOR_URL);
|
|
38
|
+
const indexPath = cleanUrl(env.CONSTRUCT_VECTOR_INDEX_PATH);
|
|
39
|
+
return {
|
|
40
|
+
endpoint: endpoint || null,
|
|
41
|
+
indexPath: indexPath || null,
|
|
42
|
+
model: cleanUrl(env.CONSTRUCT_VECTOR_MODEL) || null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/storage/embeddings.mjs — deterministic local embeddings for hybrid retrieval.
|
|
4
|
+
*/
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export const EMBEDDING_MODEL = 'hashing-bow-v1';
|
|
8
|
+
export const EMBEDDING_DIMENSIONS = 256;
|
|
9
|
+
|
|
10
|
+
function tokenize(text) {
|
|
11
|
+
return String(text || '')
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.split(/[^a-z0-9]+/)
|
|
14
|
+
.filter((term) => term.length >= 3);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function bucketForToken(token, dimensions) {
|
|
18
|
+
const hash = crypto.createHash('sha256').update(token).digest();
|
|
19
|
+
return hash.readUInt32BE(0) % dimensions;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function signForToken(token) {
|
|
23
|
+
const hash = crypto.createHash('sha256').update(token).digest();
|
|
24
|
+
return (hash[4] & 1) === 0 ? 1 : -1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function embedText(text, dimensions = EMBEDDING_DIMENSIONS) {
|
|
28
|
+
const vector = new Array(dimensions).fill(0);
|
|
29
|
+
const tokens = tokenize(text);
|
|
30
|
+
if (tokens.length === 0) return vector;
|
|
31
|
+
|
|
32
|
+
for (const token of tokens) {
|
|
33
|
+
const bucket = bucketForToken(token, dimensions);
|
|
34
|
+
vector[bucket] += signForToken(token);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const norm = Math.sqrt(vector.reduce((sum, value) => sum + (value * value), 0));
|
|
38
|
+
if (norm === 0) return vector;
|
|
39
|
+
return vector.map((value) => Number((value / norm).toFixed(8)));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function cosineSimilarity(left = [], right = []) {
|
|
43
|
+
const length = Math.min(left.length, right.length);
|
|
44
|
+
if (length === 0) return 0;
|
|
45
|
+
let dot = 0;
|
|
46
|
+
let leftNorm = 0;
|
|
47
|
+
let rightNorm = 0;
|
|
48
|
+
for (let i = 0; i < length; i += 1) {
|
|
49
|
+
const lv = Number(left[i] || 0);
|
|
50
|
+
const rv = Number(right[i] || 0);
|
|
51
|
+
dot += lv * rv;
|
|
52
|
+
leftNorm += lv * lv;
|
|
53
|
+
rightNorm += rv * rv;
|
|
54
|
+
}
|
|
55
|
+
const denom = Math.sqrt(leftNorm) * Math.sqrt(rightNorm);
|
|
56
|
+
return denom > 0 ? dot / denom : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function scoreEmbeddedDocuments(documents = [], query, { limit = 10 } = {}) {
|
|
60
|
+
const queryEmbedding = embedText(query);
|
|
61
|
+
return documents
|
|
62
|
+
.map((document) => ({
|
|
63
|
+
document,
|
|
64
|
+
score: cosineSimilarity(queryEmbedding, document.embedding || []),
|
|
65
|
+
}))
|
|
66
|
+
.filter(({ score }) => score > 0)
|
|
67
|
+
.sort((a, b) => b.score - a.score || String(a.document.id || '').localeCompare(String(b.document.id || '')))
|
|
68
|
+
.slice(0, limit)
|
|
69
|
+
.map(({ document, score }) => ({ ...document, score }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/storage/hybrid-query.mjs — hybrid file + SQL + semantic retrieval.
|
|
4
|
+
*/
|
|
5
|
+
import { loadStateSnapshot, summarizeStateSnapshot } from './state-source.mjs';
|
|
6
|
+
import { describeSqlStore } from './sql-store.mjs';
|
|
7
|
+
import { describeVectorStore, vectorSearchLocal } from './vector-store.mjs';
|
|
8
|
+
import { createSqlClient, closeSqlClient, readVectorConfig } from './backend.mjs';
|
|
9
|
+
import { EMBEDDING_MODEL, embedText, scoreEmbeddedDocuments } from './embeddings.mjs';
|
|
10
|
+
|
|
11
|
+
function collectFileCandidates(snapshot) {
|
|
12
|
+
const docs = [];
|
|
13
|
+
|
|
14
|
+
if (snapshot.context) {
|
|
15
|
+
docs.push({
|
|
16
|
+
id: '.cx/context.json',
|
|
17
|
+
kind: 'context',
|
|
18
|
+
title: 'Context state',
|
|
19
|
+
summary: snapshot.context.contextSummary || snapshot.context.summary || null,
|
|
20
|
+
body: JSON.stringify(snapshot.context, null, 2),
|
|
21
|
+
tags: ['context', 'state', 'cx'],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (snapshot.workflow) {
|
|
26
|
+
docs.push({
|
|
27
|
+
id: '.cx/workflow.json',
|
|
28
|
+
kind: 'workflow',
|
|
29
|
+
title: snapshot.workflow.title || 'Workflow state',
|
|
30
|
+
summary: snapshot.workflow.summary || null,
|
|
31
|
+
body: JSON.stringify(snapshot.workflow, null, 2),
|
|
32
|
+
tags: ['workflow', 'state', 'cx'],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (snapshot.architecture) {
|
|
37
|
+
docs.push({
|
|
38
|
+
id: 'docs/architecture.md',
|
|
39
|
+
kind: 'architecture',
|
|
40
|
+
title: 'Architecture docs',
|
|
41
|
+
summary: snapshot.architecture.slice(0, 240),
|
|
42
|
+
body: snapshot.architecture,
|
|
43
|
+
tags: ['architecture', 'docs'],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (snapshot.docsReadme) {
|
|
48
|
+
docs.push({
|
|
49
|
+
id: 'docs/README.md',
|
|
50
|
+
kind: 'docs',
|
|
51
|
+
title: 'Docs index',
|
|
52
|
+
summary: snapshot.docsReadme.slice(0, 240),
|
|
53
|
+
body: snapshot.docsReadme,
|
|
54
|
+
tags: ['docs', 'index'],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const doc of snapshot.productIntelDocs ?? []) {
|
|
59
|
+
const kind = doc.path.startsWith('docs/prd/')
|
|
60
|
+
? 'prd'
|
|
61
|
+
: doc.path.startsWith('docs/meta-prd/')
|
|
62
|
+
? 'meta-prd'
|
|
63
|
+
: 'product-intel';
|
|
64
|
+
docs.push({
|
|
65
|
+
id: doc.path,
|
|
66
|
+
kind,
|
|
67
|
+
title: doc.title,
|
|
68
|
+
summary: doc.body.slice(0, 240),
|
|
69
|
+
body: doc.body,
|
|
70
|
+
tags: ['product-intel', kind],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return docs;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildHybridSearchResults(rootDir, query, { limit = 10, env = process.env } = {}) {
|
|
78
|
+
const snapshot = loadStateSnapshot(rootDir);
|
|
79
|
+
const fileCandidates = collectFileCandidates(snapshot);
|
|
80
|
+
const fileHits = vectorSearchLocal(fileCandidates, query, { limit });
|
|
81
|
+
|
|
82
|
+
const sqlStore = describeSqlStore(env);
|
|
83
|
+
const vectorStore = describeVectorStore(env);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
query,
|
|
87
|
+
summary: summarizeStateSnapshot(snapshot),
|
|
88
|
+
stores: {
|
|
89
|
+
file: { configured: true, mode: 'canonical' },
|
|
90
|
+
sql: sqlStore,
|
|
91
|
+
vector: vectorStore,
|
|
92
|
+
},
|
|
93
|
+
results: fileHits.map((hit) => ({
|
|
94
|
+
id: hit.id,
|
|
95
|
+
kind: hit.kind,
|
|
96
|
+
title: hit.title,
|
|
97
|
+
summary: hit.summary,
|
|
98
|
+
score: hit.score,
|
|
99
|
+
})),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function buildHybridSearchResultsAsync(rootDir, query, { limit = 10, env = process.env } = {}) {
|
|
104
|
+
const base = buildHybridSearchResults(rootDir, query, { limit, env });
|
|
105
|
+
const client = createSqlClient(env);
|
|
106
|
+
if (!client) return base;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const embeddingRows = await client`
|
|
110
|
+
select d.id, d.kind, d.title, d.summary, d.body, d.source_path, d.tags, e.embedding
|
|
111
|
+
from construct_documents d
|
|
112
|
+
join construct_embeddings e on e.document_id = d.id
|
|
113
|
+
where d.project = 'construct' and e.model = ${EMBEDDING_MODEL}
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
const embeddedHits = scoreEmbeddedDocuments(
|
|
117
|
+
embeddingRows.map((row) => ({
|
|
118
|
+
...row,
|
|
119
|
+
embedding: row.embedding,
|
|
120
|
+
})),
|
|
121
|
+
query,
|
|
122
|
+
{ limit },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const sqlHits = await client`
|
|
126
|
+
select id, kind, title, summary, body, source_path
|
|
127
|
+
from construct_documents
|
|
128
|
+
where project = 'construct'
|
|
129
|
+
and (title ilike ${`%${query}%`} or coalesce(summary, '') ilike ${`%${query}%`} or body ilike ${`%${query}%`})
|
|
130
|
+
order by updated_at desc
|
|
131
|
+
limit ${limit}
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
const merged = [...base.results];
|
|
135
|
+
for (const hit of embeddedHits) {
|
|
136
|
+
if (merged.some((entry) => entry.id === hit.id)) continue;
|
|
137
|
+
merged.push({
|
|
138
|
+
id: hit.id,
|
|
139
|
+
kind: hit.kind,
|
|
140
|
+
title: hit.title,
|
|
141
|
+
summary: hit.summary,
|
|
142
|
+
score: hit.score,
|
|
143
|
+
source_path: hit.source_path,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
for (const hit of sqlHits) {
|
|
147
|
+
if (merged.some((entry) => entry.id === hit.id)) continue;
|
|
148
|
+
merged.push({
|
|
149
|
+
id: hit.id,
|
|
150
|
+
kind: hit.kind,
|
|
151
|
+
title: hit.title,
|
|
152
|
+
summary: hit.summary,
|
|
153
|
+
score: 1,
|
|
154
|
+
source_path: hit.source_path,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
...base,
|
|
160
|
+
results: merged.slice(0, limit),
|
|
161
|
+
stores: {
|
|
162
|
+
...base.stores,
|
|
163
|
+
vector: {
|
|
164
|
+
...base.stores.vector,
|
|
165
|
+
...readVectorConfig(env),
|
|
166
|
+
model: EMBEDDING_MODEL,
|
|
167
|
+
},
|
|
168
|
+
sql: {
|
|
169
|
+
...base.stores.sql,
|
|
170
|
+
mode: 'postgres',
|
|
171
|
+
configured: true,
|
|
172
|
+
sharedReady: true,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return {
|
|
178
|
+
...base,
|
|
179
|
+
error: error?.message || 'hybrid search failed',
|
|
180
|
+
};
|
|
181
|
+
} finally {
|
|
182
|
+
await closeSqlClient(client);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/storage/sql-store.mjs — lightweight shared SQL storage facade.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { createSqlClient, probeSqlClient } from './backend.mjs';
|
|
7
|
+
|
|
8
|
+
export function hasSqlStore(env = process.env) {
|
|
9
|
+
return Boolean(env.DATABASE_URL || env.CONSTRUCT_SQLITE_PATH || env.CONSTRUCT_DB_PATH);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function detectStoreMode(env = process.env) {
|
|
13
|
+
if (env.DATABASE_URL) return 'postgres';
|
|
14
|
+
if (env.CONSTRUCT_SQLITE_PATH || env.CONSTRUCT_DB_PATH) return 'sqlite';
|
|
15
|
+
return 'file';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function describeSqlStore(env = process.env) {
|
|
19
|
+
const mode = detectStoreMode(env);
|
|
20
|
+
return {
|
|
21
|
+
mode,
|
|
22
|
+
configured: mode !== 'file',
|
|
23
|
+
sharedReady: mode === 'postgres',
|
|
24
|
+
fallbackAvailable: mode === 'file' || mode === 'sqlite',
|
|
25
|
+
path: env.CONSTRUCT_SQLITE_PATH || env.CONSTRUCT_DB_PATH || null,
|
|
26
|
+
hasDatabaseUrl: Boolean(env.DATABASE_URL),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function describeSqlStoreHealth(env = process.env) {
|
|
31
|
+
const store = describeSqlStore(env);
|
|
32
|
+
if (store.mode !== 'postgres') return sqlStoreHealth(env);
|
|
33
|
+
const client = createSqlClient(env);
|
|
34
|
+
try {
|
|
35
|
+
return await probeSqlClient(client);
|
|
36
|
+
} finally {
|
|
37
|
+
if (client) await client.end({ timeout: 5 }).catch(() => {});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sqlStoreHealth(env = process.env) {
|
|
42
|
+
const store = describeSqlStore(env);
|
|
43
|
+
if (store.mode === 'postgres') {
|
|
44
|
+
return { status: 'configured', message: 'Shared Postgres store is configured' };
|
|
45
|
+
}
|
|
46
|
+
if (store.mode === 'sqlite') {
|
|
47
|
+
return { status: 'configured', message: 'SQLite-backed store is configured' };
|
|
48
|
+
}
|
|
49
|
+
return { status: 'unavailable', message: 'No SQL store configured; using file-state only' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function canOpenLocalSqlite(env = process.env) {
|
|
53
|
+
const candidate = env.CONSTRUCT_SQLITE_PATH || env.CONSTRUCT_DB_PATH || null;
|
|
54
|
+
return candidate ? existsSync(candidate) : false;
|
|
55
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/storage/state-source.mjs — normalize file-state artifacts for hybrid retrieval.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { basename, extname, join, relative } from 'node:path';
|
|
7
|
+
|
|
8
|
+
function readJsonIfExists(filePath) {
|
|
9
|
+
if (!existsSync(filePath)) return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readTextIfExists(filePath) {
|
|
18
|
+
if (!existsSync(filePath)) return null;
|
|
19
|
+
try {
|
|
20
|
+
return readFileSync(filePath, 'utf8');
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function titleFromPath(filePath) {
|
|
27
|
+
return basename(filePath, extname(filePath)).replace(/[-_]+/g, ' ');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function collectTextFiles(rootDir, dirs, { extensions = new Set(['.md', '.json']) } = {}) {
|
|
31
|
+
const files = [];
|
|
32
|
+
for (const dir of dirs) {
|
|
33
|
+
const abs = join(rootDir, dir);
|
|
34
|
+
if (!existsSync(abs)) continue;
|
|
35
|
+
const stack = [abs];
|
|
36
|
+
while (stack.length > 0) {
|
|
37
|
+
const current = stack.pop();
|
|
38
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
39
|
+
const full = join(current, entry.name);
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
stack.push(full);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!entry.isFile() || !extensions.has(extname(entry.name))) continue;
|
|
45
|
+
try {
|
|
46
|
+
const stat = statSync(full);
|
|
47
|
+
if (stat.size > 512_000) continue;
|
|
48
|
+
const body = readFileSync(full, 'utf8');
|
|
49
|
+
files.push({
|
|
50
|
+
path: relative(rootDir, full),
|
|
51
|
+
title: titleFromPath(full),
|
|
52
|
+
body,
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
// Ignore unreadable files in optional product intelligence folders.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function loadStateSnapshot(rootDir) {
|
|
64
|
+
const contextJsonPath = join(rootDir, '.cx', 'context.json');
|
|
65
|
+
const workflowJsonPath = join(rootDir, '.cx', 'workflow.json');
|
|
66
|
+
const architecturePath = join(rootDir, 'docs', 'architecture.md');
|
|
67
|
+
const docsReadmePath = join(rootDir, 'docs', 'README.md');
|
|
68
|
+
const productIntelDocs = collectTextFiles(rootDir, [
|
|
69
|
+
'.cx/product-intel',
|
|
70
|
+
'docs/prd',
|
|
71
|
+
'docs/meta-prd',
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
context: readJsonIfExists(contextJsonPath),
|
|
76
|
+
workflow: readJsonIfExists(workflowJsonPath),
|
|
77
|
+
architecture: readTextIfExists(architecturePath),
|
|
78
|
+
docsReadme: readTextIfExists(docsReadmePath),
|
|
79
|
+
productIntelDocs,
|
|
80
|
+
paths: {
|
|
81
|
+
contextJsonPath,
|
|
82
|
+
workflowJsonPath,
|
|
83
|
+
architecturePath,
|
|
84
|
+
docsReadmePath,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function summarizeStateSnapshot(snapshot) {
|
|
90
|
+
return {
|
|
91
|
+
contextSummary: snapshot.context?.contextSummary ?? snapshot.context?.summary ?? null,
|
|
92
|
+
workflowTitle: snapshot.workflow?.title ?? null,
|
|
93
|
+
workflowPhase: snapshot.workflow?.phase ?? null,
|
|
94
|
+
workflowStatus: snapshot.workflow?.status ?? null,
|
|
95
|
+
lastContextSavedAt: snapshot.context?.savedAt ?? null,
|
|
96
|
+
lastWorkflowUpdatedAt: snapshot.workflow?.updatedAt ?? null,
|
|
97
|
+
hasArchitectureDoc: Boolean(snapshot.architecture),
|
|
98
|
+
hasDocsReadme: Boolean(snapshot.docsReadme),
|
|
99
|
+
productIntelDocCount: snapshot.productIntelDocs?.length ?? 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/storage/sync.mjs — sync file-state artifacts into shared Postgres indices.
|
|
4
|
+
*/
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import { loadStateSnapshot } from './state-source.mjs';
|
|
7
|
+
import { createSqlClient, closeSqlClient, readVectorConfig } from './backend.mjs';
|
|
8
|
+
import { EMBEDDING_MODEL, embedText } from './embeddings.mjs';
|
|
9
|
+
|
|
10
|
+
function hashContent(content) {
|
|
11
|
+
return crypto.createHash('sha256').update(String(content || '')).digest('hex');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toDocumentRows(rootDir, snapshot, project = 'construct') {
|
|
15
|
+
const rows = [];
|
|
16
|
+
|
|
17
|
+
if (snapshot.context) {
|
|
18
|
+
const body = JSON.stringify(snapshot.context, null, 2);
|
|
19
|
+
rows.push({
|
|
20
|
+
id: `${project}:context`,
|
|
21
|
+
project,
|
|
22
|
+
kind: 'context',
|
|
23
|
+
title: 'Context state',
|
|
24
|
+
summary: snapshot.context.contextSummary || snapshot.context.summary || null,
|
|
25
|
+
body,
|
|
26
|
+
source_path: '.cx/context.json',
|
|
27
|
+
tags: ['context', 'state', 'cx'],
|
|
28
|
+
content_hash: hashContent(body),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (snapshot.workflow) {
|
|
33
|
+
const body = JSON.stringify(snapshot.workflow, null, 2);
|
|
34
|
+
rows.push({
|
|
35
|
+
id: `${project}:workflow`,
|
|
36
|
+
project,
|
|
37
|
+
kind: 'workflow',
|
|
38
|
+
title: snapshot.workflow.title || 'Workflow state',
|
|
39
|
+
summary: snapshot.workflow.summary || null,
|
|
40
|
+
body,
|
|
41
|
+
source_path: '.cx/workflow.json',
|
|
42
|
+
tags: ['workflow', 'state', 'cx'],
|
|
43
|
+
content_hash: hashContent(body),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (snapshot.architecture) {
|
|
48
|
+
const body = snapshot.architecture;
|
|
49
|
+
rows.push({
|
|
50
|
+
id: `${project}:architecture`,
|
|
51
|
+
project,
|
|
52
|
+
kind: 'architecture',
|
|
53
|
+
title: 'Architecture docs',
|
|
54
|
+
summary: body.slice(0, 240),
|
|
55
|
+
body,
|
|
56
|
+
source_path: 'docs/architecture.md',
|
|
57
|
+
tags: ['architecture', 'docs'],
|
|
58
|
+
content_hash: hashContent(body),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (snapshot.docsReadme) {
|
|
63
|
+
const body = snapshot.docsReadme;
|
|
64
|
+
rows.push({
|
|
65
|
+
id: `${project}:docs-readme`,
|
|
66
|
+
project,
|
|
67
|
+
kind: 'docs',
|
|
68
|
+
title: 'Docs index',
|
|
69
|
+
summary: body.slice(0, 240),
|
|
70
|
+
body,
|
|
71
|
+
source_path: 'docs/README.md',
|
|
72
|
+
tags: ['docs', 'index'],
|
|
73
|
+
content_hash: hashContent(body),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const doc of snapshot.productIntelDocs ?? []) {
|
|
78
|
+
const body = doc.body;
|
|
79
|
+
const rel = doc.path;
|
|
80
|
+
const kind = rel.startsWith('docs/prd/')
|
|
81
|
+
? 'prd'
|
|
82
|
+
: rel.startsWith('docs/meta-prd/')
|
|
83
|
+
? 'meta-prd'
|
|
84
|
+
: 'product-intel';
|
|
85
|
+
rows.push({
|
|
86
|
+
id: `${project}:${rel}`,
|
|
87
|
+
project,
|
|
88
|
+
kind,
|
|
89
|
+
title: doc.title,
|
|
90
|
+
summary: body.slice(0, 240),
|
|
91
|
+
body,
|
|
92
|
+
source_path: rel,
|
|
93
|
+
tags: ['product-intel', kind],
|
|
94
|
+
content_hash: hashContent(body),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return rows;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function ensureHybridSchema(client) {
|
|
102
|
+
const schema = await import('node:fs/promises').then((fs) => fs.readFile(new URL('../../db/migrations/001_init.sql', import.meta.url), 'utf8'));
|
|
103
|
+
await client.unsafe(schema);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function syncFileStateToSql(rootDir, { env = process.env, project = 'construct' } = {}) {
|
|
107
|
+
const client = createSqlClient(env);
|
|
108
|
+
if (!client) return { status: 'unavailable', note: 'No DATABASE_URL configured' };
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await ensureHybridSchema(client);
|
|
112
|
+
const snapshot = loadStateSnapshot(rootDir);
|
|
113
|
+
const rows = toDocumentRows(rootDir, snapshot, project);
|
|
114
|
+
|
|
115
|
+
let documentsSynced = 0;
|
|
116
|
+
let embeddingsSynced = 0;
|
|
117
|
+
for (const row of rows) {
|
|
118
|
+
await client`
|
|
119
|
+
insert into construct_documents (id, project, kind, title, summary, body, source_path, tags, content_hash, updated_at)
|
|
120
|
+
values (${row.id}, ${row.project}, ${row.kind}, ${row.title}, ${row.summary}, ${row.body}, ${row.source_path}, ${JSON.stringify(row.tags)}, ${row.content_hash}, now())
|
|
121
|
+
on conflict (id) do update set
|
|
122
|
+
project = excluded.project,
|
|
123
|
+
kind = excluded.kind,
|
|
124
|
+
title = excluded.title,
|
|
125
|
+
summary = excluded.summary,
|
|
126
|
+
body = excluded.body,
|
|
127
|
+
source_path = excluded.source_path,
|
|
128
|
+
tags = excluded.tags,
|
|
129
|
+
content_hash = excluded.content_hash,
|
|
130
|
+
updated_at = now()
|
|
131
|
+
`;
|
|
132
|
+
documentsSynced += 1;
|
|
133
|
+
|
|
134
|
+
const embedding = embedText([row.title, row.summary, row.body, row.source_path, row.kind].filter(Boolean).join('\n'));
|
|
135
|
+
await client`
|
|
136
|
+
insert into construct_embeddings (document_id, model, embedding, content_hash, updated_at)
|
|
137
|
+
values (${row.id}, ${EMBEDDING_MODEL}, ${embedding}, ${row.content_hash}, now())
|
|
138
|
+
on conflict (document_id) do update set
|
|
139
|
+
model = excluded.model,
|
|
140
|
+
embedding = excluded.embedding,
|
|
141
|
+
content_hash = excluded.content_hash,
|
|
142
|
+
updated_at = now()
|
|
143
|
+
`;
|
|
144
|
+
embeddingsSynced += 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await client`
|
|
148
|
+
insert into construct_sync_runs (project, source, documents_synced, embeddings_synced, status, note)
|
|
149
|
+
values (${project}, ${'file-state'}, ${documentsSynced}, ${0}, ${'ok'}, ${'synced file-state documents'})
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
status: 'ok',
|
|
154
|
+
documentsSynced,
|
|
155
|
+
embeddingsSynced,
|
|
156
|
+
embeddingModel: EMBEDDING_MODEL,
|
|
157
|
+
vector: readVectorConfig(env),
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return { status: 'degraded', error: error?.message || 'sync failed' };
|
|
161
|
+
} finally {
|
|
162
|
+
await closeSqlClient(client);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/storage/vector-store.mjs — derived semantic retrieval facade.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function vectorStoreMode(env = process.env) {
|
|
7
|
+
if (env.CONSTRUCT_VECTOR_URL) return 'remote';
|
|
8
|
+
if (env.CONSTRUCT_VECTOR_INDEX_PATH) return 'local';
|
|
9
|
+
return 'file';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function describeVectorStore(env = process.env) {
|
|
13
|
+
const mode = vectorStoreMode(env);
|
|
14
|
+
return {
|
|
15
|
+
mode,
|
|
16
|
+
configured: mode !== 'file',
|
|
17
|
+
sharedReady: mode === 'remote',
|
|
18
|
+
fallbackAvailable: true,
|
|
19
|
+
endpoint: env.CONSTRUCT_VECTOR_URL || null,
|
|
20
|
+
indexPath: env.CONSTRUCT_VECTOR_INDEX_PATH || null,
|
|
21
|
+
model: env.CONSTRUCT_VECTOR_MODEL || null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeTerms(input) {
|
|
26
|
+
return String(input || '')
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.split(/[^a-z0-9]+/)
|
|
29
|
+
.filter((term) => term.length >= 3);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function scoreSemanticText(text, query) {
|
|
33
|
+
const terms = normalizeTerms(query);
|
|
34
|
+
if (terms.length === 0) return 0;
|
|
35
|
+
const haystack = String(text || '').toLowerCase();
|
|
36
|
+
let score = 0;
|
|
37
|
+
for (const term of terms) {
|
|
38
|
+
if (haystack.includes(term)) score += 1;
|
|
39
|
+
}
|
|
40
|
+
return score;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function vectorSearchLocal(records = [], query = '', { limit = 10 } = {}) {
|
|
44
|
+
return records
|
|
45
|
+
.map((record) => ({
|
|
46
|
+
record,
|
|
47
|
+
score: scoreSemanticText([
|
|
48
|
+
record.title,
|
|
49
|
+
record.summary,
|
|
50
|
+
record.body,
|
|
51
|
+
record.text,
|
|
52
|
+
Array.isArray(record.tags) ? record.tags.join(' ') : '',
|
|
53
|
+
].filter(Boolean).join('\n'), query),
|
|
54
|
+
}))
|
|
55
|
+
.filter(({ score }) => score > 0)
|
|
56
|
+
.sort((a, b) => b.score - a.score || String(a.record.id || '').localeCompare(String(b.record.id || '')))
|
|
57
|
+
.slice(0, limit)
|
|
58
|
+
.map(({ record, score }) => ({ ...record, score }));
|
|
59
|
+
}
|