@hraness/wordcell 0.21.3 → 0.22.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.
Files changed (81) hide show
  1. package/README.md +170 -64
  2. package/dist/{authority-4sp9d552.js → authority-1pgftmwq.js} +55 -10
  3. package/dist/benchmark.js +3 -3
  4. package/dist/capture.js +6 -5
  5. package/dist/cli.js +37 -31
  6. package/dist/clip/acquire.js +3 -2
  7. package/dist/clip/cli.js +6 -5
  8. package/dist/clip/doctor.js +4 -3
  9. package/dist/clip/jobs.js +3 -3
  10. package/dist/clip/refresh.js +1 -1
  11. package/dist/evaluation-builder.js +18 -17
  12. package/dist/evaluation-kb.js +14 -13
  13. package/dist/graph-authority.js +3 -3
  14. package/dist/graph-percolation.js +5 -5
  15. package/dist/index-0dqfnrz9.js +1985 -0
  16. package/dist/{index-0k2x4nn9.js → index-1tm7bgx7.js} +16 -4
  17. package/dist/{index-vdw5yjfa.js → index-2hf27mws.js} +108 -18
  18. package/dist/index-3agn8scn.js +306 -0
  19. package/dist/{index-0rfcqab6.js → index-4ag64rc1.js} +555 -59
  20. package/dist/index-4knsp9qj.js +34 -0
  21. package/dist/index-66pshdtx.js +341 -0
  22. package/dist/index-9rf81m0p.js +368 -0
  23. package/dist/{index-59ssmqqm.js → index-a6kbv718.js} +1 -1
  24. package/dist/{index-51bap0mq.js → index-avbce90h.js} +9 -9
  25. package/dist/{index-9zcpdbjg.js → index-de2w8crk.js} +13 -13
  26. package/dist/{index-pbr6rv47.js → index-e6p9e926.js} +6 -6
  27. package/dist/{index-bcfn9xah.js → index-famy7fhs.js} +5 -5
  28. package/dist/{index-van48n3g.js → index-fc4dr114.js} +4 -2
  29. package/dist/{index-jzcws0zn.js → index-g5vsqmdy.js} +27 -57
  30. package/dist/{index-115b07ap.js → index-hcw140eb.js} +5 -5
  31. package/dist/index-j70m75wd.js +217 -0
  32. package/dist/{index-3ehtt59c.js → index-qwgsmtsz.js} +1 -1
  33. package/dist/{index-bgfzwt4h.js → index-t2bs9xdr.js} +4 -4
  34. package/dist/index.js +285 -93
  35. package/dist/portfolio.js +14 -13
  36. package/dist/publish-model.js +36 -0
  37. package/dist/publish-reader/reader.css +1042 -0
  38. package/dist/publish-reader/reader.js +1534 -0
  39. package/dist/publish-reader/theme.js +92 -0
  40. package/dist/publish-search.js +52 -0
  41. package/dist/publish.js +36 -0
  42. package/dist/rerank-typesafe.js +13 -0
  43. package/dist/rerank.js +14 -0
  44. package/dist/sdk.js +14 -11
  45. package/dist/search.js +2 -2
  46. package/dist/semantic.js +4 -4
  47. package/dist/{support-vm4mkw0k.js → support-mywv1em0.js} +38 -32
  48. package/dist/workflows/decision-context.js +13 -12
  49. package/dist/workflows/index.js +13 -12
  50. package/package.json +63 -5
  51. package/skills/wordcell/AGENTS.md +1 -0
  52. package/skills/wordcell/SKILL.md +2 -1
  53. package/skills/wordcell/agents/openai.yaml +1 -1
  54. package/skills/wordcell/references/publish.md +93 -0
  55. package/skills/wordcell/references/query.md +22 -0
  56. package/src/cli-program.ts +347 -9
  57. package/src/index.ts +4 -0
  58. package/src/oh/authority.ts +14 -5
  59. package/src/oh/canonical-rust.ts +127 -0
  60. package/src/oh/projection-rust.ts +63 -0
  61. package/src/oh-adoption.ts +7 -1
  62. package/src/publish-graph.ts +0 -0
  63. package/src/publish-index.ts +195 -0
  64. package/src/publish-markdown.ts +617 -0
  65. package/src/publish-model.ts +599 -0
  66. package/src/publish-nav.ts +204 -0
  67. package/src/publish-pages.ts +382 -0
  68. package/src/publish-reader/reader.css +1042 -0
  69. package/src/publish-reader/reader.ts +1017 -0
  70. package/src/publish-reader/theme.ts +97 -0
  71. package/src/publish-search.ts +418 -0
  72. package/src/publish-select.ts +419 -0
  73. package/src/publish-theme.ts +106 -0
  74. package/src/publish.ts +904 -0
  75. package/src/rerank-credentials.ts +56 -0
  76. package/src/rerank-typesafe.ts +472 -0
  77. package/src/rerank.ts +389 -0
  78. package/src/sdk.ts +157 -6
  79. package/src/serve.ts +180 -0
  80. package/src/vault.ts +23 -9
  81. package/dist/{index-why54q5p.js → index-gm9t95d9.js} +3 -3
@@ -0,0 +1,93 @@
1
+ # Publish selected knowledge as a static site
2
+
3
+ Use `wordcell publish` to turn selected Markdown notes into read-only pages,
4
+ backlinks, a graph, and browser-local search. It builds files locally with no
5
+ model call. Publishing here means generating the artifact; uploading it to a
6
+ host is a separate action.
7
+
8
+ ## Select and preview
9
+
10
+ Use the scope in the user's request. Ask only when the selection or destination
11
+ is materially ambiguous. Resolve `KB_ROOT` before running commands, and choose
12
+ an output directory outside the vault that does not contain it.
13
+
14
+ Start with a compact, read-only preview:
15
+
16
+ ```sh
17
+ wordcell publish --root "$KB_ROOT" --out ./public-notes \
18
+ --include notes/decision --include plans/implementation \
19
+ --dry-run --list-limit 20 --json
20
+ ```
21
+
22
+ The report contains counts, at most 20 selected note IDs, and a digest of the
23
+ selected Markdown. It does not print note bodies. Inspect `selection.truncated` before
24
+ treating the preview as a complete list. Raise `--list-limit` up to 1000 when
25
+ individual IDs matter, or use zero for counts and a digest alone. Open only
26
+ selected notes whose contents need review; do not load or summarize the whole
27
+ vault to choose a known slice.
28
+
29
+ Selectors are repeatable:
30
+
31
+ - `--include <id-or-directory>` selects exact notes or directory prefixes.
32
+ - `--include-glob 'notes/**'` and `--exclude-glob '**/draft-*'` select by pattern.
33
+ Quote globs so the shell does not expand them. `*` matches within one segment,
34
+ `?` matches one character, and `**` matches whole path segments.
35
+ - `--where type=concept`, `--has relations`, `--tag public`, and
36
+ `--scope packages/parser` select by authored metadata.
37
+ - `--from notes/topic --depth 2 --direction both` selects a bounded neighborhood
38
+ of explicit links and relationships. Oversized neighborhoods fail rather
39
+ than silently publish an incomplete selection.
40
+
41
+ Positive path, glob, metadata, and graph selectors form a **union**. Metadata
42
+ predicates within their group must all match. `--include notes --tag public`
43
+ therefore includes all of `notes`, not only its public-tagged notes. Use a
44
+ metadata conjunction or explicit note IDs when an intersection is required.
45
+ Excludes and `publish: false` always win.
46
+
47
+ Publication is not prose redaction. Review selected text and referenced assets
48
+ for anything unsuitable for the intended audience. A selected note can mention
49
+ excluded material in its own text. Private flags exclude a note's page, search
50
+ record, and graph membership; they cannot remove information copied into another
51
+ note or attachment. `--noindex` is a crawler preference, not access control.
52
+
53
+ ## Build the reviewed slice
54
+
55
+ Repeat the preview command without `--dry-run`. Keep the same selectors and
56
+ check the returned digest and counts; if the Markdown changed, review it again.
57
+ The digest covers selected Markdown, not attachment bytes. Review referenced
58
+ attachments separately before sharing.
59
+
60
+ ```sh
61
+ wordcell publish --root "$KB_ROOT" --out ./public-notes \
62
+ --include notes/decision --include plans/implementation \
63
+ --title "Project decisions" --deterministic --json
64
+ ```
65
+
66
+ Use `--force` only to replace a known generated output directory. Set
67
+ `--base-path /handbook/` if the host serves the files at a subpath, and add
68
+ `--base-url https://docs.example.com` when a sitemap is useful. Use
69
+ `--no-index-content` for field-only search. None of these flags changes the
70
+ source vault.
71
+
72
+ Read dropped-link, skipped-asset, and search-truncation counts. Run
73
+ `wordcell check --root "$KB_ROOT"` when missing links or attachments need
74
+ diagnosis. Do not report missing content as complete.
75
+
76
+ ## Preview and host
77
+
78
+ ```sh
79
+ wordcell serve --root ./public-notes --port 8080
80
+ ```
81
+
82
+ Open the loopback URL. Check a note, its navigation, the Search overlay, and the
83
+ Graph page. HTML can be read without JavaScript; browser search and graph need
84
+ HTTP hosting rather than `file://`. The preview server is not a production
85
+ service.
86
+
87
+ Upload the generated directory to the user's selected static host only when
88
+ hosting is in the request. Confirm the exact destination and preserve unrelated
89
+ files. Verify the public URL and selected content after upload. A successful
90
+ local build alone is not a live deployment.
91
+
92
+ The full artifact contract and hosting options are in
93
+ [the publishing guide](https://github.com/hraness/wordcell/blob/main/docs/publish.md).
@@ -155,6 +155,28 @@ form. Graph neighbors and Git history remain separate from primary
155
155
  text rank. They explain and expand candidates without becoming authored facts,
156
156
  links, or recency boosts.
157
157
 
158
+ Reranking is opt-in through `--rerank typesafe`. It uses `jev-1.13.0` and sends
159
+ the query plus each candidate's identifier, title, vault-relative path, and at
160
+ most 512 UTF-8 bytes of snippet text to TypeSafe. Enable it only for vaults
161
+ approved for external processing and provider input-token charges. Use
162
+ `--rerank-limit 25` to bound the window (2–25 candidates); four requests run
163
+ concurrently under one eight-second deadline with no retries. Exact identities
164
+ remain first. Results retain their original scores and retrieval evidence.
165
+
166
+ The CLI reads `TYPESAFE_API_KEY`, an explicit `TYPESAFE_API_KEY_FILE`, or the
167
+ owner-only file `~/.config/wordcell/typesafe-api-key` (honoring
168
+ `XDG_CONFIG_HOME`). Keep credentials outside the repository. A missing key or
169
+ provider failure retains baseline ordering and marks the rerank lane
170
+ unavailable or degraded. Inspect its structured `rerank` receipt for attempted
171
+ requests, known usage, elapsed time, and whether usage is complete; an unknown
172
+ charge is never reported as zero. A successful exit alone does not establish
173
+ that reranking occurred. Omit `--rerank` for local-only retrieval.
174
+
175
+ For a repository's ordinary KB searches, prefer its approved, pinned
176
+ `kb:search` script when present. That script declares the vault and processing
177
+ choice. Read returned notes before acting; model probabilities are ranking
178
+ signals, not proof of truth. Explicit priority rules still run last.
179
+
158
180
  `wordcell history <note>` returns the bounded commit history already associated with
159
181
  one resolved note. `wordcell history search <query-or-path>` searches the bounded Git
160
182
  projection directly and retains hashes, subjects, matched paths, co-change
@@ -96,6 +96,14 @@ import {
96
96
  openKnowledgePortfolio,
97
97
  type PortfolioSearchResult,
98
98
  } from "./portfolio.js";
99
+ import {
100
+ DEFAULT_PUBLISH_LIST_LIMIT,
101
+ MAX_PUBLISH_LIST_LIMIT,
102
+ publishVault,
103
+ renderPublishReportText,
104
+ type PublishSelectionInput,
105
+ } from "./publish.js";
106
+ import { serveSite } from "./serve.js";
99
107
  import {
100
108
  MAX_AUTHORIZED_VAULTS,
101
109
  loadPortfolioRegistry,
@@ -140,11 +148,14 @@ import {
140
148
  MAX_SEARCH_RELATED_SEEDS,
141
149
  MAX_SEARCH_RESULTS,
142
150
  openKnowledgeBase,
151
+ searchEvidenceRank,
143
152
  type KnowledgeBaseGraphOptions,
144
153
  type KnowledgeBaseSearchMode,
145
154
  type KnowledgeBaseSearchOrdering,
146
155
  type KnowledgeBaseSearchResult,
147
156
  } from "./sdk.js";
157
+ import { MAX_RERANK_CANDIDATES, type SearchReranker } from "./rerank.js";
158
+ import { createCliTypeSafeReranker } from "./rerank-credentials.js";
148
159
  import {
149
160
  refreshVault,
150
161
  scanVault,
@@ -215,7 +226,16 @@ async function loadSearchRulesFile(path: string): Promise<SearchRulesV1> {
215
226
  return parseSearchRules(input);
216
227
  }
217
228
 
218
- export const usage = `wordcell — auditable capture and derived links for Markdown vaults
229
+ export const usage = `wordcell — a local knowledge base for coding agents
230
+
231
+ Start here (no account or model needed):
232
+ wordcell init kb
233
+ wordcell note create notes/decision --title "A decision" --body "Keep retries bounded." --root kb
234
+ wordcell search "retries" --root kb --mode exact
235
+
236
+ Already have Markdown? Search it with --root <your-notes-directory>.
237
+ Use --mode exact for model-free search; optional hybrid search needs a local index.
238
+ Quick start and examples: https://wordcell.io/docs
219
239
 
220
240
  Usage:
221
241
  wordcell init [directory] [--json]
@@ -243,12 +263,14 @@ Usage:
243
263
  wordcell percolate [note] [--proofs] [--root <directory>] [--min-support <count>] [--limit <count>] [--json]
244
264
  wordcell list [--root <directory>] [--where <path=value>] [--has <path>] [--tag <tag>] [--scope <repository-path>] [--sort <field>] [--order <asc|desc>] [--limit <count>] [--json]
245
265
  wordcell index [--root <directory>] [--database <path>] [--force] [--json]
246
- 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]
266
+ 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>] [--rerank <typesafe>] [--rerank-limit <2..25>] [--json]
247
267
  wordcell history <note> [--root <directory>] [--repo <repository>] [--limit <count>] [--cochanged-limit <count>] [--json]
248
268
  wordcell history search <query-or-path> [--root <directory>] [--repo <repository>] [--limit <count>] [--commit-limit <count>] [--cochanged-limit <count>] [--json]
249
269
  wordcell evaluate <manifest.json> [--root <directory>] [--repo <repository>] [--database <path>] [--retriever <id>] [--split <development|test|all>] [--limit <count>] [--cutoff <count>] [--timeout <milliseconds>] [--baseline <id>] [--model-file <path>] [--cache-state <cold|mixed|warm>] [--json]
250
270
  wordcell portfolio search <query> --registry <file> --workspace <directory> (--shared | --vault <owner/id>...) [--mode <hybrid|exact|keyword|semantic>] [--rules <file>] [--priority] [--limit <count>] [--require-all] [--json]
251
271
  wordcell portfolio audit --registry <file> --workspace <directory> (--all | --shared | --vault <owner/id>...) [--strict] [--json]
272
+ wordcell publish --out <directory> [--root <directory>] [--index <path>] [--include <path>]... [--exclude <path>]... [--include-glob <pattern>]... [--exclude-glob <pattern>]... [--where <path=value>]... [--has <path>]... [--tag <tag>]... [--scope <repository-path>]... [--from <note> [--depth <count>] [--direction <in|out|both>]] [--title <title>] [--description <text>] [--base-path <path>] [--base-url <url>] [--noindex] [--no-index-content] [--deterministic] [--dry-run] [--list-limit <0-1000>] [--force] [--json]
273
+ wordcell serve --root <directory> [--host <host>] [--port <port>] [--json]
252
274
  wordcell inbox [--root <directory>] [--source-prefix <directory>] [--limit <count>] [--json]
253
275
  wordcell context <repository-path> [--root <vault>] [--repo <repository>] [--kind <auto|file|directory>] [--json]
254
276
  wordcell agents identity <repository-scope> [--json]
@@ -359,6 +381,8 @@ type ParsedCommand =
359
381
  readonly limit?: number;
360
382
  readonly candidateLimit?: number;
361
383
  readonly minScore?: number;
384
+ readonly rerank?: "typesafe";
385
+ readonly rerankLimit?: number;
362
386
  readonly query: string;
363
387
  readonly json: boolean;
364
388
  }
@@ -446,6 +470,31 @@ type ParsedCommand =
446
470
  readonly minSupport: number;
447
471
  readonly limit: number;
448
472
  readonly json: boolean;
473
+ }
474
+ | {
475
+ readonly kind: "publish";
476
+ readonly root: string;
477
+ readonly out: string;
478
+ readonly index?: string;
479
+ readonly title?: string;
480
+ readonly description?: string;
481
+ readonly basePath?: string;
482
+ readonly baseUrl?: string;
483
+ readonly noindex: boolean;
484
+ readonly indexContent: boolean;
485
+ readonly deterministic: boolean;
486
+ readonly dryRun: boolean;
487
+ readonly listLimit: number;
488
+ readonly force: boolean;
489
+ readonly selection: PublishSelectionInput;
490
+ readonly json: boolean;
491
+ }
492
+ | {
493
+ readonly kind: "serve";
494
+ readonly root: string;
495
+ readonly host: string;
496
+ readonly port: number;
497
+ readonly json: boolean;
449
498
  };
450
499
 
451
500
  type ParseResult =
@@ -468,6 +517,8 @@ type CliDependencies = GraphCliDependencies & {
468
517
  readonly indexSemanticVault?: typeof indexSemanticVault;
469
518
  readonly openKnowledgeBase?: typeof openKnowledgeBase;
470
519
  readonly openKnowledgeBaseEvaluation?: typeof openKnowledgeBaseEvaluation;
520
+ /** Test seam; production wiring builds the TypeSafe reranker lazily per flag. */
521
+ readonly rerankers?: readonly SearchReranker[];
471
522
  readonly digestEvaluationModel?: (path: string) => Promise<string>;
472
523
  readonly evaluationNow?: () => Date;
473
524
  readonly createNote?: typeof createNote;
@@ -479,6 +530,8 @@ type CliDependencies = GraphCliDependencies & {
479
530
  readonly buildRepositoryMemoryContext?: typeof buildRepositoryMemoryContext;
480
531
  readonly auditAgentGuideRepository?: typeof auditAgentGuideRepository;
481
532
  readonly validateMarkdownAttachments?: typeof validateMarkdownAttachments;
533
+ readonly publishVault?: typeof publishVault;
534
+ readonly serveSite?: typeof serveSite;
482
535
  };
483
536
 
484
537
  function safe(value: string): string {
@@ -866,6 +919,195 @@ function parseListCommand(arguments_: readonly string[]): ParseResult {
866
919
  };
867
920
  }
868
921
 
922
+ function parsePublishCommand(arguments_: readonly string[]): ParseResult {
923
+ let root = ".";
924
+ let out: string | undefined;
925
+ let index: string | undefined;
926
+ let title: string | undefined;
927
+ let description: string | undefined;
928
+ let basePath: string | undefined;
929
+ let baseUrl: string | undefined;
930
+ let noindex = false;
931
+ let indexContent = true;
932
+ let deterministic = false;
933
+ let dryRun = false;
934
+ let listLimit = DEFAULT_PUBLISH_LIST_LIMIT;
935
+ let force = false;
936
+ let json = false;
937
+ let from: string | undefined;
938
+ let depth = 1;
939
+ let direction: LinkDirection = "both";
940
+ const includes: string[] = [];
941
+ const excludes: string[] = [];
942
+ const includeGlobs: string[] = [];
943
+ const excludeGlobs: string[] = [];
944
+ const filters: MetadataFilter[] = [];
945
+ const tags: string[] = [];
946
+ const repositoryScopes: string[] = [];
947
+
948
+ for (let cursor = 0; cursor < arguments_.length; cursor += 1) {
949
+ const argument = arguments_[cursor];
950
+ if (argument === undefined) continue;
951
+ if (argument === "--json") { json = true; continue; }
952
+ if (argument === "--noindex") { noindex = true; continue; }
953
+ if (argument === "--no-index-content") { indexContent = false; continue; }
954
+ if (argument === "--deterministic") { deterministic = true; continue; }
955
+ if (argument === "--dry-run") { dryRun = true; continue; }
956
+ if (argument === "--force") { force = true; continue; }
957
+ if (
958
+ argument === "--root" || argument === "--out" || argument === "--index"
959
+ || argument === "--title" || argument === "--description"
960
+ || argument === "--base-path" || argument === "--base-url"
961
+ || argument === "--include" || argument === "--exclude"
962
+ || argument === "--include-glob" || argument === "--exclude-glob"
963
+ || argument === "--list-limit"
964
+ || argument === "--where" || argument === "--has" || argument === "--tag"
965
+ || argument === "--scope" || argument === "--repository-scope"
966
+ || argument === "--from" || argument === "--depth" || argument === "--direction"
967
+ ) {
968
+ const value = readValue(arguments_, cursor);
969
+ if (value === null) return { ok: false, message: `${argument} requires a value` };
970
+ if (argument === "--root") root = value;
971
+ else if (argument === "--out") out = value;
972
+ else if (argument === "--index") index = value;
973
+ else if (argument === "--title") title = value;
974
+ else if (argument === "--description") description = value;
975
+ else if (argument === "--base-path") basePath = value;
976
+ else if (argument === "--base-url") baseUrl = value;
977
+ else if (argument === "--include") includes.push(value);
978
+ else if (argument === "--exclude") excludes.push(value);
979
+ else if (argument === "--include-glob") includeGlobs.push(value);
980
+ else if (argument === "--exclude-glob") excludeGlobs.push(value);
981
+ else if (argument === "--list-limit") {
982
+ if (!/^(0|[1-9][0-9]*)$/u.test(value) || Number(value) > MAX_PUBLISH_LIST_LIMIT) {
983
+ return { ok: false, message: `--list-limit must be an integer from 0 through ${MAX_PUBLISH_LIST_LIMIT}` };
984
+ }
985
+ listLimit = Number(value);
986
+ }
987
+ else if (argument === "--from") from = value;
988
+ else if (argument === "--depth") {
989
+ const parsed = Number(value);
990
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 10) {
991
+ return { ok: false, message: "--depth must be an integer from 1 through 10" };
992
+ }
993
+ depth = parsed;
994
+ } else if (argument === "--direction") {
995
+ if (value !== "in" && value !== "out" && value !== "both") {
996
+ return { ok: false, message: "--direction must be in, out, or both" };
997
+ }
998
+ direction = value;
999
+ } else if (argument === "--tag") {
1000
+ if (tags.length >= MAX_QUERY_TAGS) {
1001
+ return { ok: false, message: `Query tags may contain at most ${MAX_QUERY_TAGS} entries.` };
1002
+ }
1003
+ tags.push(value);
1004
+ } else if (argument === "--scope" || argument === "--repository-scope") {
1005
+ if (repositoryScopes.length >= MAX_REPOSITORY_SCOPES) {
1006
+ return { ok: false, message: `Repository scope filters may contain at most ${MAX_REPOSITORY_SCOPES} entries.` };
1007
+ }
1008
+ repositoryScopes.push(value);
1009
+ } else if (argument === "--has") {
1010
+ if (value.trim() === "") return { ok: false, message: "--has requires a metadata path" };
1011
+ if (filters.length >= MAX_QUERY_FILTERS) {
1012
+ return { ok: false, message: `Query filters may contain at most ${MAX_QUERY_FILTERS} entries.` };
1013
+ }
1014
+ filters.push({ kind: "exists", path: value });
1015
+ } else {
1016
+ const equals = value.indexOf("=");
1017
+ const path = equals === -1 ? "" : value.slice(0, equals).trim();
1018
+ if (path === "") return { ok: false, message: "--where requires path=value" };
1019
+ const scalar = metadataScalar(value.slice(equals + 1));
1020
+ if (!scalar.ok) return scalar;
1021
+ if (filters.length >= MAX_QUERY_FILTERS) {
1022
+ return { ok: false, message: `Query filters may contain at most ${MAX_QUERY_FILTERS} entries.` };
1023
+ }
1024
+ filters.push({ kind: "equals", path, value: scalar.value });
1025
+ }
1026
+ cursor += 1;
1027
+ continue;
1028
+ }
1029
+ return {
1030
+ ok: false,
1031
+ message: argument.startsWith("--")
1032
+ ? "unknown publish option"
1033
+ : "publish does not accept positional arguments",
1034
+ };
1035
+ }
1036
+
1037
+ if (out === undefined) return { ok: false, message: "publish requires --out <directory>" };
1038
+ try {
1039
+ validateQueryOptions({ filters, tags, repositoryScopes });
1040
+ } catch (error: unknown) {
1041
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
1042
+ }
1043
+ const selection: PublishSelectionInput = {
1044
+ includes,
1045
+ excludes,
1046
+ includeGlobs,
1047
+ excludeGlobs,
1048
+ filters,
1049
+ tags,
1050
+ repositoryScopes,
1051
+ ...(from === undefined ? {} : { from: { note: from, depth, direction } }),
1052
+ };
1053
+ return {
1054
+ ok: true,
1055
+ value: {
1056
+ kind: "publish",
1057
+ root,
1058
+ out,
1059
+ noindex,
1060
+ indexContent,
1061
+ deterministic,
1062
+ dryRun,
1063
+ listLimit,
1064
+ force,
1065
+ selection,
1066
+ json,
1067
+ ...(index === undefined ? {} : { index }),
1068
+ ...(title === undefined ? {} : { title }),
1069
+ ...(description === undefined ? {} : { description }),
1070
+ ...(basePath === undefined ? {} : { basePath }),
1071
+ ...(baseUrl === undefined ? {} : { baseUrl }),
1072
+ },
1073
+ };
1074
+ }
1075
+
1076
+ function parseServeCommand(arguments_: readonly string[]): ParseResult {
1077
+ let root: string | undefined;
1078
+ let host = "127.0.0.1";
1079
+ let port = 8080;
1080
+ let json = false;
1081
+ for (let cursor = 0; cursor < arguments_.length; cursor += 1) {
1082
+ const argument = arguments_[cursor];
1083
+ if (argument === undefined) continue;
1084
+ if (argument === "--json") { json = true; continue; }
1085
+ if (argument === "--root" || argument === "--host" || argument === "--port") {
1086
+ const value = readValue(arguments_, cursor);
1087
+ if (value === null) return { ok: false, message: `${argument} requires a value` };
1088
+ if (argument === "--root") root = value;
1089
+ else if (argument === "--host") host = value;
1090
+ else {
1091
+ const parsed = Number(value);
1092
+ if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 65_535) {
1093
+ return { ok: false, message: "--port must be an integer from 0 through 65535" };
1094
+ }
1095
+ port = parsed;
1096
+ }
1097
+ cursor += 1;
1098
+ continue;
1099
+ }
1100
+ return {
1101
+ ok: false,
1102
+ message: argument.startsWith("--")
1103
+ ? "unknown serve option"
1104
+ : "serve does not accept positional arguments",
1105
+ };
1106
+ }
1107
+ if (root === undefined) return { ok: false, message: "serve requires --root <directory>" };
1108
+ return { ok: true, value: { kind: "serve", root, host, port, json } };
1109
+ }
1110
+
869
1111
  function parseInboxCommand(arguments_: readonly string[]): ParseResult {
870
1112
  let root = ".";
871
1113
  let index: string | undefined;
@@ -942,6 +1184,8 @@ function parseSemanticCommand(command: "index" | "search", arguments_: readonly
942
1184
  let limit: number | undefined;
943
1185
  let candidateLimit: number | undefined;
944
1186
  let minScore: number | undefined;
1187
+ let rerank: "typesafe" | undefined;
1188
+ let rerankLimit: number | undefined;
945
1189
  let graphDepth: number | undefined;
946
1190
  let noGraph = false;
947
1191
  let history = false;
@@ -1003,6 +1247,8 @@ function parseSemanticCommand(command: "index" | "search", arguments_: readonly
1003
1247
  || argument === "--limit"
1004
1248
  || argument === "--candidate-limit"
1005
1249
  || argument === "--min-score"
1250
+ || argument === "--rerank"
1251
+ || argument === "--rerank-limit"
1006
1252
  || argument === "--where"
1007
1253
  || argument === "--has"
1008
1254
  || argument === "--tag"
@@ -1018,6 +1264,17 @@ function parseSemanticCommand(command: "index" | "search", arguments_: readonly
1018
1264
  return { ok: false, message: "--mode must be hybrid, exact, keyword, or semantic" };
1019
1265
  }
1020
1266
  mode = value;
1267
+ } else if (argument === "--rerank") {
1268
+ if (value !== "typesafe") {
1269
+ return { ok: false, message: "--rerank must be typesafe" };
1270
+ }
1271
+ rerank = value;
1272
+ } else if (argument === "--rerank-limit") {
1273
+ const parsed = Number(value);
1274
+ if (!Number.isSafeInteger(parsed) || parsed < 2 || parsed > MAX_RERANK_CANDIDATES) {
1275
+ return { ok: false, message: `--rerank-limit must be an integer from 2 through ${MAX_RERANK_CANDIDATES}` };
1276
+ }
1277
+ rerankLimit = parsed;
1021
1278
  } else if (argument === "--where") {
1022
1279
  const equals = value.indexOf("=");
1023
1280
  const path = equals === -1 ? "" : value.slice(0, equals).trim();
@@ -1147,6 +1404,9 @@ function parseSemanticCommand(command: "index" | "search", arguments_: readonly
1147
1404
  message: "Search minimum score applies only to hybrid, keyword, or semantic mode.",
1148
1405
  };
1149
1406
  }
1407
+ if (rerankLimit !== undefined && rerank === undefined) {
1408
+ return { ok: false, message: "--rerank-limit requires --rerank typesafe" };
1409
+ }
1150
1410
  if (priority && rulesPath === undefined) {
1151
1411
  return { ok: false, message: "--priority requires --rules" };
1152
1412
  }
@@ -1185,6 +1445,8 @@ function parseSemanticCommand(command: "index" | "search", arguments_: readonly
1185
1445
  ...(limit === undefined ? {} : { limit }),
1186
1446
  ...(candidateLimit === undefined ? {} : { candidateLimit }),
1187
1447
  ...(minScore === undefined ? {} : { minScore }),
1448
+ ...(rerank === undefined ? {} : { rerank }),
1449
+ ...(rerankLimit === undefined ? {} : { rerankLimit }),
1188
1450
  query,
1189
1451
  json,
1190
1452
  },
@@ -1938,6 +2200,8 @@ export function parseArguments(arguments_: readonly string[]): ParseResult {
1938
2200
  if (command === "note") return parseNoteCommand(arguments_.slice(1));
1939
2201
  if (command === "relation") return parseRelationCommand(arguments_.slice(1));
1940
2202
  if (command === "percolate") return parsePercolateCommand(arguments_.slice(1));
2203
+ if (command === "publish") return parsePublishCommand(arguments_.slice(1));
2204
+ if (command === "serve") return parseServeCommand(arguments_.slice(1));
1941
2205
  return { ok: false, message: "unknown command" };
1942
2206
  }
1943
2207
 
@@ -1960,10 +2224,28 @@ function renderKnowledgeBaseSearch(result: KnowledgeBaseSearchResult): string {
1960
2224
  const lines = [
1961
2225
  `${result.mode[0]?.toLocaleUpperCase("en-US") ?? ""}${result.mode.slice(1)} results for “${safe(result.query)}” (${result.results.length})${result.partial ? " [partial]" : ""}`,
1962
2226
  ];
2227
+ const rerankLane = result.diagnostics.lanes.find(({ lane }) => lane === "rerank");
2228
+ if (rerankLane !== undefined) {
2229
+ lines.push(
2230
+ ` Rerank: typesafe over ${rerankLane.results} candidates (${rerankLane.status})`
2231
+ + (rerankLane.message === undefined ? "" : ` — ${safe(rerankLane.message)}`),
2232
+ );
2233
+ const receipt = rerankLane.rerank;
2234
+ if (receipt?.accounting !== undefined) {
2235
+ const { attempted, candidates, completed, elapsedMs, usageComplete } = receipt.accounting;
2236
+ lines.push(` Rerank requests: ${attempted}/${candidates} attempted, ${completed} settled; ${elapsedMs.toFixed(0)} ms`
2237
+ + (usageComplete ? "; usage complete" : "; usage incomplete, additional charges may be unknown"));
2238
+ if (rerankLane.status !== "ready" && receipt.usage !== undefined) {
2239
+ lines.push(` Known rerank usage: ${receipt.usage.inputTokens ?? 0} input tokens, ${receipt.usage.outputTokens ?? 0} output tokens`);
2240
+ }
2241
+ }
2242
+ }
1963
2243
  if (result.results.length === 0) lines.push(" None.");
1964
2244
  for (const hit of result.results) {
1965
2245
  const location = `${safe(hit.path)}${hit.line === undefined ? "" : `:${hit.line}`}`;
1966
- const evidence = hit.evidence.map((item) => `${item.kind}#${item.rank}`).join(", ");
2246
+ const evidence = hit.evidence
2247
+ .map((item) => `${item.kind}#${searchEvidenceRank(item)}`)
2248
+ .join(", ");
1967
2249
  lines.push(` ${hit.rank}. ${hit.score.toFixed(3)} ${location} — ${safe(hit.title)} [${safe(evidence)}]`);
1968
2250
  if (hit.snippet !== "") lines.push(` ${safe(hit.snippet)}`);
1969
2251
  }
@@ -1999,12 +2281,17 @@ async function runSemantic(
1999
2281
  const searchRules = command.rulesPath === undefined
2000
2282
  ? undefined
2001
2283
  : await loadSearchRulesFile(command.rulesPath);
2002
- const kb = await (dependencies.openKnowledgeBase ?? openKnowledgeBase)({
2003
- root: command.root,
2004
- repository: command.repository,
2005
- ...(command.database === undefined ? {} : { database: command.database }),
2006
- ...(searchRules === undefined ? {} : { searchRules }),
2007
- });
2284
+ const kb = await (dependencies.openKnowledgeBase ?? openKnowledgeBase)(
2285
+ {
2286
+ root: command.root,
2287
+ repository: command.repository,
2288
+ ...(command.database === undefined ? {} : { database: command.database }),
2289
+ ...(searchRules === undefined ? {} : { searchRules }),
2290
+ },
2291
+ ...(command.rerank === undefined
2292
+ ? []
2293
+ : [{ rerankers: dependencies.rerankers ?? [await createCliTypeSafeReranker()] }]),
2294
+ );
2008
2295
  try {
2009
2296
  const result = await kb.search({
2010
2297
  query: command.query,
@@ -2020,6 +2307,12 @@ async function runSemantic(
2020
2307
  ? {}
2021
2308
  : { candidateLimit: command.candidateLimit }),
2022
2309
  ...(command.minScore === undefined ? {} : { minScore: command.minScore }),
2310
+ ...(command.rerank === undefined ? {} : {
2311
+ rerank: {
2312
+ engine: command.rerank,
2313
+ ...(command.rerankLimit === undefined ? {} : { limit: command.rerankLimit }),
2314
+ },
2315
+ }),
2023
2316
  });
2024
2317
  output.stdout(command.json
2025
2318
  ? terminalSafeJson(result)
@@ -2834,6 +3127,49 @@ async function runList(
2834
3127
  return 0;
2835
3128
  }
2836
3129
 
3130
+ async function runPublish(
3131
+ command: Extract<ParsedCommand, { readonly kind: "publish" }>,
3132
+ output: Output,
3133
+ dependencies: CliDependencies,
3134
+ ): Promise<number> {
3135
+ const result = await (dependencies.publishVault ?? publishVault)({
3136
+ root: command.root,
3137
+ out: command.out,
3138
+ noindex: command.noindex,
3139
+ indexContent: command.indexContent,
3140
+ deterministic: command.deterministic,
3141
+ dryRun: command.dryRun,
3142
+ listLimit: command.listLimit,
3143
+ force: command.force,
3144
+ selection: command.selection,
3145
+ ...(command.index === undefined ? {} : { index: command.index }),
3146
+ ...(command.title === undefined ? {} : { title: command.title }),
3147
+ ...(command.description === undefined ? {} : { description: command.description }),
3148
+ ...(command.basePath === undefined ? {} : { basePath: command.basePath }),
3149
+ ...(command.baseUrl === undefined ? {} : { baseUrl: command.baseUrl }),
3150
+ });
3151
+ output.stdout(command.json
3152
+ ? terminalSafeJson(result.report)
3153
+ : sanitizeTerminalText(renderPublishReportText(result.report, command.dryRun)));
3154
+ return 0;
3155
+ }
3156
+
3157
+ async function runServe(
3158
+ command: Extract<ParsedCommand, { readonly kind: "serve" }>,
3159
+ output: Output,
3160
+ dependencies: CliDependencies,
3161
+ ): Promise<number> {
3162
+ const site = await (dependencies.serveSite ?? serveSite)({
3163
+ root: command.root,
3164
+ host: command.host,
3165
+ port: command.port,
3166
+ });
3167
+ output.stdout(command.json
3168
+ ? terminalSafeJson({ root: site.root, host: site.host, port: site.port, url: site.url })
3169
+ : `Serving ${safe(site.root)} at ${safe(site.url)}\n`);
3170
+ return 0;
3171
+ }
3172
+
2837
3173
  async function runCatalog(
2838
3174
  command: Extract<ParsedCommand, { readonly kind: "catalog" }>,
2839
3175
  output: Output,
@@ -3580,6 +3916,8 @@ export async function main(
3580
3916
  }
3581
3917
  if (command.kind === "percolate") return await runPercolate(command, output, dependencies);
3582
3918
  if (command.kind === "list") return await runList(command, output, dependencies);
3919
+ if (command.kind === "publish") return await runPublish(command, output, dependencies);
3920
+ if (command.kind === "serve") return await runServe(command, output, dependencies);
3583
3921
  if (command.kind === "inbox") return await runInbox(command, output, dependencies);
3584
3922
  if (command.kind === "catalog") return await runCatalog(command, output, dependencies);
3585
3923
  return await runVault(command, output, dependencies);
package/src/index.ts CHANGED
@@ -11,7 +11,11 @@ export * from "./init.js";
11
11
  export * from "./navigation.js";
12
12
  export * from "./oh-adoption.js";
13
13
  export * from "./percolate.js";
14
+ export * from "./publish.js";
15
+ export * from "./publish-model.js";
14
16
  export * from "./query.js";
17
+ export * from "./rerank.js";
18
+ export * from "./rerank-typesafe.js";
15
19
  export * from "./repository-memory.js";
16
20
  export * from "./search.js";
17
21
  export * from "./semantic.js";