@maadb/core 0.0.1 → 0.7.9
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/LICENSE +21 -0
- package/README.md +371 -3
- package/dist/architect.d.ts +2 -0
- package/dist/architect.d.ts.map +1 -0
- package/dist/architect.js +273 -0
- package/dist/architect.js.map +1 -0
- package/dist/auth/resolve.d.ts +36 -0
- package/dist/auth/resolve.d.ts.map +1 -0
- package/dist/auth/resolve.js +77 -0
- package/dist/auth/resolve.js.map +1 -0
- package/dist/auth/token-store.d.ts +82 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +464 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/auth/types.d.ts +75 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +15 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/backend/adapter.d.ts +57 -0
- package/dist/backend/adapter.d.ts.map +1 -0
- package/dist/backend/adapter.js +6 -0
- package/dist/backend/adapter.js.map +1 -0
- package/dist/backend/index.d.ts +3 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +2 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/sqlite/index.d.ts +63 -0
- package/dist/backend/sqlite/index.d.ts.map +1 -0
- package/dist/backend/sqlite/index.js +521 -0
- package/dist/backend/sqlite/index.js.map +1 -0
- package/dist/backend/sqlite/schema.d.ts +2 -0
- package/dist/backend/sqlite/schema.d.ts.map +1 -0
- package/dist/backend/sqlite/schema.js +88 -0
- package/dist/backend/sqlite/schema.js.map +1 -0
- package/dist/claude-md.d.ts +2 -0
- package/dist/claude-md.d.ts.map +1 -0
- package/dist/claude-md.js +97 -0
- package/dist/claude-md.js.map +1 -0
- package/dist/cli/commands/audit.d.ts +4 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +39 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +8 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +242 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/discover.d.ts +5 -0
- package/dist/cli/commands/discover.d.ts.map +1 -0
- package/dist/cli/commands/discover.js +48 -0
- package/dist/cli/commands/discover.js.map +1 -0
- package/dist/cli/commands/maintain.d.ts +6 -0
- package/dist/cli/commands/maintain.d.ts.map +1 -0
- package/dist/cli/commands/maintain.js +168 -0
- package/dist/cli/commands/maintain.js.map +1 -0
- package/dist/cli/commands/read.d.ts +7 -0
- package/dist/cli/commands/read.d.ts.map +1 -0
- package/dist/cli/commands/read.js +139 -0
- package/dist/cli/commands/read.js.map +1 -0
- package/dist/cli/commands/write.d.ts +4 -0
- package/dist/cli/commands/write.d.ts.map +1 -0
- package/dist/cli/commands/write.js +99 -0
- package/dist/cli/commands/write.js.map +1 -0
- package/dist/cli/helpers.d.ts +8 -0
- package/dist/cli/helpers.d.ts.map +1 -0
- package/dist/cli/helpers.js +17 -0
- package/dist/cli/helpers.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +296 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -0
- package/dist/engine/audit.d.ts +15 -0
- package/dist/engine/audit.d.ts.map +1 -0
- package/dist/engine/audit.js +42 -0
- package/dist/engine/audit.js.map +1 -0
- package/dist/engine/composites.d.ts +6 -0
- package/dist/engine/composites.d.ts.map +1 -0
- package/dist/engine/composites.js +102 -0
- package/dist/engine/composites.js.map +1 -0
- package/dist/engine/context.d.ts +47 -0
- package/dist/engine/context.d.ts.map +1 -0
- package/dist/engine/context.js +55 -0
- package/dist/engine/context.js.map +1 -0
- package/dist/engine/docid-safe.d.ts +8 -0
- package/dist/engine/docid-safe.d.ts.map +1 -0
- package/dist/engine/docid-safe.js +0 -0
- package/dist/engine/docid-safe.js.map +1 -0
- package/dist/engine/helpers.d.ts +8 -0
- package/dist/engine/helpers.d.ts.map +1 -0
- package/dist/engine/helpers.js +102 -0
- package/dist/engine/helpers.js.map +1 -0
- package/dist/engine/index.d.ts +209 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +489 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/indexing.d.ts +14 -0
- package/dist/engine/indexing.d.ts.map +1 -0
- package/dist/engine/indexing.js +225 -0
- package/dist/engine/indexing.js.map +1 -0
- package/dist/engine/journal.d.ts +32 -0
- package/dist/engine/journal.d.ts.map +1 -0
- package/dist/engine/journal.js +111 -0
- package/dist/engine/journal.js.map +1 -0
- package/dist/engine/logger.d.ts +19 -0
- package/dist/engine/logger.d.ts.map +1 -0
- package/dist/engine/logger.js +40 -0
- package/dist/engine/logger.js.map +1 -0
- package/dist/engine/maintenance.d.ts +15 -0
- package/dist/engine/maintenance.d.ts.map +1 -0
- package/dist/engine/maintenance.js +87 -0
- package/dist/engine/maintenance.js.map +1 -0
- package/dist/engine/mutex.d.ts +24 -0
- package/dist/engine/mutex.d.ts.map +1 -0
- package/dist/engine/mutex.js +50 -0
- package/dist/engine/mutex.js.map +1 -0
- package/dist/engine/pathguard.d.ts +16 -0
- package/dist/engine/pathguard.d.ts.map +1 -0
- package/dist/engine/pathguard.js +41 -0
- package/dist/engine/pathguard.js.map +1 -0
- package/dist/engine/reads.d.ts +24 -0
- package/dist/engine/reads.d.ts.map +1 -0
- package/dist/engine/reads.js +701 -0
- package/dist/engine/reads.js.map +1 -0
- package/dist/engine/types.d.ts +350 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/types.js +5 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/engine/writes.d.ts +10 -0
- package/dist/engine/writes.d.ts.map +1 -0
- package/dist/engine/writes.js +606 -0
- package/dist/engine/writes.js.map +1 -0
- package/dist/engine.d.ts +3 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +6 -0
- package/dist/engine.js.map +1 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -0
- package/dist/extractor/fields.d.ts +3 -0
- package/dist/extractor/fields.d.ts.map +1 -0
- package/dist/extractor/fields.js +58 -0
- package/dist/extractor/fields.js.map +1 -0
- package/dist/extractor/index.d.ts +7 -0
- package/dist/extractor/index.d.ts.map +1 -0
- package/dist/extractor/index.js +22 -0
- package/dist/extractor/index.js.map +1 -0
- package/dist/extractor/normalizers.d.ts +22 -0
- package/dist/extractor/normalizers.d.ts.map +1 -0
- package/dist/extractor/normalizers.js +250 -0
- package/dist/extractor/normalizers.js.map +1 -0
- package/dist/extractor/objects.d.ts +3 -0
- package/dist/extractor/objects.d.ts.map +1 -0
- package/dist/extractor/objects.js +33 -0
- package/dist/extractor/objects.js.map +1 -0
- package/dist/extractor/relationships.d.ts +3 -0
- package/dist/extractor/relationships.d.ts.map +1 -0
- package/dist/extractor/relationships.js +58 -0
- package/dist/extractor/relationships.js.map +1 -0
- package/dist/git/commit.d.ts +63 -0
- package/dist/git/commit.d.ts.map +1 -0
- package/dist/git/commit.js +161 -0
- package/dist/git/commit.js.map +1 -0
- package/dist/git/diff.d.ts +4 -0
- package/dist/git/diff.d.ts.map +1 -0
- package/dist/git/diff.js +56 -0
- package/dist/git/diff.js.map +1 -0
- package/dist/git/index.d.ts +41 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +86 -0
- package/dist/git/index.js.map +1 -0
- package/dist/git/log.d.ts +13 -0
- package/dist/git/log.d.ts.map +1 -0
- package/dist/git/log.js +116 -0
- package/dist/git/log.js.map +1 -0
- package/dist/git/snapshot.d.ts +4 -0
- package/dist/git/snapshot.d.ts.map +1 -0
- package/dist/git/snapshot.js +65 -0
- package/dist/git/snapshot.js.map +1 -0
- package/dist/instance/config.d.ts +19 -0
- package/dist/instance/config.d.ts.map +1 -0
- package/dist/instance/config.js +107 -0
- package/dist/instance/config.js.map +1 -0
- package/dist/instance/pool.d.ts +138 -0
- package/dist/instance/pool.d.ts.map +1 -0
- package/dist/instance/pool.js +335 -0
- package/dist/instance/pool.js.map +1 -0
- package/dist/instance/session.d.ts +140 -0
- package/dist/instance/session.d.ts.map +1 -0
- package/dist/instance/session.js +267 -0
- package/dist/instance/session.js.map +1 -0
- package/dist/logging.d.ts +108 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +120 -0
- package/dist/logging.js.map +1 -0
- package/dist/maad-md.d.ts +11 -0
- package/dist/maad-md.d.ts.map +1 -0
- package/dist/maad-md.js +252 -0
- package/dist/maad-md.js.map +1 -0
- package/dist/mcp/bulk-cap.d.ts +10 -0
- package/dist/mcp/bulk-cap.d.ts.map +1 -0
- package/dist/mcp/bulk-cap.js +37 -0
- package/dist/mcp/bulk-cap.js.map +1 -0
- package/dist/mcp/config.d.ts +18 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +20 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/ctx.d.ts +17 -0
- package/dist/mcp/ctx.d.ts.map +1 -0
- package/dist/mcp/ctx.js +7 -0
- package/dist/mcp/ctx.js.map +1 -0
- package/dist/mcp/guardrails.d.ts +21 -0
- package/dist/mcp/guardrails.d.ts.map +1 -0
- package/dist/mcp/guardrails.js +39 -0
- package/dist/mcp/guardrails.js.map +1 -0
- package/dist/mcp/idempotency.d.ts +69 -0
- package/dist/mcp/idempotency.d.ts.map +1 -0
- package/dist/mcp/idempotency.js +168 -0
- package/dist/mcp/idempotency.js.map +1 -0
- package/dist/mcp/instance-reload.d.ts +23 -0
- package/dist/mcp/instance-reload.d.ts.map +1 -0
- package/dist/mcp/instance-reload.js +126 -0
- package/dist/mcp/instance-reload.js.map +1 -0
- package/dist/mcp/kinds.d.ts +22 -0
- package/dist/mcp/kinds.d.ts.map +1 -0
- package/dist/mcp/kinds.js +110 -0
- package/dist/mcp/kinds.js.map +1 -0
- package/dist/mcp/lifecycle.d.ts +8 -0
- package/dist/mcp/lifecycle.d.ts.map +1 -0
- package/dist/mcp/lifecycle.js +65 -0
- package/dist/mcp/lifecycle.js.map +1 -0
- package/dist/mcp/notifications.d.ts +87 -0
- package/dist/mcp/notifications.d.ts.map +1 -0
- package/dist/mcp/notifications.js +154 -0
- package/dist/mcp/notifications.js.map +1 -0
- package/dist/mcp/query-depth.d.ts +28 -0
- package/dist/mcp/query-depth.d.ts.map +1 -0
- package/dist/mcp/query-depth.js +43 -0
- package/dist/mcp/query-depth.js.map +1 -0
- package/dist/mcp/rate-limit.d.ts +60 -0
- package/dist/mcp/rate-limit.d.ts.map +1 -0
- package/dist/mcp/rate-limit.js +203 -0
- package/dist/mcp/rate-limit.js.map +1 -0
- package/dist/mcp/reload-signal.d.ts +13 -0
- package/dist/mcp/reload-signal.d.ts.map +1 -0
- package/dist/mcp/reload-signal.js +68 -0
- package/dist/mcp/reload-signal.js.map +1 -0
- package/dist/mcp/response.d.ts +109 -0
- package/dist/mcp/response.d.ts.map +1 -0
- package/dist/mcp/response.js +132 -0
- package/dist/mcp/response.js.map +1 -0
- package/dist/mcp/roles.d.ts +7 -0
- package/dist/mcp/roles.d.ts.map +1 -0
- package/dist/mcp/roles.js +56 -0
- package/dist/mcp/roles.js.map +1 -0
- package/dist/mcp/server.d.ts +33 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +242 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/shutdown.d.ts +32 -0
- package/dist/mcp/shutdown.d.ts.map +1 -0
- package/dist/mcp/shutdown.js +130 -0
- package/dist/mcp/shutdown.js.map +1 -0
- package/dist/mcp/tools/audit.d.ts +4 -0
- package/dist/mcp/tools/audit.d.ts.map +1 -0
- package/dist/mcp/tools/audit.js +45 -0
- package/dist/mcp/tools/audit.js.map +1 -0
- package/dist/mcp/tools/auth.d.ts +4 -0
- package/dist/mcp/tools/auth.d.ts.map +1 -0
- package/dist/mcp/tools/auth.js +181 -0
- package/dist/mcp/tools/auth.js.map +1 -0
- package/dist/mcp/tools/discover.d.ts +4 -0
- package/dist/mcp/tools/discover.d.ts.map +1 -0
- package/dist/mcp/tools/discover.js +83 -0
- package/dist/mcp/tools/discover.js.map +1 -0
- package/dist/mcp/tools/instance.d.ts +15 -0
- package/dist/mcp/tools/instance.d.ts.map +1 -0
- package/dist/mcp/tools/instance.js +207 -0
- package/dist/mcp/tools/instance.js.map +1 -0
- package/dist/mcp/tools/maintain.d.ts +4 -0
- package/dist/mcp/tools/maintain.d.ts.map +1 -0
- package/dist/mcp/tools/maintain.js +118 -0
- package/dist/mcp/tools/maintain.js.map +1 -0
- package/dist/mcp/tools/read.d.ts +4 -0
- package/dist/mcp/tools/read.d.ts.map +1 -0
- package/dist/mcp/tools/read.js +260 -0
- package/dist/mcp/tools/read.js.map +1 -0
- package/dist/mcp/tools/write.d.ts +4 -0
- package/dist/mcp/tools/write.d.ts.map +1 -0
- package/dist/mcp/tools/write.js +342 -0
- package/dist/mcp/tools/write.js.map +1 -0
- package/dist/mcp/transport/auth.d.ts +41 -0
- package/dist/mcp/transport/auth.d.ts.map +1 -0
- package/dist/mcp/transport/auth.js +115 -0
- package/dist/mcp/transport/auth.js.map +1 -0
- package/dist/mcp/transport/http.d.ts +62 -0
- package/dist/mcp/transport/http.d.ts.map +1 -0
- package/dist/mcp/transport/http.js +357 -0
- package/dist/mcp/transport/http.js.map +1 -0
- package/dist/mcp/transport/pin.d.ts +15 -0
- package/dist/mcp/transport/pin.d.ts.map +1 -0
- package/dist/mcp/transport/pin.js +71 -0
- package/dist/mcp/transport/pin.js.map +1 -0
- package/dist/mcp/transport/telemetry.d.ts +79 -0
- package/dist/mcp/transport/telemetry.d.ts.map +1 -0
- package/dist/mcp/transport/telemetry.js +97 -0
- package/dist/mcp/transport/telemetry.js.map +1 -0
- package/dist/mcp/with-session.d.ts +26 -0
- package/dist/mcp/with-session.d.ts.map +1 -0
- package/dist/mcp/with-session.js +275 -0
- package/dist/mcp/with-session.js.map +1 -0
- package/dist/parser/annotations.d.ts +3 -0
- package/dist/parser/annotations.d.ts.map +1 -0
- package/dist/parser/annotations.js +41 -0
- package/dist/parser/annotations.js.map +1 -0
- package/dist/parser/blocks.d.ts +3 -0
- package/dist/parser/blocks.d.ts.map +1 -0
- package/dist/parser/blocks.js +99 -0
- package/dist/parser/blocks.js.map +1 -0
- package/dist/parser/frontmatter.d.ts +9 -0
- package/dist/parser/frontmatter.d.ts.map +1 -0
- package/dist/parser/frontmatter.js +33 -0
- package/dist/parser/frontmatter.js.map +1 -0
- package/dist/parser/index.d.ts +11 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +54 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/matter.d.ts +3 -0
- package/dist/parser/matter.d.ts.map +1 -0
- package/dist/parser/matter.js +32 -0
- package/dist/parser/matter.js.map +1 -0
- package/dist/parser/tags.d.ts +3 -0
- package/dist/parser/tags.d.ts.map +1 -0
- package/dist/parser/tags.js +32 -0
- package/dist/parser/tags.js.map +1 -0
- package/dist/parser/verbatim.d.ts +4 -0
- package/dist/parser/verbatim.d.ts.map +1 -0
- package/dist/parser/verbatim.js +121 -0
- package/dist/parser/verbatim.js.map +1 -0
- package/dist/parser/yaml-profile.d.ts +4 -0
- package/dist/parser/yaml-profile.d.ts.map +1 -0
- package/dist/parser/yaml-profile.js +73 -0
- package/dist/parser/yaml-profile.js.map +1 -0
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +2 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/loader.d.ts +4 -0
- package/dist/registry/loader.d.ts.map +1 -0
- package/dist/registry/loader.js +147 -0
- package/dist/registry/loader.js.map +1 -0
- package/dist/registry/types.d.ts +2 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +5 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/scanner.d.ts +57 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +223 -0
- package/dist/scanner.js.map +1 -0
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/loader.d.ts +4 -0
- package/dist/schema/loader.d.ts.map +1 -0
- package/dist/schema/loader.js +303 -0
- package/dist/schema/loader.js.map +1 -0
- package/dist/schema/precision.d.ts +6 -0
- package/dist/schema/precision.d.ts.map +1 -0
- package/dist/schema/precision.js +59 -0
- package/dist/schema/precision.js.map +1 -0
- package/dist/schema/validator.d.ts +21 -0
- package/dist/schema/validator.d.ts.map +1 -0
- package/dist/schema/validator.js +200 -0
- package/dist/schema/validator.js.map +1 -0
- package/dist/schema-md.d.ts +8 -0
- package/dist/schema-md.d.ts.map +1 -0
- package/dist/schema-md.js +98 -0
- package/dist/schema-md.js.map +1 -0
- package/dist/skill-files.d.ts +3 -0
- package/dist/skill-files.d.ts.map +1 -0
- package/dist/skill-files.js +362 -0
- package/dist/skill-files.js.map +1 -0
- package/dist/skills-scaffold.d.ts +10 -0
- package/dist/skills-scaffold.d.ts.map +1 -0
- package/dist/skills-scaffold.js +52 -0
- package/dist/skills-scaffold.js.map +1 -0
- package/dist/types.d.ts +309 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +111 -0
- package/dist/types.js.map +1 -0
- package/dist/writer/index.d.ts +7 -0
- package/dist/writer/index.d.ts.map +1 -0
- package/dist/writer/index.js +32 -0
- package/dist/writer/index.js.map +1 -0
- package/dist/writer/serializer.d.ts +4 -0
- package/dist/writer/serializer.d.ts.map +1 -0
- package/dist/writer/serializer.js +103 -0
- package/dist/writer/serializer.js.map +1 -0
- package/dist/writer/template.d.ts +3 -0
- package/dist/writer/template.d.ts.map +1 -0
- package/dist/writer/template.js +27 -0
- package/dist/writer/template.js.map +1 -0
- package/package.json +59 -7
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Reads — getDocument, findDocuments, searchObjects, listRelated,
|
|
3
|
+
// describe, summary, getSchema, schemaInfo
|
|
4
|
+
// ============================================================================
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { ok, singleErr } from '../errors.js';
|
|
8
|
+
import { validateFrontmatter } from '../schema/index.js';
|
|
9
|
+
import { logger } from './logger.js';
|
|
10
|
+
import { docId as toDocId, docType as toDocType, } from '../types.js';
|
|
11
|
+
import { extractBody } from '../writer/index.js';
|
|
12
|
+
import { readFrontmatter, readFrontmatterSync, readBlockContent } from './helpers.js';
|
|
13
|
+
export async function getDocument(ctx, id, depth, blockIdOrHeading) {
|
|
14
|
+
const doc = ctx.backend.getDocument(id);
|
|
15
|
+
if (!doc)
|
|
16
|
+
return singleErr('FILE_NOT_FOUND', `Document "${id}" not found`);
|
|
17
|
+
const frontmatter = await readFrontmatter(ctx.projectRoot, doc);
|
|
18
|
+
const result = {
|
|
19
|
+
docId: doc.docId,
|
|
20
|
+
docType: doc.docType,
|
|
21
|
+
version: doc.version,
|
|
22
|
+
updatedAt: doc.updatedAt,
|
|
23
|
+
depth,
|
|
24
|
+
frontmatter,
|
|
25
|
+
};
|
|
26
|
+
if (depth === 'warm' && blockIdOrHeading) {
|
|
27
|
+
const blocks = ctx.backend.getBlocks(id);
|
|
28
|
+
const match = blocks.find(b => b.id === blockIdOrHeading ||
|
|
29
|
+
b.heading.toLowerCase() === blockIdOrHeading.toLowerCase());
|
|
30
|
+
if (match) {
|
|
31
|
+
const content = await readBlockContent(ctx.projectRoot, doc, match.startLine, match.endLine, match.heading === '');
|
|
32
|
+
result.block = {
|
|
33
|
+
id: match.id,
|
|
34
|
+
heading: match.heading,
|
|
35
|
+
content,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (depth === 'cold') {
|
|
40
|
+
const absPath = path.join(ctx.projectRoot, doc.filePath);
|
|
41
|
+
try {
|
|
42
|
+
const raw = await readFile(absPath, 'utf-8');
|
|
43
|
+
result.body = extractBody(raw);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
logger.degraded('reads', 'getDocument.cold', `Failed to read file for cold read: ${absPath}`, { error: e instanceof Error ? e.message : String(e) });
|
|
47
|
+
result.body = '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return ok(result);
|
|
51
|
+
}
|
|
52
|
+
// 0.7.1 — hard caps on list-returning read tools. Prevents silent truncation
|
|
53
|
+
// when the MCP client's tool-output harness caps the response payload. Callers
|
|
54
|
+
// requesting above the cap get the clamped result plus `_meta.limit_clamped`
|
|
55
|
+
// in the MCP response, never a hidden partial.
|
|
56
|
+
export const MAX_QUERY_LIMIT = 500;
|
|
57
|
+
export const MAX_AGGREGATE_LIMIT = 2000;
|
|
58
|
+
// 0.7.1 R2 — recognized single-op filter set (excludes `between`, which is a
|
|
59
|
+
// compound shortcut desugared at expand time).
|
|
60
|
+
const ATOMIC_FILTER_OPS = new Set(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'contains']);
|
|
61
|
+
// 0.7.1 R2 — validate and normalize a single field's filter input into a flat
|
|
62
|
+
// list of atomic FilterConditions. Input accepts:
|
|
63
|
+
// - scalar (string/number, shorthand eq)
|
|
64
|
+
// - single FilterCondition object ({op, value})
|
|
65
|
+
// - 'between' shortcut ({op: "between", value: [lo, hi]}) → desugars to [gte, lte]
|
|
66
|
+
// - array of FilterConditions (AND semantics; betweens inside are also desugared)
|
|
67
|
+
// After expansion, the backend sees only atomic ops — never between, never arrays
|
|
68
|
+
// of unvalidated shapes.
|
|
69
|
+
function normalizeFilterField(field, raw) {
|
|
70
|
+
// Scalar shorthand → eq
|
|
71
|
+
if (typeof raw === 'string' || typeof raw === 'number') {
|
|
72
|
+
return ok([{ op: 'eq', value: raw }]);
|
|
73
|
+
}
|
|
74
|
+
// Array-of-ops
|
|
75
|
+
if (Array.isArray(raw)) {
|
|
76
|
+
if (raw.length === 0) {
|
|
77
|
+
return singleErr('FILTER_EMPTY_ARRAY', `filter for field "${field}" is an empty array`);
|
|
78
|
+
}
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const item of raw) {
|
|
81
|
+
const inner = normalizeFilterField(field, item);
|
|
82
|
+
if (!inner.ok)
|
|
83
|
+
return inner;
|
|
84
|
+
out.push(...inner.value);
|
|
85
|
+
}
|
|
86
|
+
return ok(out);
|
|
87
|
+
}
|
|
88
|
+
// Object — must have op
|
|
89
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
90
|
+
// Fallback: stringify scalar-ish value → eq
|
|
91
|
+
return ok([{ op: 'eq', value: String(raw) }]);
|
|
92
|
+
}
|
|
93
|
+
const obj = raw;
|
|
94
|
+
if (typeof obj.op !== 'string') {
|
|
95
|
+
return singleErr('FILTER_OP_INVALID', `filter for field "${field}" missing or non-string op`);
|
|
96
|
+
}
|
|
97
|
+
// Between — desugar to [gte, lte]
|
|
98
|
+
if (obj.op === 'between') {
|
|
99
|
+
if (!Array.isArray(obj.value) || obj.value.length !== 2) {
|
|
100
|
+
return singleErr('FILTER_BETWEEN_INVALID', `between for field "${field}" requires a 2-tuple value`);
|
|
101
|
+
}
|
|
102
|
+
const [lo, hi] = obj.value;
|
|
103
|
+
if (lo === undefined || lo === null || hi === undefined || hi === null) {
|
|
104
|
+
return singleErr('FILTER_BETWEEN_INVALID', `between for field "${field}" has null/undefined bound`);
|
|
105
|
+
}
|
|
106
|
+
// Type-homogeneous lo > hi check (strings sort lexically; mixed types pass through).
|
|
107
|
+
if (typeof lo === 'number' && typeof hi === 'number' && lo > hi) {
|
|
108
|
+
return singleErr('FILTER_BETWEEN_INVALID', `between for field "${field}": lo (${lo}) > hi (${hi})`);
|
|
109
|
+
}
|
|
110
|
+
if (typeof lo === 'string' && typeof hi === 'string' && lo > hi) {
|
|
111
|
+
return singleErr('FILTER_BETWEEN_INVALID', `between for field "${field}": lo ("${lo}") > hi ("${hi}")`);
|
|
112
|
+
}
|
|
113
|
+
return ok([
|
|
114
|
+
{ op: 'gte', value: lo },
|
|
115
|
+
{ op: 'lte', value: hi },
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
if (!ATOMIC_FILTER_OPS.has(obj.op)) {
|
|
119
|
+
return singleErr('FILTER_OP_INVALID', `unknown filter op "${obj.op}" for field "${field}"`);
|
|
120
|
+
}
|
|
121
|
+
return ok([obj]);
|
|
122
|
+
}
|
|
123
|
+
export function expandFilters(raw) {
|
|
124
|
+
if (!raw || typeof raw !== 'object')
|
|
125
|
+
return ok({});
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const [field, value] of Object.entries(raw)) {
|
|
128
|
+
const result = normalizeFilterField(field, value);
|
|
129
|
+
if (!result.ok)
|
|
130
|
+
return result;
|
|
131
|
+
out[field] = result.value;
|
|
132
|
+
}
|
|
133
|
+
return ok(out);
|
|
134
|
+
}
|
|
135
|
+
export function findDocuments(ctx, query) {
|
|
136
|
+
// 0.7.1 R2 — validate + expand filters into atomic per-field arrays.
|
|
137
|
+
const expanded = expandFilters(query.filters);
|
|
138
|
+
if (!expanded.ok)
|
|
139
|
+
return expanded;
|
|
140
|
+
let effectiveQuery = { ...query, filters: expanded.value };
|
|
141
|
+
let limitClamped;
|
|
142
|
+
if (query.limit !== undefined && query.limit > MAX_QUERY_LIMIT) {
|
|
143
|
+
limitClamped = { requested: query.limit, applied: MAX_QUERY_LIMIT };
|
|
144
|
+
effectiveQuery = { ...effectiveQuery, limit: MAX_QUERY_LIMIT };
|
|
145
|
+
}
|
|
146
|
+
const results = ctx.backend.findDocuments(effectiveQuery);
|
|
147
|
+
const total = ctx.backend.countDocuments(effectiveQuery);
|
|
148
|
+
if (effectiveQuery.fields && effectiveQuery.fields.length > 0 && results.length > 0) {
|
|
149
|
+
const docIds = results.map(r => r.docId);
|
|
150
|
+
const fieldValues = ctx.backend.getFieldValues(docIds, effectiveQuery.fields);
|
|
151
|
+
for (const match of results) {
|
|
152
|
+
match.fields = fieldValues.get(match.docId) ?? {};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const result = { total, results };
|
|
156
|
+
if (limitClamped)
|
|
157
|
+
result.limitClamped = limitClamped;
|
|
158
|
+
return ok(result);
|
|
159
|
+
}
|
|
160
|
+
export function searchObjects(ctx, query) {
|
|
161
|
+
const results = ctx.backend.findObjects(query);
|
|
162
|
+
const total = ctx.backend.countObjects(query);
|
|
163
|
+
return ok({ total, results });
|
|
164
|
+
}
|
|
165
|
+
export function listRelated(ctx, id, direction, types) {
|
|
166
|
+
const rels = ctx.backend.getRelationships(id, direction);
|
|
167
|
+
// Batch lookup all related doc IDs in one query
|
|
168
|
+
const relatedIds = new Set();
|
|
169
|
+
for (const rel of rels) {
|
|
170
|
+
if (rel.sourceDocId === id)
|
|
171
|
+
relatedIds.add(rel.targetDocId);
|
|
172
|
+
else
|
|
173
|
+
relatedIds.add(rel.sourceDocId);
|
|
174
|
+
}
|
|
175
|
+
const docsMap = ctx.backend.getDocumentsByIds([...relatedIds]);
|
|
176
|
+
const outgoing = [];
|
|
177
|
+
const incoming = [];
|
|
178
|
+
for (const rel of rels) {
|
|
179
|
+
if (rel.sourceDocId === id) {
|
|
180
|
+
const targetDoc = docsMap.get(rel.targetDocId);
|
|
181
|
+
const targetType = targetDoc?.docType ?? toDocType('unknown');
|
|
182
|
+
if (!types || types.includes(targetType)) {
|
|
183
|
+
outgoing.push({ docId: rel.targetDocId, docType: targetType, field: rel.field });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const sourceDoc = docsMap.get(rel.sourceDocId);
|
|
188
|
+
const sourceType = sourceDoc?.docType ?? toDocType('unknown');
|
|
189
|
+
if (!types || types.includes(sourceType)) {
|
|
190
|
+
incoming.push({ docId: rel.sourceDocId, docType: sourceType, field: rel.field });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return ok({ docId: id, outgoing, incoming });
|
|
195
|
+
}
|
|
196
|
+
export function describe(ctx) {
|
|
197
|
+
const stats = ctx.backend.getStats();
|
|
198
|
+
const registryTypes = [...ctx.registry.types.values()].map(rt => ({
|
|
199
|
+
type: rt.name,
|
|
200
|
+
path: rt.path,
|
|
201
|
+
idPrefix: rt.idPrefix,
|
|
202
|
+
schema: rt.schemaRef,
|
|
203
|
+
docCount: stats.documentCountByType[rt.name] ?? 0,
|
|
204
|
+
}));
|
|
205
|
+
return {
|
|
206
|
+
registryTypes,
|
|
207
|
+
extractionPrimitives: [
|
|
208
|
+
'entity', 'date', 'duration', 'amount', 'measure',
|
|
209
|
+
'quantity', 'percentage', 'location', 'identifier', 'contact', 'media',
|
|
210
|
+
],
|
|
211
|
+
totalDocuments: stats.totalDocuments,
|
|
212
|
+
lastIndexedAt: stats.lastIndexedAt,
|
|
213
|
+
subtypeInventory: ctx.backend.getSubtypeInventory(20),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export function summary(ctx) {
|
|
217
|
+
const stats = ctx.backend.getStats();
|
|
218
|
+
const types = [...ctx.registry.types.values()].map(rt => ({
|
|
219
|
+
type: rt.name,
|
|
220
|
+
count: stats.documentCountByType[rt.name] ?? 0,
|
|
221
|
+
sampleIds: ctx.backend.getSampleDocIds(rt.name, 3).map(id => id),
|
|
222
|
+
}));
|
|
223
|
+
// Warnings: broken refs (cheap SQL) + validation errors (scan all docs)
|
|
224
|
+
const brokenRefs = ctx.backend.countBrokenRefs();
|
|
225
|
+
let validationErrors = 0;
|
|
226
|
+
const allDocs = ctx.backend.findDocuments({ limit: 100000 });
|
|
227
|
+
for (const match of allDocs) {
|
|
228
|
+
const doc = ctx.backend.getDocument(match.docId);
|
|
229
|
+
if (!doc)
|
|
230
|
+
continue;
|
|
231
|
+
const schema = ctx.schemaStore.getSchemaForType(doc.docType);
|
|
232
|
+
if (!schema) {
|
|
233
|
+
validationErrors++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const fm = readFrontmatterSync(ctx.projectRoot, doc);
|
|
237
|
+
if (fm) {
|
|
238
|
+
// Read mode: precision enforcement skipped (historical records must
|
|
239
|
+
// never be judged on the way out).
|
|
240
|
+
const result = validateFrontmatter(fm, schema, ctx.registry, undefined, { mode: 'read' });
|
|
241
|
+
if (!result.valid)
|
|
242
|
+
validationErrors++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const emptyProject = ctx.registry.types.size === 0 && stats.totalDocuments === 0;
|
|
246
|
+
return {
|
|
247
|
+
types,
|
|
248
|
+
totalDocuments: stats.totalDocuments,
|
|
249
|
+
totalObjects: stats.totalObjects,
|
|
250
|
+
totalRelationships: stats.totalRelationships,
|
|
251
|
+
lastIndexedAt: stats.lastIndexedAt,
|
|
252
|
+
warnings: { brokenRefs, validationErrors },
|
|
253
|
+
emptyProject,
|
|
254
|
+
bootstrapHint: emptyProject ? '_skills/architect-core.md' : null,
|
|
255
|
+
readOnly: ctx.readOnly,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
export function getSchema(ctx, dt) {
|
|
259
|
+
return ctx.schemaStore.getSchemaForType(dt);
|
|
260
|
+
}
|
|
261
|
+
export function schemaInfo(ctx, dt) {
|
|
262
|
+
const regType = ctx.registry.types.get(dt);
|
|
263
|
+
if (!regType)
|
|
264
|
+
return singleErr('UNKNOWN_TYPE', `Type "${dt}" not in registry`);
|
|
265
|
+
const schema = ctx.schemaStore.getSchemaForType(dt);
|
|
266
|
+
if (!schema)
|
|
267
|
+
return singleErr('SCHEMA_NOT_FOUND', `No schema for type "${dt}"`);
|
|
268
|
+
const fields = [];
|
|
269
|
+
for (const [name, field] of schema.fields) {
|
|
270
|
+
let typeStr = field.type;
|
|
271
|
+
if (field.type === 'ref' && field.target)
|
|
272
|
+
typeStr = `ref -> ${field.target}`;
|
|
273
|
+
if (field.type === 'list' && field.itemType) {
|
|
274
|
+
typeStr = `list of ${field.itemType}`;
|
|
275
|
+
if (field.target)
|
|
276
|
+
typeStr += ` -> ${field.target}`;
|
|
277
|
+
}
|
|
278
|
+
const format = field.format ?? (field.type === 'amount' ? '<number> <currency> (e.g. 1250.00 USD)' : undefined);
|
|
279
|
+
// 0.7.0 — build entry with only the populated fields. Pre-0.7.0 serialized
|
|
280
|
+
// null placeholders for every optional field, bloating the response.
|
|
281
|
+
const entry = {
|
|
282
|
+
name,
|
|
283
|
+
type: typeStr,
|
|
284
|
+
required: schema.required.includes(name),
|
|
285
|
+
indexed: field.index,
|
|
286
|
+
};
|
|
287
|
+
if (field.values !== null && field.values !== undefined)
|
|
288
|
+
entry.values = field.values;
|
|
289
|
+
if (field.target !== null && field.target !== undefined)
|
|
290
|
+
entry.target = field.target;
|
|
291
|
+
if (format !== undefined)
|
|
292
|
+
entry.format = format;
|
|
293
|
+
if (field.defaultValue !== null && field.defaultValue !== undefined)
|
|
294
|
+
entry.default = field.defaultValue;
|
|
295
|
+
// 0.6.7 precision hints — already omitted when null/unset.
|
|
296
|
+
if (field.storePrecision !== null)
|
|
297
|
+
entry.storePrecision = field.storePrecision;
|
|
298
|
+
if (field.onCoarser !== null)
|
|
299
|
+
entry.onCoarser = field.onCoarser;
|
|
300
|
+
if (field.displayPrecision !== null)
|
|
301
|
+
entry.displayPrecision = field.displayPrecision;
|
|
302
|
+
fields.push(entry);
|
|
303
|
+
}
|
|
304
|
+
const result = {
|
|
305
|
+
type: dt,
|
|
306
|
+
idPrefix: regType.idPrefix,
|
|
307
|
+
schemaRef: regType.schemaRef,
|
|
308
|
+
fields,
|
|
309
|
+
};
|
|
310
|
+
if (schema.template) {
|
|
311
|
+
result.templateHeadings = schema.template.map(t => ({ level: t.level, text: t.text }));
|
|
312
|
+
}
|
|
313
|
+
return ok(result);
|
|
314
|
+
}
|
|
315
|
+
// 0.7.1 R1 — sentinel group key for records whose ref chain could not be
|
|
316
|
+
// fully resolved at query time (broken ref, null field, missing target).
|
|
317
|
+
// Surfaced as a group so data-quality issues stay visible to the caller
|
|
318
|
+
// rather than silently vanishing.
|
|
319
|
+
export const UNRESOLVED_GROUP_KEY = '__unresolved__';
|
|
320
|
+
export function aggregate(ctx, query) {
|
|
321
|
+
// 0.7.1 R2 — validate + expand filters into atomic per-field arrays.
|
|
322
|
+
const expanded = expandFilters(query.filters);
|
|
323
|
+
if (!expanded.ok)
|
|
324
|
+
return expanded;
|
|
325
|
+
let effectiveQuery = { ...query, filters: expanded.value };
|
|
326
|
+
let limitClamped;
|
|
327
|
+
if (query.limit !== undefined && query.limit > MAX_AGGREGATE_LIMIT) {
|
|
328
|
+
limitClamped = { requested: query.limit, applied: MAX_AGGREGATE_LIMIT };
|
|
329
|
+
effectiveQuery = { ...effectiveQuery, limit: MAX_AGGREGATE_LIMIT };
|
|
330
|
+
}
|
|
331
|
+
// R1 — ref-chain groupBy: "ref_field->ref_field->leaf_field". Arbitrary depth.
|
|
332
|
+
// Chain detected by presence of '->' in the groupBy string. Dispatch to chain-aware
|
|
333
|
+
// path; validate schema chain; aggregate at first hop via backend; resolve remaining
|
|
334
|
+
// hops in the engine; merge groups that collapse to the same resolved key.
|
|
335
|
+
if (effectiveQuery.groupBy.includes('->')) {
|
|
336
|
+
return aggregateWithRefChain(ctx, effectiveQuery, limitClamped);
|
|
337
|
+
}
|
|
338
|
+
const result = ctx.backend.aggregate(effectiveQuery);
|
|
339
|
+
if (limitClamped)
|
|
340
|
+
result.limitClamped = limitClamped;
|
|
341
|
+
return ok(result);
|
|
342
|
+
}
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
// R1 — ref-chain aggregate helpers
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
function splitRefChain(groupBy) {
|
|
347
|
+
return groupBy.split('->').map(s => s.trim());
|
|
348
|
+
}
|
|
349
|
+
function validateRefChain(ctx, docTypeArg, hops) {
|
|
350
|
+
if (!docTypeArg) {
|
|
351
|
+
return singleErr('SCHEMA_REF_CHAIN_INVALID', 'ref-chain groupBy requires an explicit docType');
|
|
352
|
+
}
|
|
353
|
+
if (hops.length < 2) {
|
|
354
|
+
return singleErr('SCHEMA_REF_CHAIN_INVALID', 'ref-chain groupBy must have at least two segments');
|
|
355
|
+
}
|
|
356
|
+
for (const seg of hops) {
|
|
357
|
+
if (!seg)
|
|
358
|
+
return singleErr('SCHEMA_REF_CHAIN_INVALID', 'ref-chain segment is empty');
|
|
359
|
+
}
|
|
360
|
+
let currentType = docTypeArg;
|
|
361
|
+
for (let i = 0; i < hops.length; i++) {
|
|
362
|
+
const seg = hops[i];
|
|
363
|
+
const schema = ctx.schemaStore.getSchemaForType(currentType);
|
|
364
|
+
if (!schema) {
|
|
365
|
+
return singleErr('SCHEMA_REF_CHAIN_INVALID', `no schema for type "${currentType}" at segment ${i} (${seg})`);
|
|
366
|
+
}
|
|
367
|
+
const field = schema.fields.get(seg);
|
|
368
|
+
if (!field) {
|
|
369
|
+
return singleErr('SCHEMA_REF_CHAIN_INVALID', `field "${seg}" not found on type "${currentType}"`);
|
|
370
|
+
}
|
|
371
|
+
const isLast = i === hops.length - 1;
|
|
372
|
+
if (!isLast) {
|
|
373
|
+
if (field.type !== 'ref') {
|
|
374
|
+
return singleErr('SCHEMA_REF_CHAIN_INVALID', `non-leaf segment "${seg}" must be a ref field on "${currentType}" (got ${field.type})`);
|
|
375
|
+
}
|
|
376
|
+
if (!field.target) {
|
|
377
|
+
return singleErr('SCHEMA_REF_CHAIN_INVALID', `ref field "${seg}" on "${currentType}" has no target`);
|
|
378
|
+
}
|
|
379
|
+
currentType = field.target;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
// Leaf: allowed to be anything (typically string/enum/number). A leaf ref
|
|
383
|
+
// is unusual but legal — callers get the target docId as the group key.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return ok(true);
|
|
387
|
+
}
|
|
388
|
+
// Resolve a chain of hops starting from `startDocId`. Returns the leaf value
|
|
389
|
+
// (stringified) or null if any hop fails to resolve (broken ref, missing target,
|
|
390
|
+
// null field value).
|
|
391
|
+
function resolveRefChain(ctx, startDocId, hops) {
|
|
392
|
+
let currentDocId = startDocId;
|
|
393
|
+
for (let i = 0; i < hops.length; i++) {
|
|
394
|
+
const field = hops[i];
|
|
395
|
+
const isLast = i === hops.length - 1;
|
|
396
|
+
const values = ctx.backend.getFieldValues([toDocId(currentDocId)], [field]);
|
|
397
|
+
const v = values.get(currentDocId)?.[field];
|
|
398
|
+
if (v === undefined || v === null || v === '')
|
|
399
|
+
return null;
|
|
400
|
+
const str = String(v);
|
|
401
|
+
if (isLast)
|
|
402
|
+
return str;
|
|
403
|
+
currentDocId = str;
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
function freshAccum() {
|
|
408
|
+
return { count: 0, sum: 0, min: 0, max: 0, minSet: false, maxSet: false };
|
|
409
|
+
}
|
|
410
|
+
function aggregateWithRefChain(ctx, query, limitClamped) {
|
|
411
|
+
const hops = splitRefChain(query.groupBy);
|
|
412
|
+
const validation = validateRefChain(ctx, query.docType, hops);
|
|
413
|
+
if (!validation.ok)
|
|
414
|
+
return validation;
|
|
415
|
+
// Aggregate at first hop (which we've validated is a ref field on query.docType).
|
|
416
|
+
// Pull enough groups that the limit clamp still applies after merge — ask for
|
|
417
|
+
// a generous page since multiple first-hop groups can collapse to one resolved
|
|
418
|
+
// key. MAX_AGGREGATE_LIMIT caps the underlying scan at 2000 rows.
|
|
419
|
+
const firstHop = hops[0];
|
|
420
|
+
const remainingHops = hops.slice(1);
|
|
421
|
+
const requestedFinalLimit = query.limit ?? 50;
|
|
422
|
+
const firstHopQuery = {
|
|
423
|
+
groupBy: firstHop,
|
|
424
|
+
limit: MAX_AGGREGATE_LIMIT,
|
|
425
|
+
};
|
|
426
|
+
if (query.docType !== undefined)
|
|
427
|
+
firstHopQuery.docType = query.docType;
|
|
428
|
+
if (query.metric !== undefined)
|
|
429
|
+
firstHopQuery.metric = query.metric;
|
|
430
|
+
if (query.filters !== undefined)
|
|
431
|
+
firstHopQuery.filters = query.filters;
|
|
432
|
+
const firstHopResult = ctx.backend.aggregate(firstHopQuery);
|
|
433
|
+
// Resolve each first-hop group (target docId) to a final leaf key; merge
|
|
434
|
+
// accumulators across groups that resolve to the same key.
|
|
435
|
+
const merged = new Map();
|
|
436
|
+
let unresolvedCount = 0;
|
|
437
|
+
for (const group of firstHopResult.groups) {
|
|
438
|
+
const resolved = resolveRefChain(ctx, group.value, remainingHops);
|
|
439
|
+
const key = resolved ?? UNRESOLVED_GROUP_KEY;
|
|
440
|
+
if (resolved === null)
|
|
441
|
+
unresolvedCount++;
|
|
442
|
+
const acc = merged.get(key) ?? freshAccum();
|
|
443
|
+
acc.count += group.count;
|
|
444
|
+
if (query.metric) {
|
|
445
|
+
const m = group.metric ?? 0;
|
|
446
|
+
if (query.metric.op === 'sum') {
|
|
447
|
+
acc.sum += m;
|
|
448
|
+
}
|
|
449
|
+
else if (query.metric.op === 'avg') {
|
|
450
|
+
// Reconstruct per-group sum = avg * count, then re-divide at finalize.
|
|
451
|
+
acc.sum += m * group.count;
|
|
452
|
+
}
|
|
453
|
+
else if (query.metric.op === 'min') {
|
|
454
|
+
acc.min = acc.minSet ? Math.min(acc.min, m) : m;
|
|
455
|
+
acc.minSet = true;
|
|
456
|
+
}
|
|
457
|
+
else if (query.metric.op === 'max') {
|
|
458
|
+
acc.max = acc.maxSet ? Math.max(acc.max, m) : m;
|
|
459
|
+
acc.maxSet = true;
|
|
460
|
+
}
|
|
461
|
+
// count op: tracked in acc.count directly.
|
|
462
|
+
}
|
|
463
|
+
merged.set(key, acc);
|
|
464
|
+
}
|
|
465
|
+
if (unresolvedCount > 0) {
|
|
466
|
+
logger.bestEffort('reads', 'aggregate.refChain', `${unresolvedCount} first-hop group(s) fell through to ${UNRESOLVED_GROUP_KEY} (broken ref or null target)`, { docType: String(query.docType), chain: hops.join('->'), unresolvedGroups: unresolvedCount });
|
|
467
|
+
}
|
|
468
|
+
// Finalize: compute metric values per merged group.
|
|
469
|
+
const finalGroups = [];
|
|
470
|
+
for (const [key, acc] of merged.entries()) {
|
|
471
|
+
let metric;
|
|
472
|
+
if (query.metric) {
|
|
473
|
+
switch (query.metric.op) {
|
|
474
|
+
case 'count':
|
|
475
|
+
metric = acc.count;
|
|
476
|
+
break;
|
|
477
|
+
case 'sum':
|
|
478
|
+
metric = acc.sum;
|
|
479
|
+
break;
|
|
480
|
+
case 'avg':
|
|
481
|
+
metric = acc.count > 0 ? acc.sum / acc.count : null;
|
|
482
|
+
break;
|
|
483
|
+
case 'min':
|
|
484
|
+
metric = acc.minSet ? acc.min : null;
|
|
485
|
+
break;
|
|
486
|
+
case 'max':
|
|
487
|
+
metric = acc.maxSet ? acc.max : null;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const g = { value: key, count: acc.count };
|
|
492
|
+
if (metric !== undefined)
|
|
493
|
+
g.metric = metric;
|
|
494
|
+
finalGroups.push(g);
|
|
495
|
+
}
|
|
496
|
+
// Sort to match backend behavior: metric desc when a metric is set, count desc otherwise.
|
|
497
|
+
finalGroups.sort((a, b) => {
|
|
498
|
+
const aVal = query.metric ? (a.metric ?? 0) : a.count;
|
|
499
|
+
const bVal = query.metric ? (b.metric ?? 0) : b.count;
|
|
500
|
+
return bVal - aVal;
|
|
501
|
+
});
|
|
502
|
+
// Apply user's requested limit at the final (merged) stage.
|
|
503
|
+
const limited = finalGroups.slice(0, requestedFinalLimit);
|
|
504
|
+
const total = limited.reduce((s, g) => s + g.count, 0);
|
|
505
|
+
const result = { groups: limited, total };
|
|
506
|
+
// totalMetric: only semantically meaningful for sum / count. For avg/min/max,
|
|
507
|
+
// a post-merge "total" is not defined — omit rather than emit a misleading number.
|
|
508
|
+
if (query.metric && (query.metric.op === 'sum' || query.metric.op === 'count')) {
|
|
509
|
+
result.totalMetric = limited.reduce((s, g) => s + (g.metric ?? 0), 0);
|
|
510
|
+
}
|
|
511
|
+
if (limitClamped)
|
|
512
|
+
result.limitClamped = limitClamped;
|
|
513
|
+
return ok(result);
|
|
514
|
+
}
|
|
515
|
+
export function join(ctx, query) {
|
|
516
|
+
// Step 1: Find source documents
|
|
517
|
+
const docQuery = { docType: query.docType };
|
|
518
|
+
if (query.filters)
|
|
519
|
+
docQuery.filters = query.filters;
|
|
520
|
+
if (query.limit)
|
|
521
|
+
docQuery.limit = query.limit;
|
|
522
|
+
if (query.offset)
|
|
523
|
+
docQuery.offset = query.offset;
|
|
524
|
+
const docs = ctx.backend.findDocuments(docQuery);
|
|
525
|
+
const total = ctx.backend.countDocuments(docQuery);
|
|
526
|
+
if (docs.length === 0)
|
|
527
|
+
return ok({ total, results: [] });
|
|
528
|
+
const docIds = docs.map(d => d.docId);
|
|
529
|
+
// Step 2: Get projected fields from source docs
|
|
530
|
+
const allSourceFields = [...(query.fields ?? []), ...query.refs];
|
|
531
|
+
const sourceFieldValues = ctx.backend.getFieldValues(docIds, allSourceFields);
|
|
532
|
+
// Step 3: Collect all ref target IDs and batch-fetch their fields
|
|
533
|
+
const refTargetIds = new Set();
|
|
534
|
+
for (const [, fields] of sourceFieldValues) {
|
|
535
|
+
for (const refField of query.refs) {
|
|
536
|
+
const targetId = fields[refField];
|
|
537
|
+
if (targetId)
|
|
538
|
+
refTargetIds.add(targetId);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Get all requested ref fields across all targets
|
|
542
|
+
const allRefFieldNames = new Set();
|
|
543
|
+
if (query.refFields) {
|
|
544
|
+
for (const fieldNames of Object.values(query.refFields)) {
|
|
545
|
+
for (const f of fieldNames)
|
|
546
|
+
allRefFieldNames.add(f);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const refFieldValues = refTargetIds.size > 0 && allRefFieldNames.size > 0
|
|
550
|
+
? ctx.backend.getFieldValues([...refTargetIds].map(id => id), [...allRefFieldNames])
|
|
551
|
+
: new Map();
|
|
552
|
+
// Step 4: Assemble results
|
|
553
|
+
const results = [];
|
|
554
|
+
for (const doc of docs) {
|
|
555
|
+
const srcFields = sourceFieldValues.get(doc.docId) ?? {};
|
|
556
|
+
// Build source fields (excluding ref field names)
|
|
557
|
+
const displayFields = {};
|
|
558
|
+
for (const f of (query.fields ?? [])) {
|
|
559
|
+
if (srcFields[f] !== undefined)
|
|
560
|
+
displayFields[f] = srcFields[f];
|
|
561
|
+
}
|
|
562
|
+
// Build ref results
|
|
563
|
+
const refs = {};
|
|
564
|
+
for (const refField of query.refs) {
|
|
565
|
+
const targetId = srcFields[refField];
|
|
566
|
+
if (!targetId) {
|
|
567
|
+
refs[refField] = null;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const targetFields = refFieldValues.get(targetId) ?? {};
|
|
571
|
+
const requestedRefFields = query.refFields?.[refField] ?? [];
|
|
572
|
+
const filteredFields = {};
|
|
573
|
+
for (const f of requestedRefFields) {
|
|
574
|
+
if (targetFields[f] !== undefined)
|
|
575
|
+
filteredFields[f] = targetFields[f];
|
|
576
|
+
}
|
|
577
|
+
refs[refField] = { docId: targetId, fields: filteredFields };
|
|
578
|
+
}
|
|
579
|
+
results.push({ docId: doc.docId, fields: displayFields, refs });
|
|
580
|
+
}
|
|
581
|
+
return ok({ total, results });
|
|
582
|
+
}
|
|
583
|
+
// ---- Verify — fact-checking primitive --------------------------------------
|
|
584
|
+
export async function verifyField(ctx, id, field, expected) {
|
|
585
|
+
const doc = ctx.backend.getDocument(id);
|
|
586
|
+
if (!doc) {
|
|
587
|
+
return ok({
|
|
588
|
+
grounded: false,
|
|
589
|
+
claim: 'field',
|
|
590
|
+
expected,
|
|
591
|
+
actual: null,
|
|
592
|
+
source: { docId: id, filePath: '' },
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
const fm = await readFrontmatter(ctx.projectRoot, doc);
|
|
596
|
+
const actual = fm[field] ?? null;
|
|
597
|
+
// Canonical comparison (handles dates, arrays, type coercion).
|
|
598
|
+
// For Date values, compare against full ISO string. Day-form fallback
|
|
599
|
+
// kept so pre-0.6.7 callers passing day-form strings still match
|
|
600
|
+
// records that happen to have been parsed as Date objects.
|
|
601
|
+
const actualStr = actual instanceof Date ? actual.toISOString() : String(actual ?? '');
|
|
602
|
+
const expectedStr = expected instanceof Date ? expected.toISOString() : String(expected ?? '');
|
|
603
|
+
const grounded = actualStr === expectedStr
|
|
604
|
+
|| (actual instanceof Date && String(expected ?? '') === actual.toISOString().slice(0, 10))
|
|
605
|
+
|| (expected instanceof Date && String(actual ?? '') === expected.toISOString().slice(0, 10))
|
|
606
|
+
|| (Array.isArray(expected) && Array.isArray(actual) && JSON.stringify(expected) === JSON.stringify(actual));
|
|
607
|
+
return ok({
|
|
608
|
+
grounded,
|
|
609
|
+
claim: 'field',
|
|
610
|
+
expected,
|
|
611
|
+
actual,
|
|
612
|
+
source: { docId: id, filePath: doc.filePath },
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
export function verifyCount(ctx, dt, expectedCount, filters) {
|
|
616
|
+
const query = { docType: dt, limit: 0 };
|
|
617
|
+
if (filters)
|
|
618
|
+
query.filters = filters;
|
|
619
|
+
const actual = ctx.backend.countDocuments(query);
|
|
620
|
+
return ok({
|
|
621
|
+
grounded: actual === expectedCount,
|
|
622
|
+
claim: 'count',
|
|
623
|
+
expected: expectedCount,
|
|
624
|
+
actual,
|
|
625
|
+
source: 'query',
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
// ============================================================================
|
|
629
|
+
// changesSince — polling delta (0.5.0 R5).
|
|
630
|
+
// Strictly (updated_at, doc_id)-ordered. Cursor is opaque base64url-encoded
|
|
631
|
+
// JSON {u: updatedAt, d: docId}. Omitting cursor starts from the beginning.
|
|
632
|
+
// ============================================================================
|
|
633
|
+
const DEFAULT_CHANGES_LIMIT = 100;
|
|
634
|
+
const MAX_CHANGES_LIMIT = 1000;
|
|
635
|
+
export function encodeChangesCursor(cursor) {
|
|
636
|
+
const json = JSON.stringify({ u: cursor.updatedAt, d: cursor.docId });
|
|
637
|
+
return Buffer.from(json, 'utf8').toString('base64url');
|
|
638
|
+
}
|
|
639
|
+
export function decodeChangesCursor(raw) {
|
|
640
|
+
let decoded;
|
|
641
|
+
try {
|
|
642
|
+
decoded = Buffer.from(raw, 'base64url').toString('utf8');
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return singleErr('INVALID_FIELDS', 'cursor is not valid base64url');
|
|
646
|
+
}
|
|
647
|
+
let parsed;
|
|
648
|
+
try {
|
|
649
|
+
parsed = JSON.parse(decoded);
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
return singleErr('INVALID_FIELDS', 'cursor payload is not valid JSON');
|
|
653
|
+
}
|
|
654
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
655
|
+
return singleErr('INVALID_FIELDS', 'cursor payload is not an object');
|
|
656
|
+
}
|
|
657
|
+
const obj = parsed;
|
|
658
|
+
if (typeof obj.u !== 'string' || typeof obj.d !== 'string') {
|
|
659
|
+
return singleErr('INVALID_FIELDS', 'cursor payload missing u/d fields');
|
|
660
|
+
}
|
|
661
|
+
return ok({ updatedAt: obj.u, docId: obj.d });
|
|
662
|
+
}
|
|
663
|
+
export function changesSince(ctx, query) {
|
|
664
|
+
const rawLimit = query.limit ?? DEFAULT_CHANGES_LIMIT;
|
|
665
|
+
if (!Number.isFinite(rawLimit) || rawLimit <= 0) {
|
|
666
|
+
return singleErr('INVALID_FIELDS', 'limit must be a positive integer');
|
|
667
|
+
}
|
|
668
|
+
const limit = Math.min(Math.floor(rawLimit), MAX_CHANGES_LIMIT);
|
|
669
|
+
let parsedCursor = null;
|
|
670
|
+
if (query.cursor !== undefined && query.cursor !== '') {
|
|
671
|
+
const result = decodeChangesCursor(query.cursor);
|
|
672
|
+
if (!result.ok)
|
|
673
|
+
return result;
|
|
674
|
+
parsedCursor = result.value;
|
|
675
|
+
}
|
|
676
|
+
// Query one extra row to detect hasMore without a second round-trip.
|
|
677
|
+
const rows = ctx.backend.listChangesSince({
|
|
678
|
+
cursor: parsedCursor,
|
|
679
|
+
limit: limit + 1,
|
|
680
|
+
docTypes: query.docTypes,
|
|
681
|
+
});
|
|
682
|
+
const hasMore = rows.length > limit;
|
|
683
|
+
const page = hasMore ? rows.slice(0, limit) : rows;
|
|
684
|
+
const changes = page.map((r) => ({
|
|
685
|
+
docId: r.docId,
|
|
686
|
+
docType: r.docType,
|
|
687
|
+
updatedAt: r.updatedAt,
|
|
688
|
+
// version == 1 → first materialization of the doc (create).
|
|
689
|
+
// version > 1 → subsequent update. Deletes are not emitted in 0.5.0
|
|
690
|
+
// because the engine does not tombstone (see spec).
|
|
691
|
+
operation: (r.version <= 1 ? 'create' : 'update'),
|
|
692
|
+
}));
|
|
693
|
+
const nextCursor = hasMore && changes.length > 0
|
|
694
|
+
? encodeChangesCursor({
|
|
695
|
+
updatedAt: changes[changes.length - 1].updatedAt,
|
|
696
|
+
docId: changes[changes.length - 1].docId,
|
|
697
|
+
})
|
|
698
|
+
: null;
|
|
699
|
+
return ok({ changes, nextCursor, hasMore });
|
|
700
|
+
}
|
|
701
|
+
//# sourceMappingURL=reads.js.map
|