@hraness/wordcell 0.21.2 → 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 (87) hide show
  1. package/README.md +196 -66
  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 +61 -3800
  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-4ag64rc1.js +4299 -0
  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-180zbkkt.js → index-de2w8crk.js} +14 -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-by0pbant.js → index-fc4dr114.js} +5 -2
  29. package/dist/{index-x6m57nfc.js → index-g5vsqmdy.js} +28 -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-mywv1em0.js +140 -0
  48. package/dist/workflows/decision-context.js +13 -12
  49. package/dist/workflows/index.js +13 -12
  50. package/package.json +68 -7
  51. package/skills/wordcell/AGENTS.md +1 -0
  52. package/skills/wordcell/SKILL.md +21 -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 +353 -9
  57. package/src/cli.ts +12 -1
  58. package/src/clip/acquire.ts +2 -0
  59. package/src/clip/doctor.ts +2 -0
  60. package/src/clip/media.ts +2 -0
  61. package/src/index.ts +4 -0
  62. package/src/oh/authority.ts +14 -5
  63. package/src/oh/canonical-rust.ts +127 -0
  64. package/src/oh/projection-rust.ts +63 -0
  65. package/src/oh-adoption.ts +7 -1
  66. package/src/publish-graph.ts +0 -0
  67. package/src/publish-index.ts +195 -0
  68. package/src/publish-markdown.ts +617 -0
  69. package/src/publish-model.ts +599 -0
  70. package/src/publish-nav.ts +204 -0
  71. package/src/publish-pages.ts +382 -0
  72. package/src/publish-reader/reader.css +1042 -0
  73. package/src/publish-reader/reader.ts +1017 -0
  74. package/src/publish-reader/theme.ts +97 -0
  75. package/src/publish-search.ts +418 -0
  76. package/src/publish-select.ts +419 -0
  77. package/src/publish-theme.ts +106 -0
  78. package/src/publish.ts +904 -0
  79. package/src/rerank-credentials.ts +56 -0
  80. package/src/rerank-typesafe.ts +472 -0
  81. package/src/rerank.ts +389 -0
  82. package/src/sdk.ts +157 -6
  83. package/src/serve.ts +180 -0
  84. package/src/support-profile.ts +7 -0
  85. package/src/support.ts +73 -0
  86. package/src/vault.ts +23 -9
  87. package/dist/{index-why54q5p.js → index-gm9t95d9.js} +3 -3
@@ -0,0 +1,127 @@
1
+ import { canonicalJson } from "@hraness/oh";
2
+ import {
3
+ OH_CANONICAL_RAW_WASM_BASE64,
4
+ OH_CANONICAL_RAW_WASM_SHA256,
5
+ } from "@hraness/oh/canonical-rust/artifact";
6
+ import { emitOhRustFallback } from "@hraness/oh/rust-fallback";
7
+
8
+ /**
9
+ * Optional Rust canonical-JSON/digest engine (`oh.canonical.rust.v1`), vendored
10
+ * from `hraness/oh`. Returns `null` when the engine cannot be loaded or cannot
11
+ * match the reference `@hraness/oh` output, so callers keep the existing TypeScript
12
+ * path as the fallback.
13
+ */
14
+
15
+ interface OhCanonicalRawExports {
16
+ readonly memory: WebAssembly.Memory;
17
+ readonly oh_canonical_alloc: (len: number) => number;
18
+ readonly oh_canonical_free: (ptr: number, capacity: number) => void;
19
+ readonly oh_canonical_json: (ptr: number, len: number) => number;
20
+ readonly oh_canonical_sha256: (ptr: number, len: number) => number;
21
+ }
22
+
23
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
24
+
25
+ function decodeBase64(text: string): Uint8Array {
26
+ const clean = text.replace(/=+$/u, "");
27
+ const bytes = new Uint8Array(Math.floor((clean.length * 3) / 4));
28
+ let bits = 0;
29
+ let bitCount = 0;
30
+ let offset = 0;
31
+ for (const character of clean) {
32
+ const value = BASE64_ALPHABET.indexOf(character);
33
+ if (value < 0) throw new Error("invalid base64 artifact");
34
+ bits = (bits << 6) | value;
35
+ bitCount += 6;
36
+ if (bitCount >= 8) {
37
+ bitCount -= 8;
38
+ bytes[offset] = (bits >> bitCount) & 0xff;
39
+ offset += 1;
40
+ }
41
+ }
42
+ if (offset !== bytes.length) throw new Error("base64 artifact length mismatch");
43
+ return bytes;
44
+ }
45
+
46
+ let cached: OhCanonicalRawExports | null | undefined;
47
+
48
+ function engine(): OhCanonicalRawExports | null {
49
+ if (cached !== undefined) return cached;
50
+ try {
51
+ const bytes = decodeBase64(OH_CANONICAL_RAW_WASM_BASE64);
52
+ const instance = new WebAssembly.Instance(new WebAssembly.Module(bytes.buffer as ArrayBuffer));
53
+ cached = instance.exports as unknown as OhCanonicalRawExports;
54
+ } catch {
55
+ cached = null;
56
+ }
57
+ return cached;
58
+ }
59
+
60
+ const encoder = new TextEncoder();
61
+ const decoder = new TextDecoder();
62
+
63
+ function runEngine(
64
+ text: string,
65
+ call: (exports: OhCanonicalRawExports, ptr: number, len: number) => number,
66
+ ): string | null {
67
+ const exports = engine();
68
+ if (exports === null) return null;
69
+ const input = encoder.encode(text);
70
+ if (input.length === 0) return null;
71
+ const inputPtr = exports.oh_canonical_alloc(input.length);
72
+ if (inputPtr === 0) return null;
73
+ let resultPtr = 0;
74
+ let resultCapacity = 0;
75
+ try {
76
+ new Uint8Array(exports.memory.buffer as ArrayBuffer, inputPtr, input.length).set(input);
77
+ resultPtr = call(exports, inputPtr, input.length);
78
+ if (resultPtr === 0) return null;
79
+ const memoryLength = exports.memory.buffer.byteLength;
80
+ if (resultPtr > memoryLength - 12) return null;
81
+ const view = new DataView(exports.memory.buffer as ArrayBuffer, resultPtr, 12);
82
+ const capacity = view.getUint32(0, true);
83
+ const status = view.getUint32(4, true);
84
+ const payloadLength = view.getUint32(8, true);
85
+ if (capacity < 12 || capacity > memoryLength - resultPtr || payloadLength > capacity - 12) return null;
86
+ resultCapacity = capacity;
87
+ if (status !== 0 || payloadLength === 0) return null;
88
+ return decoder.decode(new Uint8Array(exports.memory.buffer as ArrayBuffer, resultPtr + 12, payloadLength));
89
+ } catch {
90
+ return null;
91
+ } finally {
92
+ if (resultPtr !== 0 && resultCapacity > 0) exports.oh_canonical_free(resultPtr, resultCapacity);
93
+ exports.oh_canonical_free(inputPtr, input.length);
94
+ }
95
+ }
96
+
97
+ /** Canonical JSON text matching `@hraness/oh`, or `null` if the engine cannot match. */
98
+ export function canonicalJsonRust(value: unknown): string | null {
99
+ const json = JSON.stringify(value);
100
+ if (json === undefined) return null;
101
+ const rust = runEngine(json, (exports, ptr, len) => exports.oh_canonical_json(ptr, len));
102
+ if (rust === null) return null;
103
+ try {
104
+ if (rust !== canonicalJson(value)) return null;
105
+ } catch {
106
+ return null;
107
+ }
108
+ return rust;
109
+ }
110
+
111
+ /** SHA-256 hex of the canonical JSON, matching `@hraness/oh`, or `null` if the engine cannot match. */
112
+ export function canonicalSha256Rust(value: unknown): string | null {
113
+ if (canonicalJsonRust(value) === null) return null;
114
+ const json = JSON.stringify(value);
115
+ if (json === undefined) return null;
116
+ return runEngine(json, (exports, ptr, len) => exports.oh_canonical_sha256(ptr, len));
117
+ }
118
+
119
+ /** Engine identity and artifact digest for diagnostics and tests. */
120
+ export function canonicalRustEngineInfo(): { engine: string; artifactSha256: string; available: boolean } {
121
+ return { engine: "oh.canonical.rust.v1", artifactSha256: OH_CANONICAL_RAW_WASM_SHA256, available: engine() !== null };
122
+ }
123
+
124
+ /** Emit a non-fatal telemetry notice when the Rust engine falls back to TS. */
125
+ export function emitCanonicalRustFallback(reason: string, inputClass: string): void {
126
+ emitOhRustFallback({ tag: "oh-canonical-rust-fallback", reason, inputClass });
127
+ }
@@ -0,0 +1,63 @@
1
+ import { evaluateOhProjectionV1, type OhProjectionDatasetV1, type OhProjectionEvaluationOptionsV1, type OhProjectionQueryV1, type OhProjectionResultV1, type OhProjectionRulePackV1, type OhProjectionSnapshotV1 } from "@hraness/oh/projection";
2
+ import { emitOhRustFallback } from "@hraness/oh/rust-fallback";
3
+
4
+ interface ProjectionRustEngineV1 {
5
+ readonly engine: "oh.projection.rust.v1";
6
+ evaluate(input: Readonly<{
7
+ dataset: OhProjectionDatasetV1;
8
+ options?: OhProjectionEvaluationOptionsV1;
9
+ query: OhProjectionQueryV1;
10
+ rulePack: OhProjectionRulePackV1;
11
+ snapshot: OhProjectionSnapshotV1;
12
+ }>): OhProjectionResultV1;
13
+ }
14
+
15
+ let cached: ProjectionRustEngineV1 | null | undefined;
16
+
17
+ export function emitProjectionRustFallback(reason: "load-failed" | "evaluate-failed"): void {
18
+ emitOhRustFallback({ tag: "oh-projection-rust-fallback", reason });
19
+ }
20
+
21
+ const OH_PROJECTION_RUST_MODULE = "@hraness/oh/projection/rust";
22
+
23
+ async function loadEngine(): Promise<ProjectionRustEngineV1 | null> {
24
+ if (cached !== undefined) return cached;
25
+ try {
26
+ const module = (await import(OH_PROJECTION_RUST_MODULE)) as {
27
+ loadProjectionRustEngineV1?(): Promise<ProjectionRustEngineV1 | null>;
28
+ };
29
+ if (typeof module.loadProjectionRustEngineV1 !== "function") {
30
+ cached = null;
31
+ return null;
32
+ }
33
+ cached = await module.loadProjectionRustEngineV1();
34
+ } catch {
35
+ emitProjectionRustFallback("load-failed");
36
+ cached = null;
37
+ }
38
+ return cached;
39
+ }
40
+
41
+ /** Load the optional Rust projection engine once per process. */
42
+ export async function loadProjectionRustEngineV1(): Promise<ProjectionRustEngineV1 | null> {
43
+ return loadEngine();
44
+ }
45
+
46
+ /** Evaluate a projection, preferring the Rust engine when available. */
47
+ export function evaluateOhProjectionWithFallbackV1(input: Readonly<{
48
+ dataset: OhProjectionDatasetV1;
49
+ engine: ProjectionRustEngineV1 | null;
50
+ options?: OhProjectionEvaluationOptionsV1;
51
+ query: OhProjectionQueryV1;
52
+ rulePack: OhProjectionRulePackV1;
53
+ snapshot: OhProjectionSnapshotV1;
54
+ }>): OhProjectionResultV1 {
55
+ if (input.engine !== null) {
56
+ try {
57
+ return input.engine.evaluate(input);
58
+ } catch {
59
+ emitProjectionRustFallback("evaluate-failed");
60
+ }
61
+ }
62
+ return evaluateOhProjectionV1(input);
63
+ }
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
2
2
  import { posix } from "node:path";
3
3
 
4
4
  import { canonicalJson, canonicalSha256 } from "@hraness/oh";
5
+
6
+ import { canonicalSha256Rust, emitCanonicalRustFallback } from "./oh/canonical-rust.js";
5
7
  import {
6
8
  parseOhHeadV1,
7
9
  parseOhStoreBindingV1,
@@ -419,7 +421,11 @@ function prepareWithPolicy(
419
421
  transformations,
420
422
  v: 1 as const,
421
423
  };
422
- const candidateSha256 = canonicalSha256(manifest);
424
+ const rustSha256 = canonicalSha256Rust(manifest);
425
+ if (rustSha256 === null) {
426
+ emitCanonicalRustFallback("canonicalSha256Rust", "object");
427
+ }
428
+ const candidateSha256 = rustSha256 ?? canonicalSha256(manifest);
423
429
  const markdown = renderMarkdown(manifest, candidateSha256);
424
430
  if (Buffer.byteLength(markdown, "utf8") > MAX_CAPSULE_BYTES) {
425
431
  throw new RangeError("The adoption candidate exceeds its Markdown byte limit.");
Binary file
@@ -0,0 +1,195 @@
1
+ import { canonicalJson } from "@hraness/oh";
2
+
3
+ import type { Note } from "./graph.js";
4
+ import {
5
+ WORDCELL_SITE_DOCS_FORMAT_V1,
6
+ WORDCELL_SITE_LIMITS_V1,
7
+ WORDCELL_SITE_POSTINGS_FORMAT_V1,
8
+ WORDCELL_SITE_TERMS_FORMAT_V1,
9
+ type WordcellSiteContentSearchV1,
10
+ type WordcellSiteDocsV1,
11
+ type WordcellSiteDocV1,
12
+ type WordcellSitePostingsV1,
13
+ type WordcellSiteTermsV1,
14
+ } from "./publish-model.js";
15
+ import {
16
+ publishNormalize,
17
+ publishShardName,
18
+ } from "./publish-search.js";
19
+
20
+ const TERM_PATTERN = /[\p{L}\p{N}][\p{L}\p{N}._/-]*/gu;
21
+
22
+ export type SiteIndexBuild = {
23
+ readonly docs: WordcellSiteDocsV1;
24
+ readonly terms: WordcellSiteTermsV1;
25
+ readonly postings: ReadonlyMap<string, WordcellSitePostingsV1>;
26
+ readonly termsCount: number;
27
+ readonly termsTruncated: boolean;
28
+ readonly textTruncated: boolean;
29
+ readonly mode: WordcellSiteContentSearchV1;
30
+ };
31
+
32
+ function bounded(value: string, maximumBytes: number): { text: string; truncated: boolean } {
33
+ if (Buffer.byteLength(value, "utf8") <= maximumBytes) return { text: value, truncated: false };
34
+ const bytes = Buffer.from(value, "utf8");
35
+ let end = maximumBytes;
36
+ while (end > 0 && ((bytes[end] ?? 0) & 0xc0) === 0x80) end -= 1;
37
+ return { text: bytes.subarray(0, end).toString("utf8"), truncated: true };
38
+ }
39
+
40
+ function previewText(note: Note): string {
41
+ const basis = note.summary !== "" ? note.summary : note.searchableText;
42
+ const collapsed = basis.replace(/\s+/gu, " ").trim();
43
+ return bounded(collapsed, WORDCELL_SITE_LIMITS_V1.docPreviewBytes).text;
44
+ }
45
+
46
+ function indexableTerms(text: string): readonly string[] {
47
+ const terms = new Set<string>();
48
+ for (const match of text.matchAll(TERM_PATTERN)) terms.add(match[0]);
49
+ return [...terms];
50
+ }
51
+
52
+ /**
53
+ * Build the document table, term dictionary, and content postings for a
54
+ * published selection. The document table always carries normalized eager
55
+ * fields (title, aliases, path/id, tags, metadata) so browser field matching
56
+ * reproduces the exact lane's substring semantics. Content is indexed inline
57
+ * when the whole selection fits the inline budget, otherwise by sharded
58
+ * postings; `indexContent: false` emits no content index at all.
59
+ */
60
+ export function buildSiteIndex(
61
+ notes: readonly Note[],
62
+ slugById: ReadonlyMap<string, string>,
63
+ options: { readonly indexContent?: boolean } = {},
64
+ ): SiteIndexBuild {
65
+ const indexContent = options.indexContent !== false;
66
+ const fields: WordcellSiteDocV1["f"][] = [];
67
+ const inlineTexts: string[] = [];
68
+ let textTruncated = false;
69
+ let inlineTotal = 0;
70
+
71
+ for (const note of notes) {
72
+ const title = bounded(publishNormalize(note.title), WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
73
+ const aliases = bounded(
74
+ note.aliases.map(publishNormalize).join("\n"),
75
+ WORDCELL_SITE_LIMITS_V1.fieldTextBytes,
76
+ );
77
+ const path = bounded(
78
+ `${publishNormalize(note.path)}\n${publishNormalize(note.id)}`,
79
+ WORDCELL_SITE_LIMITS_V1.fieldTextBytes,
80
+ );
81
+ const tags = bounded(
82
+ note.tags.map(publishNormalize).join("\n"),
83
+ WORDCELL_SITE_LIMITS_V1.fieldTextBytes,
84
+ );
85
+ const metadata = bounded(
86
+ publishNormalize(canonicalJson(note.metadata)),
87
+ WORDCELL_SITE_LIMITS_V1.fieldTextBytes,
88
+ );
89
+ textTruncated ||= title.truncated || aliases.truncated || path.truncated
90
+ || tags.truncated || metadata.truncated;
91
+ fields.push({ t: title.text, a: aliases.text, p: path.text, g: tags.text, m: metadata.text });
92
+
93
+ if (indexContent) {
94
+ // Match the exact lane's basis: searchableText strips frontmatter,
95
+ // code fences, and comments before indexing.
96
+ const inline = bounded(
97
+ publishNormalize(note.searchableText),
98
+ WORDCELL_SITE_LIMITS_V1.inlineTextBytes,
99
+ );
100
+ textTruncated ||= inline.truncated;
101
+ inlineTexts.push(inline.text);
102
+ inlineTotal += Buffer.byteLength(inline.text, "utf8");
103
+ }
104
+ }
105
+
106
+ const mode: WordcellSiteContentSearchV1 = !indexContent
107
+ ? "none"
108
+ : inlineTotal <= WORDCELL_SITE_LIMITS_V1.inlineTotalBytes
109
+ ? "inline"
110
+ : "shards";
111
+
112
+ const allTerms = new Set<string>();
113
+ const contentPostings = new Map<string, number[]>();
114
+ const docs: WordcellSiteDocV1[] = [];
115
+
116
+ for (const [index, note] of notes.entries()) {
117
+ const slug = slugById.get(note.id);
118
+ if (slug === undefined) throw new Error(`Missing published slug for ${note.id}.`);
119
+ const field = fields[index];
120
+ if (field === undefined) throw new Error(`Missing indexed fields for ${note.id}.`);
121
+ const inline = mode === "inline" ? inlineTexts[index] : undefined;
122
+
123
+ const fieldTerms = indexableTerms(
124
+ `${field.t}\n${field.a}\n${field.p}\n${field.g}\n${field.m}`,
125
+ );
126
+ for (const term of fieldTerms) allTerms.add(term);
127
+
128
+ if (mode !== "none") {
129
+ const content = inlineTexts[index] ?? "";
130
+ for (const term of indexableTerms(content)) {
131
+ allTerms.add(term);
132
+ if (mode === "shards") {
133
+ const list = contentPostings.get(term) ?? [];
134
+ list.push(index);
135
+ contentPostings.set(term, list);
136
+ }
137
+ }
138
+ }
139
+
140
+ docs.push({
141
+ i: index,
142
+ s: slug,
143
+ t: note.title,
144
+ p: previewText(note),
145
+ f: field,
146
+ ...(inline === undefined ? {} : { x: inline }),
147
+ });
148
+ }
149
+
150
+ let termsTruncated = false;
151
+ let sortedTerms = [...allTerms].toSorted((left, right) => left.localeCompare(right));
152
+ if (sortedTerms.length > WORDCELL_SITE_LIMITS_V1.indexTerms) {
153
+ sortedTerms = sortedTerms.slice(0, WORDCELL_SITE_LIMITS_V1.indexTerms);
154
+ termsTruncated = true;
155
+ }
156
+ const admitted = new Set(sortedTerms);
157
+
158
+ const postings = new Map<string, WordcellSitePostingsV1>();
159
+ if (mode === "shards") {
160
+ const grouped = new Map<string, Record<string, readonly number[]>>();
161
+ for (const [term, docIds] of [...contentPostings.entries()].toSorted(([a], [b]) =>
162
+ a.localeCompare(b))) {
163
+ if (!admitted.has(term)) continue;
164
+ const shard = publishShardName(term);
165
+ const bucket = grouped.get(shard) ?? Object.create(null) as Record<string, readonly number[]>;
166
+ bucket[term] = docIds;
167
+ grouped.set(shard, bucket);
168
+ }
169
+ for (const [shard, bucket] of [...grouped.entries()].toSorted(([a], [b]) =>
170
+ a.localeCompare(b))) {
171
+ postings.set(shard, {
172
+ format: WORDCELL_SITE_POSTINGS_FORMAT_V1,
173
+ shard,
174
+ postings: Object.freeze(bucket),
175
+ });
176
+ }
177
+ }
178
+
179
+ return {
180
+ docs: {
181
+ format: WORDCELL_SITE_DOCS_FORMAT_V1,
182
+ content: mode,
183
+ docs,
184
+ },
185
+ terms: {
186
+ format: WORDCELL_SITE_TERMS_FORMAT_V1,
187
+ terms: sortedTerms,
188
+ },
189
+ postings,
190
+ termsCount: sortedTerms.length,
191
+ termsTruncated,
192
+ textTruncated,
193
+ mode,
194
+ };
195
+ }