@hraness/wordcell 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -15
- package/dist/agent-context.js +1 -0
- package/dist/agent-guide-audit.js +1 -0
- package/dist/attachments.js +1 -0
- package/dist/authoring.js +1 -0
- package/dist/authority-4sp9d552.js +663 -0
- package/dist/benchmark.js +1 -0
- package/dist/capture.js +1 -0
- package/dist/cli.js +3781 -32
- package/dist/clip/acquire.js +1 -0
- package/dist/clip/args.js +1 -0
- package/dist/clip/bounded-byte-buffer.js +1 -0
- package/dist/clip/browser-profiles.js +1 -0
- package/dist/clip/bundle-reader.js +1 -0
- package/dist/clip/cli.js +1 -0
- package/dist/clip/cookies.js +1 -0
- package/dist/clip/doctor.js +1 -0
- package/dist/clip/jobs.js +1 -0
- package/dist/clip/network-proxy.js +1 -0
- package/dist/clip/network.js +1 -0
- package/dist/clip/persist.js +1 -0
- package/dist/clip/refresh.js +1 -0
- package/dist/clip/terminal.js +1 -0
- package/dist/evaluation-builder.js +13 -5
- package/dist/evaluation-kb.js +11 -4
- package/dist/evaluation.js +1 -0
- package/dist/git.js +1 -0
- package/dist/graph-authority.js +29 -0
- package/dist/graph-percolation.js +16 -0
- package/dist/graph.js +1 -0
- package/dist/index-0k2x4nn9.js +393 -0
- package/dist/{index-n05s3wsb.js → index-115b07ap.js} +156 -541
- package/dist/index-11621h23.js +160 -0
- package/dist/{index-tkpxp48t.js → index-51bap0mq.js} +2 -2
- package/dist/{index-8a08v6x8.js → index-59ssmqqm.js} +1 -1
- package/dist/index-bcfn9xah.js +472 -0
- package/dist/index-bgfzwt4h.js +51 -0
- package/dist/{index-bs0yf4pp.js → index-pbr6rv47.js} +2 -2
- package/dist/{index-m7et4rz4.js → index-vdw5yjfa.js} +61 -9
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index.js +61 -34
- package/dist/navigation.js +1 -0
- package/dist/pdf.js +1 -0
- package/dist/percolate.js +1 -0
- package/dist/portfolio.js +11 -4
- package/dist/query.js +1 -0
- package/dist/repository-memory.js +1 -0
- package/dist/sdk.js +10 -3
- package/dist/search-rules.js +1 -0
- package/dist/search.js +1 -0
- package/dist/semantic.js +3 -1
- package/dist/source-inbox.js +1 -0
- package/dist/untrusted-content.js +1 -0
- package/dist/url-intelligence.js +1 -0
- package/dist/workflow.js +1 -0
- package/dist/workflows/decision-context.js +11 -4
- package/dist/workflows/explain-change.js +1 -0
- package/dist/workflows/index.js +11 -4
- package/dist/workflows/plan-radar.js +1 -0
- package/package.json +24 -6
- package/skills/wordcell/SKILL.md +2 -2
- package/skills/wordcell/references/percolate.md +4 -0
- package/skills/wordcell/references/query.md +6 -0
- package/src/cli-program.ts +45 -2
- package/src/graph-authority-model.ts +116 -0
- package/src/graph-authority.ts +219 -0
- package/src/graph-cache-filesystem.ts +4 -0
- package/src/graph-cli.ts +96 -0
- package/src/graph-facts.ts +206 -0
- package/src/graph-percolation.ts +39 -0
- package/src/graph-query.ts +122 -0
- package/src/index.ts +2 -0
- package/src/oh/authority.ts +193 -0
- package/src/oh/programs.ts +99 -0
- package/src/oh/schema.ts +40 -0
- package/src/oh/snapshot.ts +114 -0
- package/src/oh/validation.ts +61 -0
- package/src/sdk.ts +51 -6
- package/dist/index-sy0zety7.js +0 -3630
- package/dist/kb-alias.js +0 -55
- package/src/kb-alias.ts +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/wordcell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "A knowledge base for coding agents, built from Markdown, backlinks, semantic search, and Git context.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -261,11 +261,18 @@
|
|
|
261
261
|
"./workflows/plan-radar": {
|
|
262
262
|
"types": "./src/workflows/plan-radar.ts",
|
|
263
263
|
"import": "./dist/workflows/plan-radar.js"
|
|
264
|
+
},
|
|
265
|
+
"./graph-authority": {
|
|
266
|
+
"types": "./src/graph-authority.ts",
|
|
267
|
+
"import": "./dist/graph-authority.js"
|
|
268
|
+
},
|
|
269
|
+
"./graph-percolation": {
|
|
270
|
+
"types": "./src/graph-percolation.ts",
|
|
271
|
+
"import": "./dist/graph-percolation.js"
|
|
264
272
|
}
|
|
265
273
|
},
|
|
266
274
|
"bin": {
|
|
267
275
|
"wordcell": "./dist/cli.js",
|
|
268
|
-
"kb": "./dist/kb-alias.js",
|
|
269
276
|
"wordcell-evaluation-builder": "./dist/evaluation-builder.js"
|
|
270
277
|
},
|
|
271
278
|
"main": "./dist/index.js",
|
|
@@ -285,7 +292,6 @@
|
|
|
285
292
|
"src/url-intelligence.ts",
|
|
286
293
|
"src/cli.ts",
|
|
287
294
|
"src/cli-program.ts",
|
|
288
|
-
"src/kb-alias.ts",
|
|
289
295
|
"src/clip/acquire.ts",
|
|
290
296
|
"src/clip/archive-today.ts",
|
|
291
297
|
"src/clip/args.ts",
|
|
@@ -378,14 +384,26 @@
|
|
|
378
384
|
"src/workflows/plan-radar.ts",
|
|
379
385
|
"skills/wordcell",
|
|
380
386
|
"README.md",
|
|
381
|
-
"LICENSE"
|
|
387
|
+
"LICENSE",
|
|
388
|
+
"src/graph-authority.ts",
|
|
389
|
+
"src/graph-authority-model.ts",
|
|
390
|
+
"src/graph-cache-filesystem.ts",
|
|
391
|
+
"src/graph-facts.ts",
|
|
392
|
+
"src/graph-query.ts",
|
|
393
|
+
"src/graph-cli.ts",
|
|
394
|
+
"src/graph-percolation.ts",
|
|
395
|
+
"src/oh/authority.ts",
|
|
396
|
+
"src/oh/programs.ts",
|
|
397
|
+
"src/oh/schema.ts",
|
|
398
|
+
"src/oh/snapshot.ts",
|
|
399
|
+
"src/oh/validation.ts"
|
|
382
400
|
],
|
|
383
401
|
"scripts": {
|
|
384
402
|
"kb:refresh": "bunx --bun github:hraness/kb#v0.15.1 refresh --root kb",
|
|
385
403
|
"kb:check:lane": "bunx --bun github:hraness/kb#v0.15.1 check --root kb --no-catalog",
|
|
386
404
|
"kb:check": "bunx --bun github:hraness/kb#v0.15.1 check --root kb && bunx --bun github:hraness/kb#v0.15.1 agents check --root kb --repo .",
|
|
387
405
|
"kb:catalog": "bunx --bun github:hraness/kb#v0.15.1 catalog --root kb",
|
|
388
|
-
"build": "bun -e 'await (await import(\"node:fs/promises\")).rm(\"./dist\", { recursive: true, force: true })' && bun build ./src/index.ts ./src/agent-context.ts ./src/agent-guide-audit.ts ./src/attachments.ts ./src/authoring.ts ./src/benchmark.ts ./src/clip/browser-profiles.ts ./src/capture.ts ./src/cli.ts ./src/
|
|
406
|
+
"build": "bun -e 'await (await import(\"node:fs/promises\")).rm(\"./dist\", { recursive: true, force: true })' && bun build ./src/index.ts ./src/agent-context.ts ./src/agent-guide-audit.ts ./src/attachments.ts ./src/authoring.ts ./src/benchmark.ts ./src/clip/browser-profiles.ts ./src/capture.ts ./src/cli.ts ./src/clip/acquire.ts ./src/clip/args.ts ./src/clip/bounded-byte-buffer.ts ./src/clip/bundle-reader.ts ./src/clip/cli.ts ./src/clip/cookies.ts ./src/clip/doctor.ts ./src/clip/jobs.ts ./src/clip/network.ts ./src/clip/network-proxy.ts ./src/clip/persist.ts ./src/clip/refresh.ts ./src/clip/terminal.ts ./src/evaluation.ts ./src/evaluation-builder.ts ./src/evaluation-kb.ts ./src/git.ts ./src/graph.ts ./src/graph-percolation.ts ./src/graph-authority.ts ./src/navigation.ts ./src/pdf.ts ./src/percolate.ts ./src/portfolio.ts ./src/query.ts ./src/repository-memory.ts ./src/sdk.ts ./src/search.ts ./src/search-rules.ts ./src/semantic.ts ./src/source-inbox.ts ./src/untrusted-content.ts ./src/url-intelligence.ts ./src/workflow.ts ./src/workflows/index.ts ./src/workflows/decision-context.ts ./src/workflows/explain-change.ts ./src/workflows/plan-radar.ts ./src/clip/defuddle-worker.ts --outdir ./dist --root ./src --target bun --format esm --splitting --packages external",
|
|
389
407
|
"check:workflows": "bun ./scripts/check-workflow-yaml.ts",
|
|
390
408
|
"check:installed-command-docs": "bun ./scripts/check-installed-command-docs.ts",
|
|
391
409
|
"check:portfolio-inventory": "bun ./scripts/check-portfolio-inventory.ts",
|
|
@@ -400,7 +418,7 @@
|
|
|
400
418
|
"check:effect": "bun run scripts/check-effect-policy.ts && bun test ./scripts/check-effect-architecture.test.ts"
|
|
401
419
|
},
|
|
402
420
|
"dependencies": {
|
|
403
|
-
"@hraness/oh": "github
|
|
421
|
+
"@hraness/oh": "https://github.com/hraness/oh/releases/download/v0.4.3/hraness-oh-0.4.3.tgz",
|
|
404
422
|
"@steipete/sweet-cookie": "0.4.3",
|
|
405
423
|
"@tobilu/qmd": "git+https://github.com/hraness/qmd.git#aa993dceb3ef8cfb71d470554ca437570f5a2b3c",
|
|
406
424
|
"agent-browser": "0.32.3",
|
package/skills/wordcell/SKILL.md
CHANGED
|
@@ -53,9 +53,9 @@ rules, then repeat command discovery. Install Wordcell only while `wordcell` rem
|
|
|
53
53
|
missing:
|
|
54
54
|
|
|
55
55
|
```sh
|
|
56
|
-
command -v
|
|
56
|
+
command -v wordcell >/dev/null 2>&1 || {
|
|
57
57
|
command -v bun >/dev/null 2>&1 || exit 1
|
|
58
|
-
bun add --global --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.
|
|
58
|
+
bun add --global --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.21.0/hraness-wordcell-0.21.0.tgz
|
|
59
59
|
}
|
|
60
60
|
wordcell --help
|
|
61
61
|
```
|
|
@@ -174,3 +174,7 @@ authored-catalog vault, refresh and check leave the front door untouched, while
|
|
|
174
174
|
`wordcell catalog --root "$KB_ROOT"` renders an exhaustive disposable inventory.
|
|
175
175
|
Resolve same-note Git conflicts from the prose and evidence; do not accept one
|
|
176
176
|
side's frontmatter mechanically.
|
|
177
|
+
|
|
178
|
+
## Inspect positive graph proofs
|
|
179
|
+
|
|
180
|
+
Use `wordcell percolate <note> --proofs --root "$KB_ROOT" --json` when you need shared-tag and shared-concept proof trees. This explicit envelope keeps existing V2 candidates under `suggestions` and separate bounded query results under `positiveSupport`. Absence and counts remain Wordcell decisions; choosing a predicate and writing a relationship remain author decisions. Inspect all truncation markers and read the cited Markdown before editing.
|
|
@@ -250,3 +250,9 @@ then leave it untouched. `wordcell catalog --root "$KB_ROOT"` renders the exhaus
|
|
|
250
250
|
inventory on demand. A managed vault keeps the original generated-catalog
|
|
251
251
|
behavior. Neither mode changes scanning, graph analysis, semantic indexing, or
|
|
252
252
|
attachment validation.
|
|
253
|
+
|
|
254
|
+
## Query named graph programs
|
|
255
|
+
|
|
256
|
+
Use `wordcell graph query --program backlinks --note <exact-id> --root "$KB_ROOT" --json` for positive source proofs. Programs also include bounded `reachability`, predicate-specific `relation-closure`, exact `scope-route`, `shared-tags`, and `shared-concepts`. Default queries write nothing. `graph rebuild` writes only the disposable `.wordcell/oh.sqlite` cache; `graph verify` and queries with `--persisted` reject missing or stale state without repairing it. After a damaged cache, `graph rebuild --fresh` builds a replacement from Markdown.
|
|
257
|
+
|
|
258
|
+
Inspect the source revision, row/proof truncation and source record digests. Reopen SDK sessions after Markdown changes. Proofs establish derivation from source records, not the truth of an authored assertion. `wordcell graph --json` keeps its existing structural report.
|
package/src/cli-program.ts
CHANGED
|
@@ -68,6 +68,13 @@ import {
|
|
|
68
68
|
type RelationIssue,
|
|
69
69
|
type VaultAnalysis,
|
|
70
70
|
} from "./graph.js";
|
|
71
|
+
import {
|
|
72
|
+
executeGraphCommand,
|
|
73
|
+
parseGraphCommand,
|
|
74
|
+
type GraphCliCommand,
|
|
75
|
+
type GraphCliDependencies,
|
|
76
|
+
} from "./graph-cli.js";
|
|
77
|
+
import { percolateWithGraph } from "./graph-percolation.js";
|
|
71
78
|
import { initVault, type InitVaultResult } from "./init.js";
|
|
72
79
|
import type {
|
|
73
80
|
GitHistoryForNotesResult,
|
|
@@ -223,13 +230,16 @@ Usage:
|
|
|
223
230
|
wordcell check [--root <directory>] [--index <path>] [--no-catalog] [--json]
|
|
224
231
|
wordcell catalog [--root <directory>] [--index <path>] [--json]
|
|
225
232
|
wordcell graph [--root <directory>] [--index <path>] [--json]
|
|
233
|
+
wordcell graph rebuild [--fresh] [--root <directory>] [--index <path>] [--json]
|
|
234
|
+
wordcell graph verify [--root <directory>] [--index <path>] [--json]
|
|
235
|
+
wordcell graph query --program <backlinks|reachability|scope-route|relation-closure|shared-tags|shared-concepts> [--note <id> | --scope <path>] [--predicate <predicate>] [--depth <count>] [--limit <count>] [--persisted] [--root <directory>] [--index <path>] [--json]
|
|
226
236
|
wordcell backlinks <note> [--root <directory>] [--index <path>] [--json]
|
|
227
237
|
wordcell links <note> [--root <directory>] [--direction <in|out|both>] [--depth <count>] [--limit <count>] [--json]
|
|
228
238
|
wordcell note create <id> --title <title> [--type <type>] [--tag <tag>] [--body <markdown> | --body-file <path>] [--root <directory>] [--json]
|
|
229
239
|
wordcell relation add <source> <predicate> <target> [--root <directory>] [--expected-revision <sha256:...>] [--json]
|
|
230
240
|
wordcell relation remove <source> <predicate> <target> [--root <directory>] [--expected-revision <sha256:...>] [--json]
|
|
231
241
|
wordcell relation list <note> [--root <directory>] [--json]
|
|
232
|
-
wordcell percolate [note] [--root <directory>] [--min-support <count>] [--limit <count>] [--json]
|
|
242
|
+
wordcell percolate [note] [--proofs] [--root <directory>] [--min-support <count>] [--limit <count>] [--json]
|
|
233
243
|
wordcell list [--root <directory>] [--where <path=value>] [--has <path>] [--tag <tag>] [--scope <repository-path>] [--sort <field>] [--order <asc|desc>] [--limit <count>] [--json]
|
|
234
244
|
wordcell index [--root <directory>] [--database <path>] [--force] [--json]
|
|
235
245
|
wordcell search <query> [--root <directory>] [--repo <repository>] [--database <path>] [--mode <hybrid|exact|keyword|semantic>] [--rules <file>] [--priority] [--where <path=value>] [--has <path>] [--tag <tag>] [--scope <repository-path>] [--related <note>] [--graph-depth <1|2>] [--no-graph] [--history | --no-history | --require-history] [--limit <count>] [--candidate-limit <count>] [--min-score <score>] [--json]
|
|
@@ -252,6 +262,7 @@ Run \`wordcell clip --help\` for web capture options or \`wordcell pdf --help\`
|
|
|
252
262
|
type VaultCommand = "refresh" | "check" | "graph" | "backlinks" | "links";
|
|
253
263
|
|
|
254
264
|
type ParsedCommand =
|
|
265
|
+
| GraphCliCommand
|
|
255
266
|
| { readonly kind: "help" }
|
|
256
267
|
| { readonly kind: "clip"; readonly arguments: readonly string[] }
|
|
257
268
|
| {
|
|
@@ -422,6 +433,7 @@ type ParsedCommand =
|
|
|
422
433
|
}
|
|
423
434
|
| {
|
|
424
435
|
readonly kind: "percolate";
|
|
436
|
+
readonly proofs?: true;
|
|
425
437
|
readonly root: string;
|
|
426
438
|
readonly note?: string;
|
|
427
439
|
readonly minSupport: number;
|
|
@@ -433,7 +445,7 @@ type ParseResult =
|
|
|
433
445
|
| { readonly ok: true; readonly value: ParsedCommand }
|
|
434
446
|
| { readonly ok: false; readonly message: string };
|
|
435
447
|
|
|
436
|
-
type CliDependencies = {
|
|
448
|
+
type CliDependencies = GraphCliDependencies & {
|
|
437
449
|
readonly runClipCommand?: typeof runClipCommand;
|
|
438
450
|
readonly readCaptureBundle?: typeof readCaptureBundle;
|
|
439
451
|
readonly verifyCaptureBundle?: typeof verifyCaptureBundle;
|
|
@@ -455,6 +467,7 @@ type CliDependencies = {
|
|
|
455
467
|
readonly addNoteRelation?: typeof addNoteRelation;
|
|
456
468
|
readonly removeNoteRelation?: typeof removeNoteRelation;
|
|
457
469
|
readonly percolateVault?: typeof percolateVault;
|
|
470
|
+
readonly percolateWithGraph?: typeof percolateWithGraph;
|
|
458
471
|
readonly inspectAgentContextRepository?: typeof inspectAgentContextRepository;
|
|
459
472
|
readonly buildRepositoryMemoryContext?: typeof buildRepositoryMemoryContext;
|
|
460
473
|
readonly auditAgentGuideRepository?: typeof auditAgentGuideRepository;
|
|
@@ -1678,10 +1691,16 @@ function parsePercolateCommand(arguments_: readonly string[]): ParseResult {
|
|
|
1678
1691
|
let minSupport = 2;
|
|
1679
1692
|
let limit = 25;
|
|
1680
1693
|
let json = false;
|
|
1694
|
+
let proofs = false;
|
|
1681
1695
|
const positional: string[] = [];
|
|
1682
1696
|
for (let cursor = 0; cursor < arguments_.length; cursor += 1) {
|
|
1683
1697
|
const argument = arguments_[cursor];
|
|
1684
1698
|
if (argument === undefined) continue;
|
|
1699
|
+
if (argument === "--proofs") {
|
|
1700
|
+
if (proofs) return { ok: false, message: "duplicate percolation proofs option" };
|
|
1701
|
+
proofs = true;
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1685
1704
|
if (argument === "--json") {
|
|
1686
1705
|
json = true;
|
|
1687
1706
|
continue;
|
|
@@ -1714,10 +1733,12 @@ function parsePercolateCommand(arguments_: readonly string[]): ParseResult {
|
|
|
1714
1733
|
if (positional.length > 1) {
|
|
1715
1734
|
return { ok: false, message: "percolate accepts at most one note ID" };
|
|
1716
1735
|
}
|
|
1736
|
+
if (proofs && note === undefined) return { ok: false, message: "percolate --proofs requires one note" };
|
|
1717
1737
|
return {
|
|
1718
1738
|
ok: true,
|
|
1719
1739
|
value: {
|
|
1720
1740
|
kind: "percolate",
|
|
1741
|
+
...(proofs ? { proofs: true as const } : {}),
|
|
1721
1742
|
root,
|
|
1722
1743
|
...(note === undefined ? {} : { note }),
|
|
1723
1744
|
minSupport,
|
|
@@ -1888,6 +1909,9 @@ export function parseArguments(arguments_: readonly string[]): ParseResult {
|
|
|
1888
1909
|
if (positional[0] !== undefined) directory = positional[0];
|
|
1889
1910
|
return { ok: true, value: { kind: "init", directory, json } };
|
|
1890
1911
|
}
|
|
1912
|
+
if (command === "graph" && ["rebuild", "verify", "query"].includes(arguments_[1] ?? "")) {
|
|
1913
|
+
return parseGraphCommand(arguments_.slice(1));
|
|
1914
|
+
}
|
|
1891
1915
|
if (command === "refresh" || command === "check" || command === "graph" || command === "backlinks" || command === "links") {
|
|
1892
1916
|
return parseVaultCommand(command, arguments_.slice(1));
|
|
1893
1917
|
}
|
|
@@ -2745,6 +2769,20 @@ async function runPercolate(
|
|
|
2745
2769
|
...(command.note === undefined ? {} : { mentionScope: command.note }),
|
|
2746
2770
|
},
|
|
2747
2771
|
);
|
|
2772
|
+
if (command.proofs === true) {
|
|
2773
|
+
if (command.note === undefined) throw new Error("percolate --proofs requires one note");
|
|
2774
|
+
const result = await (dependencies.percolateWithGraph ?? percolateWithGraph)(snapshot, {
|
|
2775
|
+
note: command.note, minSupport: command.minSupport, limit: command.limit,
|
|
2776
|
+
});
|
|
2777
|
+
const support = [result.positiveSupport.sharedTags, result.positiveSupport.sharedConcepts];
|
|
2778
|
+
const partial = support.some((item) => item.truncated || item.proofsTruncated);
|
|
2779
|
+
const text = `${renderPercolation(result.suggestions, command.note)}\nSnapshot: ${result.revision}\n`
|
|
2780
|
+
+ "Positive shared-tag and shared-concept proofs are included in --json output.\n"
|
|
2781
|
+
+ "Absence checks, support counts, and predicate choices remain Wordcell authoring decisions.\n"
|
|
2782
|
+
+ (partial ? "Partial proof evidence: a graph query reached its bounds.\n" : "");
|
|
2783
|
+
output.stdout(command.json ? terminalSafeJson(result) : sanitizeTerminalText(redactSensitiveText(text)));
|
|
2784
|
+
return partial ? 4 : 0;
|
|
2785
|
+
}
|
|
2748
2786
|
const result = (dependencies.percolateVault ?? percolateVault)(
|
|
2749
2787
|
snapshot.notes,
|
|
2750
2788
|
snapshot.analysis,
|
|
@@ -3527,6 +3565,11 @@ export async function main(
|
|
|
3527
3565
|
if (command.kind === "agents") return await runAgents(command, output, dependencies);
|
|
3528
3566
|
if (command.kind === "note-create") return await runNoteCreate(command, output, dependencies);
|
|
3529
3567
|
if (command.kind === "relation") return await runRelation(command, output, dependencies);
|
|
3568
|
+
if (command.kind === "graph-rebuild" || command.kind === "graph-verify" || command.kind === "graph-query") {
|
|
3569
|
+
const result = await executeGraphCommand(command, dependencies);
|
|
3570
|
+
output.stdout(command.json ? terminalSafeJson(result.value) : sanitizeTerminalText(redactSensitiveText(result.text)));
|
|
3571
|
+
return result.exitCode;
|
|
3572
|
+
}
|
|
3530
3573
|
if (command.kind === "percolate") return await runPercolate(command, output, dependencies);
|
|
3531
3574
|
if (command.kind === "list") return await runList(command, output, dependencies);
|
|
3532
3575
|
if (command.kind === "inbox") return await runInbox(command, output, dependencies);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Wordcell-owned graph protocol. Vendor record and program types stay in the adapter. */
|
|
2
|
+
export type GraphAtom = string | number | boolean | null;
|
|
3
|
+
export type GraphFactRelation = "wordcell.note" | "wordcell.link" | "wordcell.relation"
|
|
4
|
+
| "wordcell.external-relation" | "wordcell.tag" | "wordcell.scope" | "wordcell.concept";
|
|
5
|
+
|
|
6
|
+
export type GraphFact = Readonly<{ relation: GraphFactRelation; tuple: readonly GraphAtom[] }>;
|
|
7
|
+
export type GraphSourceRecord = Readonly<{
|
|
8
|
+
key: string;
|
|
9
|
+
id: string;
|
|
10
|
+
path: string;
|
|
11
|
+
documentId: string | null;
|
|
12
|
+
contentSha256: string;
|
|
13
|
+
facts: readonly GraphFact[];
|
|
14
|
+
}>;
|
|
15
|
+
export type GraphSnapshot = Readonly<{
|
|
16
|
+
schemaVersion: 1;
|
|
17
|
+
vaultIdentity: string;
|
|
18
|
+
revision: string;
|
|
19
|
+
catalogNoteId: string;
|
|
20
|
+
sourceDigests: readonly Readonly<{ path: string; contentSha256: string }>[];
|
|
21
|
+
records: readonly GraphSourceRecord[];
|
|
22
|
+
factCount: number;
|
|
23
|
+
}>;
|
|
24
|
+
|
|
25
|
+
export const GRAPH_LIMITS = Object.freeze({
|
|
26
|
+
notes: 4_000,
|
|
27
|
+
facts: 100_000,
|
|
28
|
+
sourceBytes: 64 * 1024 * 1024,
|
|
29
|
+
atomBytes: 16 * 1024,
|
|
30
|
+
depth: 8,
|
|
31
|
+
rows: 1_000,
|
|
32
|
+
workUnits: 8_000_000,
|
|
33
|
+
derivedTuples: 100_000,
|
|
34
|
+
rounds: 64,
|
|
35
|
+
proofDepth: 64,
|
|
36
|
+
proofNodes: 4_096,
|
|
37
|
+
totalProofNodes: 32_768,
|
|
38
|
+
resultBytes: 8 * 1024 * 1024,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export type GraphQueryLimits = Readonly<{
|
|
42
|
+
rows?: number;
|
|
43
|
+
workUnits?: number;
|
|
44
|
+
derivedTuples?: number;
|
|
45
|
+
rounds?: number;
|
|
46
|
+
proofDepth?: number;
|
|
47
|
+
proofNodes?: number;
|
|
48
|
+
totalProofNodes?: number;
|
|
49
|
+
resultBytes?: number;
|
|
50
|
+
}>;
|
|
51
|
+
|
|
52
|
+
export type GraphProgramRequest =
|
|
53
|
+
| Readonly<{ program: "backlinks"; note: string }>
|
|
54
|
+
| Readonly<{ program: "reachability"; note: string; depth?: number }>
|
|
55
|
+
| Readonly<{ program: "scope-route"; scope: string }>
|
|
56
|
+
| Readonly<{ program: "relation-closure"; note: string; predicate: string; depth?: number }>
|
|
57
|
+
| Readonly<{ program: "shared-tags"; note: string }>
|
|
58
|
+
| Readonly<{ program: "shared-concepts"; note: string }>;
|
|
59
|
+
export type GraphQueryRequest = GraphProgramRequest & Readonly<{ limits?: GraphQueryLimits }>;
|
|
60
|
+
|
|
61
|
+
export type GraphProofSource = Readonly<{
|
|
62
|
+
key: string;
|
|
63
|
+
recordSha256: string;
|
|
64
|
+
noteId: string;
|
|
65
|
+
path: string;
|
|
66
|
+
contentSha256: string;
|
|
67
|
+
}>;
|
|
68
|
+
export type GraphProof =
|
|
69
|
+
| Readonly<{ kind: "fact"; relation: string; tuple: readonly GraphAtom[]; sources: readonly GraphProofSource[] }>
|
|
70
|
+
| Readonly<{ kind: "derived"; relation: string; tuple: readonly GraphAtom[]; ruleId: string;
|
|
71
|
+
ruleSha256: string; premises: readonly GraphProof[]; premisesTruncated: boolean }>
|
|
72
|
+
| Readonly<{ kind: "truncated"; relation: string; tuple: readonly GraphAtom[]; reason: "cycle" | "depth" | "nodes" }>;
|
|
73
|
+
|
|
74
|
+
export type GraphQueryResult = Readonly<{
|
|
75
|
+
schemaVersion: 1;
|
|
76
|
+
authority: "derived";
|
|
77
|
+
vaultIdentity: string;
|
|
78
|
+
revision: string;
|
|
79
|
+
projectionSha256: string;
|
|
80
|
+
request: GraphQueryRequest;
|
|
81
|
+
columns: readonly string[];
|
|
82
|
+
rows: readonly Readonly<{ values: readonly GraphAtom[]; proofs: readonly GraphProof[];
|
|
83
|
+
proofsTruncated: boolean; supportCount: number }>[];
|
|
84
|
+
limits: Required<GraphQueryLimits>;
|
|
85
|
+
truncated: boolean;
|
|
86
|
+
proofsTruncated: boolean;
|
|
87
|
+
truncationReasons: readonly string[];
|
|
88
|
+
stats: Readonly<{ baseFacts: number; derivedFacts: number; workUnits: number;
|
|
89
|
+
rounds: number; proofNodes: number; queryMatches: number }>;
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
export type GraphVerification = Readonly<{
|
|
93
|
+
schemaVersion: 1;
|
|
94
|
+
status: "verified";
|
|
95
|
+
vaultIdentity: string;
|
|
96
|
+
revision: string;
|
|
97
|
+
records: number;
|
|
98
|
+
facts: number;
|
|
99
|
+
}>;
|
|
100
|
+
|
|
101
|
+
export interface GraphAuthority {
|
|
102
|
+
query(request: GraphQueryRequest): Promise<GraphQueryResult>;
|
|
103
|
+
/** Re-evaluate a bounded result against this authority; foreign or stale evidence is rejected. */
|
|
104
|
+
verifyResult(value: unknown): Promise<boolean>;
|
|
105
|
+
verify(): Promise<GraphVerification>;
|
|
106
|
+
close(): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class GraphAuthorityError extends Error {
|
|
110
|
+
readonly code: "invalid-input" | "budget" | "stale" | "unsafe-cache" | "corrupt-cache" | "closed";
|
|
111
|
+
constructor(code: GraphAuthorityError["code"], message: string) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.name = "GraphAuthorityError";
|
|
114
|
+
this.code = code;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, realpath, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { createGraphSnapshot } from "./graph-facts.js";
|
|
7
|
+
import { graphCacheFileSystem as fs } from "./graph-cache-filesystem.js";
|
|
8
|
+
import { GraphAuthorityError, GRAPH_LIMITS } from "./graph-authority-model.js";
|
|
9
|
+
import type { GraphAuthority, GraphQueryRequest, GraphQueryResult, GraphVerification } from "./graph-authority-model.js";
|
|
10
|
+
import { acquireFileLease } from "./note-lock.js";
|
|
11
|
+
import type { openOhGraphAdapter as OhAdapterFactory } from "./oh/authority.js";
|
|
12
|
+
|
|
13
|
+
// Keep Node-compatible package discovery independent of the Bun-only graph runtime.
|
|
14
|
+
const openOhGraphAdapter: typeof OhAdapterFactory = async (...args) =>
|
|
15
|
+
await (await import("./oh/authority.js")).openOhGraphAdapter(...args);
|
|
16
|
+
import { validateGraphQueryRequest } from "./graph-query.js";
|
|
17
|
+
import { scanVault } from "./vault.js";
|
|
18
|
+
import type { VaultSnapshot } from "./vault.js";
|
|
19
|
+
|
|
20
|
+
export * from "./graph-authority-model.js";
|
|
21
|
+
export { createGraphSnapshot, validateGraphQueryRequest };
|
|
22
|
+
export type GraphOptions = Readonly<{ index?: string }>;
|
|
23
|
+
export type GraphRebuildOptions = GraphOptions & Readonly<{ fresh?: boolean }>;
|
|
24
|
+
export type GraphReadOptions = GraphOptions & Readonly<{ persisted?: boolean }>;
|
|
25
|
+
|
|
26
|
+
const MAX_CACHE_BYTES = 64 * 1024 * 1024;
|
|
27
|
+
const ignoredCache = "# Disposable Wordcell graph state. Rebuild from Markdown.\n*\n";
|
|
28
|
+
const hash = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
|
|
29
|
+
function errno(value: unknown, code: string): boolean {
|
|
30
|
+
return value !== null && typeof value === "object" && "code" in value && value.code === code;
|
|
31
|
+
}
|
|
32
|
+
async function rootPath(root: string): Promise<string> {
|
|
33
|
+
if (typeof root !== "string" || root.trim() === "" || root.includes("\0")) {
|
|
34
|
+
throw new GraphAuthorityError("invalid-input", "A graph operation requires a vault root.");
|
|
35
|
+
}
|
|
36
|
+
return await realpath(resolve(root));
|
|
37
|
+
}
|
|
38
|
+
async function scan(root: string, options: GraphOptions): Promise<VaultSnapshot> {
|
|
39
|
+
return await scanVault(root, { ...options, mentionScope: false,
|
|
40
|
+
maxNotes: GRAPH_LIMITS.notes, maxTotalBytes: GRAPH_LIMITS.sourceBytes,
|
|
41
|
+
maxConnectionObservations: GRAPH_LIMITS.facts });
|
|
42
|
+
}
|
|
43
|
+
async function directoryIdentity(path: string) {
|
|
44
|
+
const info = await fs.lstat(path, { bigint: true });
|
|
45
|
+
if (!info.isDirectory() || info.isSymbolicLink() || await realpath(path) !== path) {
|
|
46
|
+
throw new GraphAuthorityError("unsafe-cache", "Wordcell graph state requires a real directory inside the vault.");
|
|
47
|
+
}
|
|
48
|
+
return { dev: info.dev, ino: info.ino };
|
|
49
|
+
}
|
|
50
|
+
async function sameDirectory(path: string, expected: Awaited<ReturnType<typeof directoryIdentity>>): Promise<void> {
|
|
51
|
+
const actual = await directoryIdentity(path);
|
|
52
|
+
if (actual.dev !== expected.dev || actual.ino !== expected.ino) {
|
|
53
|
+
throw new GraphAuthorityError("unsafe-cache", "The graph cache directory changed during the operation.");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Read one bounded, private regular file through its observed identity. Never follow a cache symlink. */
|
|
57
|
+
async function readCache(path: string, maximumBytes = MAX_CACHE_BYTES): Promise<Buffer | null> {
|
|
58
|
+
let observed;
|
|
59
|
+
try { observed = await fs.lstat(path, { bigint: true }); }
|
|
60
|
+
catch (error) { if (errno(error, "ENOENT")) return null; throw error; }
|
|
61
|
+
if (!observed.isFile() || observed.isSymbolicLink() || observed.nlink !== 1n) {
|
|
62
|
+
throw new GraphAuthorityError("unsafe-cache", "Graph cache files must be regular files with one link.");
|
|
63
|
+
}
|
|
64
|
+
if (observed.size > BigInt(maximumBytes)) throw new GraphAuthorityError("budget", "Graph cache exceeds its byte limit.");
|
|
65
|
+
const handle = await fs.open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
66
|
+
try {
|
|
67
|
+
const before = await handle.stat({ bigint: true });
|
|
68
|
+
if (!before.isFile() || before.nlink !== 1n || before.dev !== observed.dev || before.ino !== observed.ino) {
|
|
69
|
+
throw new GraphAuthorityError("unsafe-cache", "The graph cache changed before it could be read.");
|
|
70
|
+
}
|
|
71
|
+
if (before.size > BigInt(maximumBytes)) {
|
|
72
|
+
throw new GraphAuthorityError("budget", "Graph cache exceeds its byte limit.");
|
|
73
|
+
}
|
|
74
|
+
if (before.size !== observed.size || before.mtimeNs !== observed.mtimeNs || before.ctimeNs !== observed.ctimeNs) {
|
|
75
|
+
throw new GraphAuthorityError("stale", "The graph cache changed before it could be read.");
|
|
76
|
+
}
|
|
77
|
+
const bytes = Buffer.alloc(Number(before.size));
|
|
78
|
+
let offset = 0;
|
|
79
|
+
while (offset < bytes.length) {
|
|
80
|
+
const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
|
|
81
|
+
if (bytesRead === 0) throw new GraphAuthorityError("stale", "The graph cache changed during its read.");
|
|
82
|
+
offset += bytesRead;
|
|
83
|
+
}
|
|
84
|
+
const after = await handle.stat({ bigint: true });
|
|
85
|
+
const current = await fs.lstat(path, { bigint: true });
|
|
86
|
+
if (before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs
|
|
87
|
+
|| current.dev !== before.dev || current.ino !== before.ino || current.nlink !== 1n) {
|
|
88
|
+
throw new GraphAuthorityError("stale", "The graph cache changed during its read.");
|
|
89
|
+
}
|
|
90
|
+
return bytes;
|
|
91
|
+
} finally { await handle.close(); }
|
|
92
|
+
}
|
|
93
|
+
async function requireSingleFileCache(path: string): Promise<void> {
|
|
94
|
+
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
95
|
+
try { await fs.lstat(path + suffix); }
|
|
96
|
+
catch (error) { if (errno(error, "ENOENT")) continue; throw error; }
|
|
97
|
+
throw new GraphAuthorityError("unsafe-cache", "Graph cache has an active or foreign SQLite sidecar; close its writer before rebuilding.");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function assertFresh(root: string, options: GraphOptions, revision: string): Promise<void> {
|
|
101
|
+
if (createGraphSnapshot(await scan(root, options)).revision !== revision) {
|
|
102
|
+
throw new GraphAuthorityError("stale", "Markdown changed during the graph operation; retry from a fresh snapshot.");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A read-only, in-memory authority sharing the caller's exact Markdown snapshot. */
|
|
107
|
+
export async function openGraphAuthority(snapshot: VaultSnapshot): Promise<GraphAuthority> {
|
|
108
|
+
return await openOhGraphAdapter(createGraphSnapshot(snapshot));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function withGraph<T>(rootInput: string, options: GraphReadOptions,
|
|
112
|
+
work: (authority: GraphAuthority) => Promise<T>): Promise<T> {
|
|
113
|
+
const root = await rootPath(rootInput);
|
|
114
|
+
const snapshot = createGraphSnapshot(await scan(root, options));
|
|
115
|
+
let authority: GraphAuthority;
|
|
116
|
+
if (options.persisted) {
|
|
117
|
+
const cache = join(root, ".wordcell");
|
|
118
|
+
let identity;
|
|
119
|
+
try { identity = await directoryIdentity(cache); }
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (errno(error, "ENOENT")) throw new GraphAuthorityError("stale", "No graph cache exists; run wordcell graph rebuild first.");
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
const path = join(cache, "oh.sqlite");
|
|
125
|
+
await requireSingleFileCache(path);
|
|
126
|
+
const bytes = await readCache(path);
|
|
127
|
+
if (bytes === null) throw new GraphAuthorityError("stale", "No graph cache exists; run wordcell graph rebuild first.");
|
|
128
|
+
await sameDirectory(cache, identity);
|
|
129
|
+
authority = await openOhGraphAdapter(snapshot, { databaseBytes: bytes });
|
|
130
|
+
} else authority = await openOhGraphAdapter(snapshot);
|
|
131
|
+
try {
|
|
132
|
+
const result = await work(authority);
|
|
133
|
+
await assertFresh(root, options, snapshot.revision);
|
|
134
|
+
return result;
|
|
135
|
+
} finally { await authority.close(); }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Verify persisted state without opening any vault file for writing. */
|
|
139
|
+
export async function verifyGraph(root: string, options: GraphOptions = {}): Promise<GraphVerification> {
|
|
140
|
+
return await withGraph(root, { ...options, persisted: true }, async (authority) => await authority.verify());
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function queryGraph(root: string, request: GraphQueryRequest, options: GraphReadOptions = {}): Promise<GraphQueryResult> {
|
|
144
|
+
const checked = validateGraphQueryRequest(request);
|
|
145
|
+
return await withGraph(root, options, async (authority) => await authority.query(checked));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Reconcile in a private staging database and install only after the source and old cache still match. */
|
|
149
|
+
export async function rebuildGraph(rootInput: string, options: GraphRebuildOptions = {}): Promise<GraphVerification> {
|
|
150
|
+
const root = await rootPath(rootInput);
|
|
151
|
+
const rootIdentity = await directoryIdentity(root);
|
|
152
|
+
const snapshot = createGraphSnapshot(await scan(root, options));
|
|
153
|
+
const cache = join(root, ".wordcell");
|
|
154
|
+
try { await mkdir(cache, { mode: 0o700 }); }
|
|
155
|
+
catch (error) { if (!errno(error, "EEXIST")) throw error; }
|
|
156
|
+
const cacheIdentity = await directoryIdentity(cache);
|
|
157
|
+
const lease = await acquireFileLease(join(cache, ".rebuild.lock"));
|
|
158
|
+
let staging: string | undefined;
|
|
159
|
+
let stagingIdentity: Awaited<ReturnType<typeof directoryIdentity>> | undefined;
|
|
160
|
+
try {
|
|
161
|
+
await sameDirectory(root, rootIdentity);
|
|
162
|
+
await sameDirectory(cache, cacheIdentity);
|
|
163
|
+
const ignore = join(cache, ".gitignore");
|
|
164
|
+
const oldIgnore = await readCache(ignore, 4096);
|
|
165
|
+
if (oldIgnore !== null && oldIgnore.toString("utf8") !== ignoredCache) {
|
|
166
|
+
throw new GraphAuthorityError("unsafe-cache", "The graph cache has an unrecognized .gitignore; preserve it and use a separate vault root.");
|
|
167
|
+
}
|
|
168
|
+
if (oldIgnore === null) {
|
|
169
|
+
const handle = await fs.open(ignore, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
170
|
+
try { await handle.writeFile(ignoredCache); await handle.sync(); } finally { await handle.close(); }
|
|
171
|
+
}
|
|
172
|
+
const path = join(cache, "oh.sqlite");
|
|
173
|
+
await requireSingleFileCache(path);
|
|
174
|
+
const previous = await readCache(path);
|
|
175
|
+
staging = await mkdtemp(join(cache, ".build-"));
|
|
176
|
+
stagingIdentity = await directoryIdentity(staging);
|
|
177
|
+
const stagedPath = join(staging, "oh.sqlite");
|
|
178
|
+
if (previous !== null && !options.fresh) {
|
|
179
|
+
const handle = await fs.open(stagedPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
180
|
+
try { await handle.writeFile(previous); } finally { await handle.close(); }
|
|
181
|
+
}
|
|
182
|
+
const authority = await openOhGraphAdapter(snapshot, { databasePath: stagedPath, writable: true });
|
|
183
|
+
let result;
|
|
184
|
+
try { result = await authority.verify(); } finally { await authority.close(); }
|
|
185
|
+
const ready = await readCache(stagedPath);
|
|
186
|
+
if (ready === null) throw new GraphAuthorityError("corrupt-cache", "The graph rebuild did not produce a database.");
|
|
187
|
+
await requireSingleFileCache(stagedPath);
|
|
188
|
+
await assertFresh(root, options, snapshot.revision);
|
|
189
|
+
await lease.assertOwned();
|
|
190
|
+
await sameDirectory(root, rootIdentity);
|
|
191
|
+
await sameDirectory(cache, cacheIdentity);
|
|
192
|
+
await requireSingleFileCache(path);
|
|
193
|
+
const current = await readCache(path);
|
|
194
|
+
if ((previous === null) !== (current === null) || (previous !== null && current !== null && hash(previous) !== hash(current))) {
|
|
195
|
+
throw new GraphAuthorityError("stale", "The graph cache changed during rebuild; retry from its current state.");
|
|
196
|
+
}
|
|
197
|
+
const handle = await fs.open(stagedPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
198
|
+
try { await handle.sync(); } finally { await handle.close(); }
|
|
199
|
+
await rename(stagedPath, path);
|
|
200
|
+
const directory = await fs.open(cache, constants.O_RDONLY);
|
|
201
|
+
try { await directory.sync(); } finally { await directory.close(); }
|
|
202
|
+
return result;
|
|
203
|
+
} finally {
|
|
204
|
+
try {
|
|
205
|
+
if (staging !== undefined && stagingIdentity !== undefined) {
|
|
206
|
+
// Never recursively remove a replacement directory after losing the original parent.
|
|
207
|
+
let stillOwned = false;
|
|
208
|
+
try {
|
|
209
|
+
await sameDirectory(root, rootIdentity);
|
|
210
|
+
await sameDirectory(cache, cacheIdentity);
|
|
211
|
+
await sameDirectory(staging, stagingIdentity);
|
|
212
|
+
stillOwned = true;
|
|
213
|
+
} catch { /* Preserve uncertain paths for the owner to inspect. */ }
|
|
214
|
+
if (stillOwned) await rm(staging, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
finally { await lease.release(); }
|
|
218
|
+
}
|
|
219
|
+
}
|