mrplex 0.0.1
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 +284 -0
- package/dist/cli/config.d.ts +28 -0
- package/dist/cli/config.js +48 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +13 -0
- package/dist/cli/exit-codes.js +39 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/format.d.ts +22 -0
- package/dist/cli/format.js +148 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/main.d.ts +16 -0
- package/dist/cli/main.js +1518 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/client/kernel-client.d.ts +97 -0
- package/dist/client/kernel-client.js +14 -0
- package/dist/client/kernel-client.js.map +1 -0
- package/dist/client/local.d.ts +26 -0
- package/dist/client/local.js +103 -0
- package/dist/client/local.js.map +1 -0
- package/dist/client/remote-mcp.d.ts +35 -0
- package/dist/client/remote-mcp.js +174 -0
- package/dist/client/remote-mcp.js.map +1 -0
- package/dist/embed/backfill.d.ts +24 -0
- package/dist/embed/backfill.js +33 -0
- package/dist/embed/backfill.js.map +1 -0
- package/dist/embed/chunker.d.ts +31 -0
- package/dist/embed/chunker.js +121 -0
- package/dist/embed/chunker.js.map +1 -0
- package/dist/embed/cmd-hook.d.ts +21 -0
- package/dist/embed/cmd-hook.js +100 -0
- package/dist/embed/cmd-hook.js.map +1 -0
- package/dist/embed/config.d.ts +61 -0
- package/dist/embed/config.js +109 -0
- package/dist/embed/config.js.map +1 -0
- package/dist/embed/hook.d.ts +38 -0
- package/dist/embed/hook.js +56 -0
- package/dist/embed/hook.js.map +1 -0
- package/dist/embed/http-hook.d.ts +13 -0
- package/dist/embed/http-hook.js +39 -0
- package/dist/embed/http-hook.js.map +1 -0
- package/dist/embed/worker.d.ts +62 -0
- package/dist/embed/worker.js +228 -0
- package/dist/embed/worker.js.map +1 -0
- package/dist/fixtures/hwe/characters/albion-ever.md +12 -0
- package/dist/fixtures/hwe/characters/cent.md +11 -0
- package/dist/fixtures/hwe/concepts/cent-demiurge-resonance.md +10 -0
- package/dist/fixtures/hwe/concepts/phase-looper.md +10 -0
- package/dist/fixtures/hwe/entities/demiurge.md +11 -0
- package/dist/fixtures/hwe/entities/monad.md +11 -0
- package/dist/fixtures/hwe/moc/globocorp-network.md +19 -0
- package/dist/fixtures/hwe/organizations/globocorp.md +11 -0
- package/dist/fixtures/hwe/readme.md +24 -0
- package/dist/fixtures/starship/crew/aria-okonkwo.md +14 -0
- package/dist/fixtures/starship/crew/bexley-orr.md +14 -0
- package/dist/fixtures/starship/crew/dax-thorne.md +14 -0
- package/dist/fixtures/starship/crew/isolde-marsh.md +16 -0
- package/dist/fixtures/starship/crew/kestrel-vance.md +15 -0
- package/dist/fixtures/starship/crew/quill-vasquez.md +14 -0
- package/dist/fixtures/starship/crew/soren-halloway.md +16 -0
- package/dist/fixtures/starship/encounters/the-drift-choir.md +14 -0
- package/dist/fixtures/starship/encounters/the-hollow-signal.md +16 -0
- package/dist/fixtures/starship/encounters/the-wandering-buoy.md +14 -0
- package/dist/fixtures/starship/equipment/coolant-loop-b.md +13 -0
- package/dist/fixtures/starship/equipment/emergency-ration-bars.md +12 -0
- package/dist/fixtures/starship/equipment/long-range-sensors.md +13 -0
- package/dist/fixtures/starship/equipment/plasma-manifold-3.md +15 -0
- package/dist/fixtures/starship/equipment/shuttle-corvid.md +13 -0
- package/dist/fixtures/starship/logs/marsh-4419-1.md +13 -0
- package/dist/fixtures/starship/logs/okonkwo-4415-1.md +13 -0
- package/dist/fixtures/starship/logs/orr-4413-1.md +13 -0
- package/dist/fixtures/starship/logs/orr-4420-1.md +12 -0
- package/dist/fixtures/starship/logs/thorne-4413-2.md +13 -0
- package/dist/fixtures/starship/logs/vance-4419-2.md +13 -0
- package/dist/fixtures/starship/misc/the-galley-ficus.md +11 -0
- package/dist/fixtures/starship/missions/the-cinder-run.md +17 -0
- package/dist/fixtures/starship/missions/the-drift-choir.md +17 -0
- package/dist/fixtures/starship/missions/the-hollow-signal.md +17 -0
- package/dist/fixtures/starship/missions/the-silent-beacon.md +17 -0
- package/dist/fixtures/starship/moc/crew.md +19 -0
- package/dist/fixtures/starship/moc/encounters.md +14 -0
- package/dist/fixtures/starship/moc/missions.md +15 -0
- package/dist/fixtures/starship/readme.md +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/auth/glob.d.ts +46 -0
- package/dist/kernel/auth/glob.js +112 -0
- package/dist/kernel/auth/glob.js.map +1 -0
- package/dist/kernel/auth/scope.d.ts +53 -0
- package/dist/kernel/auth/scope.js +83 -0
- package/dist/kernel/auth/scope.js.map +1 -0
- package/dist/kernel/casefold.d.ts +37 -0
- package/dist/kernel/casefold.js +44 -0
- package/dist/kernel/casefold.js.map +1 -0
- package/dist/kernel/constants.d.ts +21 -0
- package/dist/kernel/constants.js +22 -0
- package/dist/kernel/constants.js.map +1 -0
- package/dist/kernel/context.d.ts +70 -0
- package/dist/kernel/context.js +114 -0
- package/dist/kernel/context.js.map +1 -0
- package/dist/kernel/deletion.d.ts +50 -0
- package/dist/kernel/deletion.js +83 -0
- package/dist/kernel/deletion.js.map +1 -0
- package/dist/kernel/diff.d.ts +36 -0
- package/dist/kernel/diff.js +61 -0
- package/dist/kernel/diff.js.map +1 -0
- package/dist/kernel/errors.d.ts +37 -0
- package/dist/kernel/errors.js +55 -0
- package/dist/kernel/errors.js.map +1 -0
- package/dist/kernel/frontmatter-input.d.ts +32 -0
- package/dist/kernel/frontmatter-input.js +85 -0
- package/dist/kernel/frontmatter-input.js.map +1 -0
- package/dist/kernel/graph.d.ts +34 -0
- package/dist/kernel/graph.js +559 -0
- package/dist/kernel/graph.js.map +1 -0
- package/dist/kernel/history.d.ts +74 -0
- package/dist/kernel/history.js +143 -0
- package/dist/kernel/history.js.map +1 -0
- package/dist/kernel/kernel.d.ts +162 -0
- package/dist/kernel/kernel.js +675 -0
- package/dist/kernel/kernel.js.map +1 -0
- package/dist/kernel/path-config.d.ts +101 -0
- package/dist/kernel/path-config.js +171 -0
- package/dist/kernel/path-config.js.map +1 -0
- package/dist/kernel/query/ast.d.ts +24 -0
- package/dist/kernel/query/ast.js +52 -0
- package/dist/kernel/query/ast.js.map +1 -0
- package/dist/kernel/query/cel-parse.d.ts +37 -0
- package/dist/kernel/query/cel-parse.js +88 -0
- package/dist/kernel/query/cel-parse.js.map +1 -0
- package/dist/kernel/query/degrees.d.ts +31 -0
- package/dist/kernel/query/degrees.js +135 -0
- package/dist/kernel/query/degrees.js.map +1 -0
- package/dist/kernel/query/graph-ast.d.ts +51 -0
- package/dist/kernel/query/graph-ast.js +139 -0
- package/dist/kernel/query/graph-ast.js.map +1 -0
- package/dist/kernel/query/query.d.ts +56 -0
- package/dist/kernel/query/query.js +276 -0
- package/dist/kernel/query/query.js.map +1 -0
- package/dist/kernel/validation.d.ts +64 -0
- package/dist/kernel/validation.js +183 -0
- package/dist/kernel/validation.js.map +1 -0
- package/dist/kernel/version-id.d.ts +10 -0
- package/dist/kernel/version-id.js +21 -0
- package/dist/kernel/version-id.js.map +1 -0
- package/dist/kernel/wire.d.ts +148 -0
- package/dist/kernel/wire.js +6 -0
- package/dist/kernel/wire.js.map +1 -0
- package/dist/links/backfill.d.ts +28 -0
- package/dist/links/backfill.js +33 -0
- package/dist/links/backfill.js.map +1 -0
- package/dist/links/extract.d.ts +29 -0
- package/dist/links/extract.js +233 -0
- package/dist/links/extract.js.map +1 -0
- package/dist/links/link-config.d.ts +70 -0
- package/dist/links/link-config.js +107 -0
- package/dist/links/link-config.js.map +1 -0
- package/dist/links/maintain.d.ts +37 -0
- package/dist/links/maintain.js +91 -0
- package/dist/links/maintain.js.map +1 -0
- package/dist/links/repair.d.ts +46 -0
- package/dist/links/repair.js +96 -0
- package/dist/links/repair.js.map +1 -0
- package/dist/links/resolve.d.ts +41 -0
- package/dist/links/resolve.js +121 -0
- package/dist/links/resolve.js.map +1 -0
- package/dist/links/stale.d.ts +32 -0
- package/dist/links/stale.js +73 -0
- package/dist/links/stale.js.map +1 -0
- package/dist/markdown/content-hash.d.ts +34 -0
- package/dist/markdown/content-hash.js +46 -0
- package/dist/markdown/content-hash.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +69 -0
- package/dist/markdown/frontmatter.js +147 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/hash-backfill.d.ts +22 -0
- package/dist/markdown/hash-backfill.js +43 -0
- package/dist/markdown/hash-backfill.js.map +1 -0
- package/dist/mcp/query-syntax.d.ts +20 -0
- package/dist/mcp/query-syntax.js +194 -0
- package/dist/mcp/query-syntax.js.map +1 -0
- package/dist/mcp/render.d.ts +31 -0
- package/dist/mcp/render.js +110 -0
- package/dist/mcp/render.js.map +1 -0
- package/dist/mcp/server.d.ts +60 -0
- package/dist/mcp/server.js +170 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +93 -0
- package/dist/mcp/tools.js +1218 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/rest/conditional.d.ts +37 -0
- package/dist/rest/conditional.js +58 -0
- package/dist/rest/conditional.js.map +1 -0
- package/dist/rest/negotiate.d.ts +33 -0
- package/dist/rest/negotiate.js +54 -0
- package/dist/rest/negotiate.js.map +1 -0
- package/dist/rest/routes.d.ts +37 -0
- package/dist/rest/routes.js +744 -0
- package/dist/rest/routes.js.map +1 -0
- package/dist/seed/fixture-seed.d.ts +35 -0
- package/dist/seed/fixture-seed.js +75 -0
- package/dist/seed/fixture-seed.js.map +1 -0
- package/dist/seed/starship-config.d.ts +3 -0
- package/dist/seed/starship-config.js +5 -0
- package/dist/seed/starship-config.js.map +1 -0
- package/dist/server/headers.d.ts +36 -0
- package/dist/server/headers.js +30 -0
- package/dist/server/headers.js.map +1 -0
- package/dist/server/http-error.d.ts +53 -0
- package/dist/server/http-error.js +104 -0
- package/dist/server/http-error.js.map +1 -0
- package/dist/server/serve.d.ts +56 -0
- package/dist/server/serve.js +135 -0
- package/dist/server/serve.js.map +1 -0
- package/dist/shell/audit.d.ts +32 -0
- package/dist/shell/audit.js +28 -0
- package/dist/shell/audit.js.map +1 -0
- package/dist/shell/guard.d.ts +50 -0
- package/dist/shell/guard.js +176 -0
- package/dist/shell/guard.js.map +1 -0
- package/dist/shell/keys.d.ts +36 -0
- package/dist/shell/keys.js +64 -0
- package/dist/shell/keys.js.map +1 -0
- package/dist/shell/login.d.ts +56 -0
- package/dist/shell/login.js +130 -0
- package/dist/shell/login.js.map +1 -0
- package/dist/shell/oidc.d.ts +62 -0
- package/dist/shell/oidc.js +102 -0
- package/dist/shell/oidc.js.map +1 -0
- package/dist/shell/policy.d.ts +100 -0
- package/dist/shell/policy.js +232 -0
- package/dist/shell/policy.js.map +1 -0
- package/dist/shell/proxy-policy.d.ts +34 -0
- package/dist/shell/proxy-policy.js +120 -0
- package/dist/shell/proxy-policy.js.map +1 -0
- package/dist/shell/proxy.d.ts +47 -0
- package/dist/shell/proxy.js +253 -0
- package/dist/shell/proxy.js.map +1 -0
- package/dist/shell/serve.d.ts +86 -0
- package/dist/shell/serve.js +0 -0
- package/dist/shell/serve.js.map +1 -0
- package/dist/shell/stdio.d.ts +55 -0
- package/dist/shell/stdio.js +63 -0
- package/dist/shell/stdio.js.map +1 -0
- package/dist/storage/registry.d.ts +20 -0
- package/dist/storage/registry.js +38 -0
- package/dist/storage/registry.js.map +1 -0
- package/dist/storage/search-plan.d.ts +93 -0
- package/dist/storage/search-plan.js +15 -0
- package/dist/storage/search-plan.js.map +1 -0
- package/dist/storage/types.d.ts +448 -0
- package/dist/storage/types.js +14 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/storage/versions-since.d.ts +72 -0
- package/dist/storage/versions-since.js +92 -0
- package/dist/storage/versions-since.js.map +1 -0
- package/dist/storage-postgres/adapter.d.ts +17 -0
- package/dist/storage-postgres/adapter.js +782 -0
- package/dist/storage-postgres/adapter.js.map +1 -0
- package/dist/storage-postgres/compile-postgres.d.ts +22 -0
- package/dist/storage-postgres/compile-postgres.js +830 -0
- package/dist/storage-postgres/compile-postgres.js.map +1 -0
- package/dist/storage-postgres/errors.d.ts +34 -0
- package/dist/storage-postgres/errors.js +50 -0
- package/dist/storage-postgres/errors.js.map +1 -0
- package/dist/storage-postgres/migrations/0001_init.sql +113 -0
- package/dist/storage-postgres/migrations/0002_content_hash.sql +7 -0
- package/dist/storage-postgres/migrations/index.d.ts +22 -0
- package/dist/storage-postgres/migrations/index.js +70 -0
- package/dist/storage-postgres/migrations/index.js.map +1 -0
- package/dist/storage-sqlite/adapter.d.ts +2 -0
- package/dist/storage-sqlite/adapter.js +688 -0
- package/dist/storage-sqlite/adapter.js.map +1 -0
- package/dist/storage-sqlite/compile-filter.d.ts +41 -0
- package/dist/storage-sqlite/compile-filter.js +881 -0
- package/dist/storage-sqlite/compile-filter.js.map +1 -0
- package/dist/storage-sqlite/compile-sqlite.d.ts +19 -0
- package/dist/storage-sqlite/compile-sqlite.js +176 -0
- package/dist/storage-sqlite/compile-sqlite.js.map +1 -0
- package/dist/storage-sqlite/migrations/0001_init.sql +123 -0
- package/dist/storage-sqlite/migrations/0002_content_hash.sql +7 -0
- package/dist/storage-sqlite/migrations/index.d.ts +6 -0
- package/dist/storage-sqlite/migrations/index.js +41 -0
- package/dist/storage-sqlite/migrations/index.js.map +1 -0
- package/dist/storage-sqlite/vec.d.ts +40 -0
- package/dist/storage-sqlite/vec.js +62 -0
- package/dist/storage-sqlite/vec.js.map +1 -0
- package/dist/sync/cursor.d.ts +36 -0
- package/dist/sync/cursor.js +66 -0
- package/dist/sync/cursor.js.map +1 -0
- package/dist/sync/daemon.d.ts +40 -0
- package/dist/sync/daemon.js +202 -0
- package/dist/sync/daemon.js.map +1 -0
- package/dist/sync/feed.d.ts +38 -0
- package/dist/sync/feed.js +125 -0
- package/dist/sync/feed.js.map +1 -0
- package/dist/sync/fs-store.d.ts +8 -0
- package/dist/sync/fs-store.js +76 -0
- package/dist/sync/fs-store.js.map +1 -0
- package/dist/sync/intrinsics.d.ts +52 -0
- package/dist/sync/intrinsics.js +76 -0
- package/dist/sync/intrinsics.js.map +1 -0
- package/dist/sync/paths.d.ts +38 -0
- package/dist/sync/paths.js +56 -0
- package/dist/sync/paths.js.map +1 -0
- package/dist/sync/push.d.ts +48 -0
- package/dist/sync/push.js +328 -0
- package/dist/sync/push.js.map +1 -0
- package/dist/sync/reconcile.d.ts +54 -0
- package/dist/sync/reconcile.js +252 -0
- package/dist/sync/reconcile.js.map +1 -0
- package/dist/sync/run.d.ts +27 -0
- package/dist/sync/run.js +46 -0
- package/dist/sync/run.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel — the only place the read model, error catalog, and slug↔id
|
|
3
|
+
* translation live. Surfaces (CLI, MCP, REST) call these methods and never
|
|
4
|
+
* touch the storage layer directly.
|
|
5
|
+
*
|
|
6
|
+
* No-auth model (design §8, noauth plan): the kernel trusts every caller.
|
|
7
|
+
* Every op takes a uniform `ctx: CallContext` first parameter — a plain
|
|
8
|
+
* caller-supplied value, not a resolved identity. Writes stamp `ctx.author`
|
|
9
|
+
* (default "mrplex"); reads narrow visibility through `ctx.scope` when present.
|
|
10
|
+
*
|
|
11
|
+
* All kernel methods are async (m5-plan WS1). SQLite adapter is
|
|
12
|
+
* internally synchronous so calls resolve on the next microtask; the
|
|
13
|
+
* async signature is honesty about the Postgres adapter and the
|
|
14
|
+
* embedding hook.
|
|
15
|
+
*/
|
|
16
|
+
import { randomBytes } from "node:crypto";
|
|
17
|
+
import { backfillRepoLinks } from "../links/backfill.js";
|
|
18
|
+
import { HARDCODED_DEFAULTS as LINK_DEFAULTS, effectiveLinkConfig, parseRepoOverride as parseLinkOverride, validateRepoOverride as validateLinkOverride, } from "../links/link-config.js";
|
|
19
|
+
import { bindDanglingToPath, reindexOutboundLinks } from "../links/maintain.js";
|
|
20
|
+
import { planRepairs } from "../links/repair.js";
|
|
21
|
+
import { findStaleLinks } from "../links/stale.js";
|
|
22
|
+
import { contentHash } from "../markdown/content-hash.js";
|
|
23
|
+
import { globToRegexSource } from "./auth/glob.js";
|
|
24
|
+
import { claimsGrantRead, claimsGrantRepo, normalizeClaims, } from "./auth/scope.js";
|
|
25
|
+
import { normalizeKey } from "./casefold.js";
|
|
26
|
+
import { resolveAuthor } from "./context.js";
|
|
27
|
+
import { deletionPath, pathIsInSystemNamespace } from "./deletion.js";
|
|
28
|
+
import { runDiff } from "./diff.js";
|
|
29
|
+
import { KernelError, docNotFound, forbidden, repoNotFound, versionNotFound } from "./errors.js";
|
|
30
|
+
import { canonicalizeFrontmatter, } from "./frontmatter-input.js";
|
|
31
|
+
import { runGraph } from "./graph.js";
|
|
32
|
+
import { HISTORY_INDEX_DEFAULT_LIMIT, HISTORY_SAFETY_WINDOW_MS, HISTORY_SINCE_DEFAULT_LIMIT, runHistoryIndex, runHistorySince, } from "./history.js";
|
|
33
|
+
import { HARDCODED_DEFAULTS, effectivePathConfig, parseRepoOverride, pathWarning, validateRepoOverride, } from "./path-config.js";
|
|
34
|
+
import { DEFAULT_QUERY_LIMIT, runQuery } from "./query/query.js";
|
|
35
|
+
import { isPathGlobPattern, normalizeExactDocumentPath, validatePath, validateSlug, } from "./validation.js";
|
|
36
|
+
import { decodeVersionId, encodeVersionId } from "./version-id.js";
|
|
37
|
+
/** Hard cap on named paths per `docs.get_many` call (after dedupe). */
|
|
38
|
+
export const GET_MANY_MAX_PATHS = DEFAULT_QUERY_LIMIT;
|
|
39
|
+
export function createKernel(config) {
|
|
40
|
+
const cfg = "storage" in config
|
|
41
|
+
? config
|
|
42
|
+
: { storage: config };
|
|
43
|
+
const storage = cfg.storage;
|
|
44
|
+
const serverPathConfig = cfg.serverPathConfig ?? HARDCODED_DEFAULTS;
|
|
45
|
+
const serverLinkConfig = cfg.serverLinkConfig ?? LINK_DEFAULTS;
|
|
46
|
+
const onVersionCommitted = cfg.onVersionCommitted;
|
|
47
|
+
const queryEmbed = cfg.queryEmbed;
|
|
48
|
+
function toRepoWire(row) {
|
|
49
|
+
return {
|
|
50
|
+
repo: row.slug,
|
|
51
|
+
path_config: row.path_config ? JSON.parse(row.path_config) : null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function toVersionWire(row, repoSlug) {
|
|
55
|
+
return {
|
|
56
|
+
version_id: encodeVersionId(row.id),
|
|
57
|
+
prev_version_id: row.prev_id === null ? null : encodeVersionId(row.prev_id),
|
|
58
|
+
next_version_id: row.next_id === null ? null : encodeVersionId(row.next_id),
|
|
59
|
+
repo: repoSlug,
|
|
60
|
+
path: row.path,
|
|
61
|
+
frontmatter: row.frontmatter,
|
|
62
|
+
frontmatter_raw: row.frontmatter_raw,
|
|
63
|
+
body: row.body,
|
|
64
|
+
author: row.author,
|
|
65
|
+
created_at: row.created_at,
|
|
66
|
+
// Pre-backfill rows (written before migration 0002) carry a null
|
|
67
|
+
// column; compute on the fly during the transition (§2.6).
|
|
68
|
+
content_hash: row.content_hash ?? contentHash(row.frontmatter_raw, row.body),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function repoEffectiveConfig(repo) {
|
|
72
|
+
return effectivePathConfig(serverPathConfig, parseRepoOverride(repo.path_config));
|
|
73
|
+
}
|
|
74
|
+
function repoEffectiveLinkConfig(repo) {
|
|
75
|
+
return effectiveLinkConfig(serverLinkConfig, parseLinkOverride(repo.link_config));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Normalized read claims for this call, or null when the context supplies no
|
|
79
|
+
* scope (full visibility). Null short-circuits every visibility check.
|
|
80
|
+
*/
|
|
81
|
+
function claimsFor(ctx) {
|
|
82
|
+
return ctx.scope === undefined ? null : normalizeClaims(ctx.scope);
|
|
83
|
+
}
|
|
84
|
+
async function resolveRepo(ctx, slug) {
|
|
85
|
+
const row = await storage.repos_by_slug(slug);
|
|
86
|
+
if (!row)
|
|
87
|
+
throw repoNotFound(slug);
|
|
88
|
+
// A read claim that doesn't bind this repo at all hides it entirely — same
|
|
89
|
+
// shape as not-found so out-of-claim leaks no existence information (§8.4).
|
|
90
|
+
const claims = claimsFor(ctx);
|
|
91
|
+
if (claims && !claimsGrantRepo(claims, row.slug))
|
|
92
|
+
throw repoNotFound(slug);
|
|
93
|
+
return row;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Throw `forbidden` when a targeted read (docs.get / get_version / history /
|
|
97
|
+
* diff) names a path outside the call's claims. Absent scope = full access.
|
|
98
|
+
*/
|
|
99
|
+
function assertReadable(claims, repoSlug, path) {
|
|
100
|
+
if (claims === null)
|
|
101
|
+
return;
|
|
102
|
+
if (!claimsGrantRead(claims, repoSlug, path))
|
|
103
|
+
throw forbidden();
|
|
104
|
+
}
|
|
105
|
+
function normalizeDocPath(repo, path) {
|
|
106
|
+
return normalizeExactDocumentPath(path, repoEffectiveConfig(repo));
|
|
107
|
+
}
|
|
108
|
+
function dedupeGetManyPaths(paths, repo) {
|
|
109
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
110
|
+
throw new KernelError("filter_invalid", {
|
|
111
|
+
reason: "paths must be a non-empty array of strings",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const unique = [];
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (const p of paths) {
|
|
117
|
+
if (typeof p !== "string") {
|
|
118
|
+
throw new KernelError("filter_invalid", {
|
|
119
|
+
reason: "paths must be a non-empty array of strings",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const canonical = normalizeDocPath(repo, p);
|
|
123
|
+
const key = normalizeKey(canonical);
|
|
124
|
+
if (!seen.has(key)) {
|
|
125
|
+
seen.add(key);
|
|
126
|
+
unique.push(canonical);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (unique.length > GET_MANY_MAX_PATHS) {
|
|
130
|
+
throw new KernelError("payload_too_large", {
|
|
131
|
+
limit: GET_MANY_MAX_PATHS,
|
|
132
|
+
got: unique.length,
|
|
133
|
+
reason: `paths exceeds ${GET_MANY_MAX_PATHS}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return unique;
|
|
137
|
+
}
|
|
138
|
+
function currentPathForStaleError(claims, repoSlug, path) {
|
|
139
|
+
if (claims === null)
|
|
140
|
+
return path;
|
|
141
|
+
return claimsGrantRead(claims, repoSlug, path) ? path : null;
|
|
142
|
+
}
|
|
143
|
+
function canonicalizeOrCarry(input, prev) {
|
|
144
|
+
if (input.frontmatter === undefined && input.frontmatter_raw === undefined && prev) {
|
|
145
|
+
return { frontmatter: prev.frontmatter, frontmatter_raw: prev.frontmatter_raw };
|
|
146
|
+
}
|
|
147
|
+
return canonicalizeFrontmatter(input);
|
|
148
|
+
}
|
|
149
|
+
function slugUniquifier() {
|
|
150
|
+
return randomBytes(3).toString("hex");
|
|
151
|
+
}
|
|
152
|
+
function isSlugSystemNamespaced(slug) {
|
|
153
|
+
return serverPathConfig.system_sigils.some((sigil) => slug.startsWith(sigil));
|
|
154
|
+
}
|
|
155
|
+
function slugCollisionError(slug) {
|
|
156
|
+
return new KernelError("slug_taken", { slug });
|
|
157
|
+
}
|
|
158
|
+
const kernel = {
|
|
159
|
+
repos: {
|
|
160
|
+
async list(ctx, opts) {
|
|
161
|
+
const includeSystem = opts?.include_system ?? false;
|
|
162
|
+
const claims = claimsFor(ctx);
|
|
163
|
+
const rows = await storage.repos_list();
|
|
164
|
+
return rows
|
|
165
|
+
.filter((r) => includeSystem || !isSlugSystemNamespaced(r.slug))
|
|
166
|
+
.filter((r) => claims === null || claimsGrantRepo(claims, r.slug))
|
|
167
|
+
.map(toRepoWire);
|
|
168
|
+
},
|
|
169
|
+
async get(ctx, slug) {
|
|
170
|
+
const repo = await storage.repos_by_slug(slug);
|
|
171
|
+
if (!repo)
|
|
172
|
+
throw repoNotFound(slug);
|
|
173
|
+
const claims = claimsFor(ctx);
|
|
174
|
+
if (claims && !claimsGrantRepo(claims, repo.slug))
|
|
175
|
+
throw repoNotFound(slug);
|
|
176
|
+
return toRepoWire(repo);
|
|
177
|
+
},
|
|
178
|
+
// The five destructive repo ops below intentionally ignore `ctx` (hence
|
|
179
|
+
// `_ctx`): per noauth-plan decision 11, destructive repo ops are ungated —
|
|
180
|
+
// reachable = allowed; the shell gates them by route/tool-name. This is
|
|
181
|
+
// deliberately asymmetric with `list`/`get` above, which DO filter by
|
|
182
|
+
// read claim: a scoped caller can `delete` a repo it can't `list`. That
|
|
183
|
+
// asymmetry is the design, not an oversight — don't "fix" it by threading
|
|
184
|
+
// claims through here.
|
|
185
|
+
async create(_ctx, slug) {
|
|
186
|
+
validateSlug(slug, serverPathConfig);
|
|
187
|
+
if (await storage.repos_by_slug(slug))
|
|
188
|
+
throw slugCollisionError(slug);
|
|
189
|
+
const row = await storage.repos_create({ slug, created_at: new Date().toISOString() });
|
|
190
|
+
return toRepoWire(row);
|
|
191
|
+
},
|
|
192
|
+
async rename(_ctx, slug, new_slug) {
|
|
193
|
+
const repo = await storage.repos_by_slug(slug);
|
|
194
|
+
if (!repo)
|
|
195
|
+
throw repoNotFound(slug);
|
|
196
|
+
if (new_slug === slug)
|
|
197
|
+
return toRepoWire(repo);
|
|
198
|
+
validateSlug(new_slug, serverPathConfig);
|
|
199
|
+
// A folded-equal collision that resolves to THIS repo is a recasing
|
|
200
|
+
// (e.g. "notes" → "Notes"), not a conflict — allow it (§3.5.1).
|
|
201
|
+
const occupant = await storage.repos_by_slug(new_slug);
|
|
202
|
+
if (occupant && occupant.id !== repo.id)
|
|
203
|
+
throw slugCollisionError(new_slug);
|
|
204
|
+
return toRepoWire(await storage.repos_rename(repo.id, new_slug));
|
|
205
|
+
},
|
|
206
|
+
async delete(_ctx, slug) {
|
|
207
|
+
const repo = await storage.repos_by_slug(slug);
|
|
208
|
+
if (!repo)
|
|
209
|
+
throw repoNotFound(slug);
|
|
210
|
+
if (isSlugSystemNamespaced(repo.slug))
|
|
211
|
+
return toRepoWire(repo);
|
|
212
|
+
const sigil = serverPathConfig.system_sigils[0];
|
|
213
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
214
|
+
const newSlug = `${sigil}deleted-${repo.slug}-${slugUniquifier()}`;
|
|
215
|
+
if (await storage.repos_by_slug(newSlug))
|
|
216
|
+
continue;
|
|
217
|
+
return toRepoWire(await storage.repos_rename(repo.id, newSlug));
|
|
218
|
+
}
|
|
219
|
+
throw new Error("repos.delete: uniquifier collision (retries exhausted)");
|
|
220
|
+
},
|
|
221
|
+
async set_path_config(_ctx, slug, config) {
|
|
222
|
+
const repo = await storage.repos_by_slug(slug);
|
|
223
|
+
if (!repo)
|
|
224
|
+
throw repoNotFound(slug);
|
|
225
|
+
if (config !== null) {
|
|
226
|
+
validateRepoOverride(config, serverPathConfig);
|
|
227
|
+
}
|
|
228
|
+
const configJson = config === null ? null : JSON.stringify(config);
|
|
229
|
+
const updated = await storage.repos_set_path_config(repo.id, configJson);
|
|
230
|
+
const newEffective = effectivePathConfig(serverPathConfig, config);
|
|
231
|
+
const warnings = [];
|
|
232
|
+
for (const version of await storage.versions_live_by_repo(repo.id)) {
|
|
233
|
+
if (pathIsInSystemNamespace(version.path, newEffective.system_sigils)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
validatePath(version.path, newEffective);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
warnings.push(pathWarning(encodeVersionId(version.id), version.path, err));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { repo: toRepoWire(updated), warnings };
|
|
244
|
+
},
|
|
245
|
+
async set_link_config(_ctx, slug, config) {
|
|
246
|
+
const repo = await storage.repos_by_slug(slug);
|
|
247
|
+
if (!repo)
|
|
248
|
+
throw repoNotFound(slug);
|
|
249
|
+
if (config !== null) {
|
|
250
|
+
// Validates the merge; throws link_config_invalid on a bad override.
|
|
251
|
+
validateLinkOverride(config, serverLinkConfig);
|
|
252
|
+
}
|
|
253
|
+
const configJson = config === null ? null : JSON.stringify(config);
|
|
254
|
+
const updated = await storage.repos_set_link_config(repo.id, configJson);
|
|
255
|
+
// A config change makes the link index stale for this repo — re-extract
|
|
256
|
+
// the whole corpus under the new effective config now, so the index is
|
|
257
|
+
// immediately consistent (§11.2 "Config change ⇒ re-extraction").
|
|
258
|
+
const reindexed = await backfillRepoLinks(storage, repo.id, effectiveLinkConfig(serverLinkConfig, config));
|
|
259
|
+
return { repo: toRepoWire(updated), reindexed };
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
docs: {
|
|
263
|
+
async get(ctx, repoSlug, path) {
|
|
264
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
265
|
+
const canonical = normalizeDocPath(repo, path);
|
|
266
|
+
assertReadable(claimsFor(ctx), repo.slug, canonical);
|
|
267
|
+
const row = await storage.version_current(repo.id, canonical);
|
|
268
|
+
if (!row)
|
|
269
|
+
throw docNotFound(repoSlug, canonical);
|
|
270
|
+
return toVersionWire(row, repoSlug);
|
|
271
|
+
},
|
|
272
|
+
async get_many(ctx, repoSlug, paths) {
|
|
273
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
274
|
+
const uniquePaths = dedupeGetManyPaths(paths, repo);
|
|
275
|
+
const rows = await storage.versions_current_by_paths(repo.id, uniquePaths);
|
|
276
|
+
const byNorm = new Map(rows.map((r) => [normalizeKey(r.path), r]));
|
|
277
|
+
const claims = claimsFor(ctx);
|
|
278
|
+
const items = [];
|
|
279
|
+
const errors = [];
|
|
280
|
+
for (const path of uniquePaths) {
|
|
281
|
+
if (claims !== null && !claimsGrantRead(claims, repo.slug, path)) {
|
|
282
|
+
errors.push({ path, code: "forbidden", data: {} });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const row = byNorm.get(normalizeKey(path));
|
|
286
|
+
if (!row) {
|
|
287
|
+
errors.push({ path, code: "doc_not_found", data: { repo: repoSlug, path } });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
items.push(toVersionWire(row, repoSlug));
|
|
291
|
+
}
|
|
292
|
+
return { items, errors };
|
|
293
|
+
},
|
|
294
|
+
async get_version(ctx, repoSlug, versionId) {
|
|
295
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
296
|
+
const id = decodeVersionId(versionId);
|
|
297
|
+
if (id === null)
|
|
298
|
+
throw versionNotFound(versionId);
|
|
299
|
+
const row = await storage.version_by_id(id);
|
|
300
|
+
if (!row || row.repo_id !== repo.id)
|
|
301
|
+
throw versionNotFound(versionId);
|
|
302
|
+
assertReadable(claimsFor(ctx), repo.slug, row.path);
|
|
303
|
+
return toVersionWire(row, repoSlug);
|
|
304
|
+
},
|
|
305
|
+
async history(ctx, repoSlug, path, opts) {
|
|
306
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
307
|
+
const canonical = normalizeDocPath(repo, path);
|
|
308
|
+
assertReadable(claimsFor(ctx), repo.slug, canonical);
|
|
309
|
+
const current = await storage.version_current(repo.id, canonical);
|
|
310
|
+
if (!current)
|
|
311
|
+
throw docNotFound(repoSlug, canonical);
|
|
312
|
+
const rows = await storage.version_history(current.document_id, opts);
|
|
313
|
+
return rows.map((r) => toVersionWire(r, repoSlug));
|
|
314
|
+
},
|
|
315
|
+
async diff(ctx, repoSlug, path, fromVersionId, toVersionId) {
|
|
316
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
317
|
+
const canonical = normalizeDocPath(repo, path);
|
|
318
|
+
const claims = claimsFor(ctx);
|
|
319
|
+
return runDiff({
|
|
320
|
+
repo: repoSlug,
|
|
321
|
+
path: canonical,
|
|
322
|
+
from_version_id: fromVersionId,
|
|
323
|
+
to_version_id: toVersionId,
|
|
324
|
+
}, {
|
|
325
|
+
storage,
|
|
326
|
+
resolveReadRepo: (slug) => resolveRepo(ctx, slug),
|
|
327
|
+
authorizeReadPath: (repoSlug, p) => assertReadable(claims, repoSlug, p),
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
async create(ctx, repoSlug, path, input) {
|
|
331
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
332
|
+
const canonical = normalizeDocPath(repo, path);
|
|
333
|
+
const author = resolveAuthor(ctx);
|
|
334
|
+
const canon = canonicalizeFrontmatter(input);
|
|
335
|
+
const { version, insertedId } = await storage.tx(async () => {
|
|
336
|
+
const existing = await storage.version_current(repo.id, canonical);
|
|
337
|
+
if (existing) {
|
|
338
|
+
throw new KernelError("create_conflict", {
|
|
339
|
+
repo: repoSlug,
|
|
340
|
+
path: canonical,
|
|
341
|
+
current_version_id: encodeVersionId(existing.id),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
const doc = await storage.documents_create(repo.id);
|
|
345
|
+
const inserted = await storage.version_insert({
|
|
346
|
+
document_id: doc.id,
|
|
347
|
+
repo_id: repo.id,
|
|
348
|
+
prev_id: null,
|
|
349
|
+
path: canonical,
|
|
350
|
+
frontmatter_raw: canon.frontmatter_raw,
|
|
351
|
+
frontmatter: canon.frontmatter,
|
|
352
|
+
body: input.body,
|
|
353
|
+
author,
|
|
354
|
+
created_at: new Date().toISOString(),
|
|
355
|
+
});
|
|
356
|
+
// Links (§11.2): index this doc's outbound edges, and bind any
|
|
357
|
+
// danglers that were waiting for a document at this path.
|
|
358
|
+
const linkConfig = repoEffectiveLinkConfig(repo);
|
|
359
|
+
await reindexOutboundLinks(storage, linkConfig, repo.id, doc.id, canonical, input.body, canon.frontmatter);
|
|
360
|
+
await bindDanglingToPath(storage, repo.id, canonical, doc.id);
|
|
361
|
+
return {
|
|
362
|
+
version: toVersionWire(inserted, repoSlug),
|
|
363
|
+
insertedId: inserted.id,
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
await onVersionCommitted?.(insertedId);
|
|
367
|
+
return version;
|
|
368
|
+
},
|
|
369
|
+
async put(ctx, repoSlug, prevVersionId, destPath, input) {
|
|
370
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
371
|
+
const prevId = decodeVersionId(prevVersionId);
|
|
372
|
+
if (prevId === null)
|
|
373
|
+
throw versionNotFound(prevVersionId);
|
|
374
|
+
const prev = await storage.version_by_id(prevId);
|
|
375
|
+
if (!prev || prev.repo_id !== repo.id)
|
|
376
|
+
throw versionNotFound(prevVersionId);
|
|
377
|
+
const canonicalDest = normalizeDocPath(repo, destPath);
|
|
378
|
+
const author = resolveAuthor(ctx);
|
|
379
|
+
const canon = canonicalizeOrCarry(input, prev);
|
|
380
|
+
const body = input.body ?? prev.body;
|
|
381
|
+
const claims = claimsFor(ctx);
|
|
382
|
+
const { version, insertedId } = await storage.tx(async () => {
|
|
383
|
+
const current = await storage.version_current(repo.id, prev.path);
|
|
384
|
+
if (!current || current.id !== prev.id) {
|
|
385
|
+
const docCurrent = current ?? (await storage.version_history(prev.document_id, { limit: 1 }))[0] ?? null;
|
|
386
|
+
throw new KernelError("stale_prev", {
|
|
387
|
+
current_version_id: docCurrent ? encodeVersionId(docCurrent.id) : null,
|
|
388
|
+
current_path: docCurrent
|
|
389
|
+
? currentPathForStaleError(claims, repo.slug, docCurrent.path)
|
|
390
|
+
: null,
|
|
391
|
+
submitted_prev_version_id: prevVersionId,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (canonicalDest !== prev.path) {
|
|
395
|
+
const occupant = await storage.version_current(repo.id, canonicalDest);
|
|
396
|
+
if (occupant && occupant.document_id !== prev.document_id) {
|
|
397
|
+
throw new KernelError("path_taken", {
|
|
398
|
+
repo: repoSlug,
|
|
399
|
+
path: canonicalDest,
|
|
400
|
+
current_version_id: encodeVersionId(occupant.id),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const inserted = await storage.version_insert({
|
|
405
|
+
document_id: prev.document_id,
|
|
406
|
+
repo_id: repo.id,
|
|
407
|
+
prev_id: prev.id,
|
|
408
|
+
path: canonicalDest,
|
|
409
|
+
frontmatter_raw: canon.frontmatter_raw,
|
|
410
|
+
frontmatter: canon.frontmatter,
|
|
411
|
+
body,
|
|
412
|
+
author,
|
|
413
|
+
created_at: new Date().toISOString(),
|
|
414
|
+
});
|
|
415
|
+
// Links (§11.2): re-extract this doc's outbound edges from the new
|
|
416
|
+
// version (body/frontmatter or the source path may have changed).
|
|
417
|
+
// Inbound edges are identity-bound, so a move needs no inbound
|
|
418
|
+
// churn; but a move INTO a path that has danglers binds them.
|
|
419
|
+
const linkConfig = repoEffectiveLinkConfig(repo);
|
|
420
|
+
await reindexOutboundLinks(storage, linkConfig, repo.id, prev.document_id, canonicalDest, body, canon.frontmatter);
|
|
421
|
+
if (canonicalDest !== prev.path) {
|
|
422
|
+
await bindDanglingToPath(storage, repo.id, canonicalDest, prev.document_id);
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
version: toVersionWire(inserted, repoSlug),
|
|
426
|
+
insertedId: inserted.id,
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
await onVersionCommitted?.(insertedId);
|
|
430
|
+
return version;
|
|
431
|
+
},
|
|
432
|
+
async delete(ctx, repoSlug, prevVersionId) {
|
|
433
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
434
|
+
const prevId = decodeVersionId(prevVersionId);
|
|
435
|
+
if (prevId === null)
|
|
436
|
+
throw versionNotFound(prevVersionId);
|
|
437
|
+
const prev = await storage.version_by_id(prevId);
|
|
438
|
+
if (!prev || prev.repo_id !== repo.id)
|
|
439
|
+
throw versionNotFound(prevVersionId);
|
|
440
|
+
const cfg = repoEffectiveConfig(repo);
|
|
441
|
+
// Deletion is an authored act by the deleter (noauth plan §1).
|
|
442
|
+
const author = resolveAuthor(ctx);
|
|
443
|
+
const claims = claimsFor(ctx);
|
|
444
|
+
const currentAtPrevPath = await storage.version_current(repo.id, prev.path);
|
|
445
|
+
if (!currentAtPrevPath || currentAtPrevPath.id !== prev.id) {
|
|
446
|
+
const docCurrent = currentAtPrevPath ??
|
|
447
|
+
(await storage.version_history(prev.document_id, { limit: 1 }))[0] ??
|
|
448
|
+
null;
|
|
449
|
+
throw new KernelError("stale_prev", {
|
|
450
|
+
current_version_id: docCurrent ? encodeVersionId(docCurrent.id) : null,
|
|
451
|
+
current_path: docCurrent
|
|
452
|
+
? currentPathForStaleError(claims, repo.slug, docCurrent.path)
|
|
453
|
+
: null,
|
|
454
|
+
submitted_prev_version_id: prevVersionId,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
if (pathIsInSystemNamespace(prev.path, cfg.system_sigils)) {
|
|
458
|
+
return toVersionWire(prev, repoSlug);
|
|
459
|
+
}
|
|
460
|
+
const destPath = deletionPath(cfg.system_sigils[0], prev.path, encodeVersionId(prev.id));
|
|
461
|
+
const { version, insertedId } = await storage.tx(async () => {
|
|
462
|
+
const current = await storage.version_current(repo.id, prev.path);
|
|
463
|
+
if (!current || current.id !== prev.id) {
|
|
464
|
+
const docCurrent = current ?? (await storage.version_history(prev.document_id, { limit: 1 }))[0] ?? null;
|
|
465
|
+
throw new KernelError("stale_prev", {
|
|
466
|
+
current_version_id: docCurrent ? encodeVersionId(docCurrent.id) : null,
|
|
467
|
+
current_path: docCurrent
|
|
468
|
+
? currentPathForStaleError(claims, repo.slug, docCurrent.path)
|
|
469
|
+
: null,
|
|
470
|
+
submitted_prev_version_id: prevVersionId,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
const inserted = await storage.version_insert({
|
|
474
|
+
document_id: prev.document_id,
|
|
475
|
+
repo_id: repo.id,
|
|
476
|
+
prev_id: prev.id,
|
|
477
|
+
path: destPath,
|
|
478
|
+
frontmatter_raw: prev.frontmatter_raw,
|
|
479
|
+
frontmatter: prev.frontmatter,
|
|
480
|
+
body: prev.body,
|
|
481
|
+
author,
|
|
482
|
+
created_at: new Date().toISOString(),
|
|
483
|
+
});
|
|
484
|
+
// Links (§11.2): clear the deleted doc's OUTBOUND edges — a doc in
|
|
485
|
+
// the system namespace links to nothing readable. INBOUND edges
|
|
486
|
+
// (target = this doc) stay bound; visibility filtering excludes
|
|
487
|
+
// them from live-namespace queries, and they rebind naturally if
|
|
488
|
+
// the doc is restored.
|
|
489
|
+
await storage.links_clear(prev.document_id);
|
|
490
|
+
return {
|
|
491
|
+
version: toVersionWire(inserted, repoSlug),
|
|
492
|
+
insertedId: inserted.id,
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
await onVersionCommitted?.(insertedId);
|
|
496
|
+
return version;
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
links: {
|
|
500
|
+
async backfill(ctx, repoSlug) {
|
|
501
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
502
|
+
return backfillRepoLinks(storage, repo.id, repoEffectiveLinkConfig(repo));
|
|
503
|
+
},
|
|
504
|
+
async stale(ctx, repoSlug) {
|
|
505
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
506
|
+
const rows = await findStaleLinks(storage, repo.id, repoEffectiveLinkConfig(repo));
|
|
507
|
+
// Scope (§11.2 "Scope interaction" — visible graph = readable graph):
|
|
508
|
+
// surface a stale link only when the caller can read BOTH endpoints.
|
|
509
|
+
// The SOURCE path is what we return + would rewrite; the `current`
|
|
510
|
+
// (TARGET) path we'd expose is private if the caller can't read it —
|
|
511
|
+
// dropping the row prevents leaking the moved target's new location.
|
|
512
|
+
const claims = claimsFor(ctx);
|
|
513
|
+
const canRead = (p) => claims === null || claimsGrantRead(claims, repo.slug, p);
|
|
514
|
+
return rows
|
|
515
|
+
.filter((r) => canRead(r.source_path) && canRead(r.current))
|
|
516
|
+
.map((r) => ({
|
|
517
|
+
repo: repoSlug,
|
|
518
|
+
source_path: r.source_path,
|
|
519
|
+
ord: r.ord,
|
|
520
|
+
written: r.written,
|
|
521
|
+
current: r.current,
|
|
522
|
+
}));
|
|
523
|
+
},
|
|
524
|
+
async repair(ctx, repoSlug, opts) {
|
|
525
|
+
const dryRun = opts?.dry_run ?? false;
|
|
526
|
+
const repo = await resolveRepo(ctx, repoSlug);
|
|
527
|
+
const plans = await planRepairs(storage, repo.id, repoEffectiveLinkConfig(repo));
|
|
528
|
+
const repaired = [];
|
|
529
|
+
const skipped = [];
|
|
530
|
+
const claims = claimsFor(ctx);
|
|
531
|
+
for (const plan of plans) {
|
|
532
|
+
// Only repair docs the caller may read (no write scopes in the
|
|
533
|
+
// engine — read claims gate what's touchable, noauth plan §1 "Out").
|
|
534
|
+
if (claims && !claimsGrantRead(claims, repo.slug, plan.version.path)) {
|
|
535
|
+
skipped.push({ path: plan.version.path, reason: "forbidden" });
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (dryRun) {
|
|
539
|
+
repaired.push({ path: plan.version.path, edges: plan.edges });
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
// Repair preserves authorship: the original author effectively
|
|
544
|
+
// wrote this content (only stale link text changed), so carry
|
|
545
|
+
// forward prev.author rather than stamping the repair caller
|
|
546
|
+
// (noauth plan decision 7).
|
|
547
|
+
await kernel.docs.put({ ...ctx, author: plan.version.author }, repoSlug, encodeVersionId(plan.version.id), plan.version.path, { body: plan.newBody });
|
|
548
|
+
repaired.push({ path: plan.version.path, edges: plan.edges });
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
if (err instanceof KernelError && err.code === "stale_prev") {
|
|
552
|
+
skipped.push({ path: plan.version.path, reason: "conflict" });
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return { dry_run: dryRun, repaired, skipped };
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
async query(ctx, spec) {
|
|
563
|
+
return runQuery(claimsFor(ctx), spec, {
|
|
564
|
+
storage,
|
|
565
|
+
serverPathConfig,
|
|
566
|
+
toVersionWire,
|
|
567
|
+
queryEmbed,
|
|
568
|
+
});
|
|
569
|
+
},
|
|
570
|
+
async graph(ctx, spec) {
|
|
571
|
+
const repo = await storage.repos_by_slug(spec.repo);
|
|
572
|
+
const pathConfig = repo === null
|
|
573
|
+
? serverPathConfig
|
|
574
|
+
: effectivePathConfig(serverPathConfig, parseRepoOverride(repo.path_config));
|
|
575
|
+
const deps = { storage, serverPathConfig, pathConfig };
|
|
576
|
+
return runGraph(claimsFor(ctx), spec, deps);
|
|
577
|
+
},
|
|
578
|
+
history: {
|
|
579
|
+
async since(ctx, input) {
|
|
580
|
+
// Resolve the optional repo filter to an id (and gate its existence
|
|
581
|
+
// through the caller's scope, same as resolveRepo).
|
|
582
|
+
let repoId;
|
|
583
|
+
if (input.repo !== undefined) {
|
|
584
|
+
const repo = await resolveRepo(ctx, input.repo);
|
|
585
|
+
repoId = repo.id;
|
|
586
|
+
}
|
|
587
|
+
// Build id→slug once per call.
|
|
588
|
+
const repos = await storage.repos_list();
|
|
589
|
+
const slugById = new Map(repos.map((r) => [r.id, r.slug]));
|
|
590
|
+
const claims = claimsFor(ctx);
|
|
591
|
+
const deps = {
|
|
592
|
+
storage,
|
|
593
|
+
repoSlug: (id) => slugById.get(id) ?? "",
|
|
594
|
+
// Delete tombstones live under the server's system sigils (`:deleted/`
|
|
595
|
+
// etc.); these are server-wide, so the server config is authoritative.
|
|
596
|
+
isSystemPath: (path) => pathIsInSystemNamespace(path, serverPathConfig.system_sigils),
|
|
597
|
+
canRead: (id, path) => {
|
|
598
|
+
if (claims === null)
|
|
599
|
+
return true;
|
|
600
|
+
const slug = slugById.get(id);
|
|
601
|
+
return slug !== undefined && claimsGrantRead(claims, slug, path);
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
return runHistorySince({
|
|
605
|
+
after_version: input.after_version,
|
|
606
|
+
repo_id: repoId,
|
|
607
|
+
limit: input.limit ?? HISTORY_SINCE_DEFAULT_LIMIT,
|
|
608
|
+
now_ms: Date.now(),
|
|
609
|
+
window_ms: HISTORY_SAFETY_WINDOW_MS,
|
|
610
|
+
}, deps);
|
|
611
|
+
},
|
|
612
|
+
async index(ctx, input) {
|
|
613
|
+
const repo = await resolveRepo(ctx, input.repo);
|
|
614
|
+
const cfg = repoEffectiveConfig(repo);
|
|
615
|
+
// Exclude system (`:deleted/`) and hidden (`.`) namespaces, as `query`
|
|
616
|
+
// defaults do — the sync live set is the visible, current corpus.
|
|
617
|
+
const excludedSigils = [...cfg.system_sigils, ...cfg.hidden_sigils];
|
|
618
|
+
const claims = claimsFor(ctx);
|
|
619
|
+
const deps = {
|
|
620
|
+
storage,
|
|
621
|
+
isExcluded: (path) => pathIsInSystemNamespace(path, excludedSigils),
|
|
622
|
+
canRead: (path) => claims === null || claimsGrantRead(claims, repo.slug, path),
|
|
623
|
+
};
|
|
624
|
+
return runHistoryIndex({
|
|
625
|
+
repo_id: repo.id,
|
|
626
|
+
through_version: input.through_version,
|
|
627
|
+
after_version: input.after_version,
|
|
628
|
+
limit: input.limit ?? HISTORY_INDEX_DEFAULT_LIMIT,
|
|
629
|
+
now_ms: Date.now(),
|
|
630
|
+
window_ms: HISTORY_SAFETY_WINDOW_MS,
|
|
631
|
+
}, deps);
|
|
632
|
+
},
|
|
633
|
+
async list(ctx, input) {
|
|
634
|
+
const repo = await resolveRepo(ctx, input.repo);
|
|
635
|
+
const cfg = repoEffectiveConfig(repo);
|
|
636
|
+
const claims = claimsFor(ctx);
|
|
637
|
+
// Compile the path glob to an anchored regex source (the same dialect
|
|
638
|
+
// scope + graph use). Omitted glob = every document in the repo.
|
|
639
|
+
// Literal paths accept one leading `/`; glob operands keep theirs.
|
|
640
|
+
const pathGlob = input.path === undefined
|
|
641
|
+
? undefined
|
|
642
|
+
: isPathGlobPattern(input.path)
|
|
643
|
+
? input.path
|
|
644
|
+
: normalizeExactDocumentPath(input.path, cfg);
|
|
645
|
+
const pathRegexes = pathGlob === undefined ? [] : [`^${globToRegexSource(pathGlob)}$`];
|
|
646
|
+
// Reject an unparseable bound rather than coercing to 0 — for `until`
|
|
647
|
+
// that would silently mean `id <= 0` (empty result), inverting intent.
|
|
648
|
+
const decodeBound = (kind, value) => {
|
|
649
|
+
const id = decodeVersionId(value);
|
|
650
|
+
if (id === null) {
|
|
651
|
+
throw new KernelError("version_not_found", { version_id: value, bound: kind });
|
|
652
|
+
}
|
|
653
|
+
return id;
|
|
654
|
+
};
|
|
655
|
+
const rows = await storage.versions_list({
|
|
656
|
+
repo_id: repo.id,
|
|
657
|
+
path_regexes: pathRegexes,
|
|
658
|
+
ever: input.ever ?? false,
|
|
659
|
+
after_id: input.since !== undefined ? decodeBound("since", input.since) : undefined,
|
|
660
|
+
until_id: input.until !== undefined ? decodeBound("until", input.until) : undefined,
|
|
661
|
+
order: input.order ?? "desc",
|
|
662
|
+
limit: input.limit ?? HISTORY_LIST_DEFAULT_LIMIT,
|
|
663
|
+
});
|
|
664
|
+
// Scope: a version is visible only if the caller can read its path.
|
|
665
|
+
return rows
|
|
666
|
+
.filter((r) => claims === null || claimsGrantRead(claims, repo.slug, r.path))
|
|
667
|
+
.map((r) => toVersionWire(r, repo.slug));
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
return kernel;
|
|
672
|
+
}
|
|
673
|
+
/** Default `history.list` page size when a caller omits `limit` (§3.5). */
|
|
674
|
+
const HISTORY_LIST_DEFAULT_LIMIT = 100;
|
|
675
|
+
//# sourceMappingURL=kernel.js.map
|