@hraness/wordcell 0.21.3 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -64
- package/dist/authoring.js +5 -3
- package/dist/{authority-4sp9d552.js → authority-06eqhqam.js} +57 -12
- package/dist/benchmark.js +4 -4
- package/dist/capture.js +6 -5
- package/dist/cli.js +39 -33
- package/dist/clip/acquire.js +3 -2
- package/dist/clip/cli.js +6 -5
- package/dist/clip/doctor.js +4 -3
- package/dist/clip/jobs.js +3 -3
- package/dist/clip/refresh.js +1 -1
- package/dist/evaluation-builder.js +20 -19
- package/dist/evaluation-kb.js +16 -15
- package/dist/graph-authority.js +5 -5
- package/dist/graph-percolation.js +7 -7
- package/dist/graph.js +1 -1
- package/dist/{index-pbr6rv47.js → index-1k595z6v.js} +6 -6
- package/dist/{index-0k2x4nn9.js → index-233z9wmn.js} +17 -5
- package/dist/index-3agn8scn.js +306 -0
- package/dist/index-4knsp9qj.js +34 -0
- package/dist/index-66pshdtx.js +341 -0
- package/dist/{index-59ssmqqm.js → index-76dbn6pe.js} +1 -1
- package/dist/{index-0rfcqab6.js → index-810nfzdj.js} +617 -66
- package/dist/index-9rf81m0p.js +368 -0
- package/dist/{index-51bap0mq.js → index-bqtqeak3.js} +10 -10
- package/dist/{index-9zcpdbjg.js → index-de2w8crk.js} +13 -13
- package/dist/{index-bgfzwt4h.js → index-djrcc1yf.js} +6 -6
- package/dist/{index-6sw24nvv.js → index-ecqpgr5y.js} +53 -2
- package/dist/{index-van48n3g.js → index-fc4dr114.js} +4 -2
- package/dist/{index-jzcws0zn.js → index-g5vsqmdy.js} +27 -57
- package/dist/index-hmw17zaa.js +1985 -0
- package/dist/index-j70m75wd.js +217 -0
- package/dist/{index-nd6nynv2.js → index-jk1sgx36.js} +1 -1
- package/dist/{index-3ehtt59c.js → index-n386evxj.js} +1 -1
- package/dist/{index-vdw5yjfa.js → index-q7tbg07z.js} +110 -20
- package/dist/{index-ekpwvbra.js → index-qbssx940.js} +1 -2
- package/dist/{index-115b07ap.js → index-rr9kzq4n.js} +5 -5
- package/dist/{index-11621h23.js → index-tf4wzjew.js} +1 -1
- package/dist/{index-why54q5p.js → index-vnybgywh.js} +4 -4
- package/dist/{index-bcfn9xah.js → index-xv80vzfp.js} +7 -7
- package/dist/index.js +289 -95
- package/dist/percolate.js +2 -2
- package/dist/portfolio.js +16 -15
- package/dist/publish-model.js +36 -0
- package/dist/publish-reader/reader.css +1042 -0
- package/dist/publish-reader/reader.js +1534 -0
- package/dist/publish-reader/theme.js +92 -0
- package/dist/publish-search.js +52 -0
- package/dist/publish.js +36 -0
- package/dist/rerank-typesafe.js +13 -0
- package/dist/rerank.js +14 -0
- package/dist/sdk.js +16 -13
- package/dist/search.js +3 -3
- package/dist/semantic.js +5 -5
- package/dist/{support-vm4mkw0k.js → support-1rh2d7mt.js} +38 -32
- package/dist/workflows/decision-context.js +15 -14
- package/dist/workflows/index.js +15 -14
- package/package.json +65 -6
- package/skills/wordcell/AGENTS.md +1 -0
- package/skills/wordcell/SKILL.md +2 -1
- package/skills/wordcell/agents/openai.yaml +1 -1
- package/skills/wordcell/references/publish.md +127 -0
- package/skills/wordcell/references/query.md +22 -0
- package/skills/wordcell/references/refresh.md +14 -0
- package/src/authoring-model.ts +29 -0
- package/src/authoring-program.ts +38 -3
- package/src/authoring.ts +17 -3
- package/src/cli-program.ts +441 -12
- package/src/graph.ts +1 -1
- package/src/index.ts +4 -0
- package/src/oh/authority.ts +14 -5
- package/src/oh/canonical-rust.ts +127 -0
- package/src/oh/projection-rust.ts +63 -0
- package/src/oh-adoption.ts +7 -1
- package/src/publish-graph.ts +0 -0
- package/src/publish-index.ts +195 -0
- package/src/publish-markdown.ts +617 -0
- package/src/publish-model.ts +599 -0
- package/src/publish-nav.ts +204 -0
- package/src/publish-pages.ts +382 -0
- package/src/publish-reader/reader.css +1042 -0
- package/src/publish-reader/reader.ts +1017 -0
- package/src/publish-reader/theme.ts +97 -0
- package/src/publish-search.ts +418 -0
- package/src/publish-select.ts +419 -0
- package/src/publish-theme.ts +106 -0
- package/src/publish.ts +904 -0
- package/src/rerank-credentials.ts +56 -0
- package/src/rerank-typesafe.ts +472 -0
- package/src/rerank.ts +389 -0
- package/src/sdk.ts +157 -6
- package/src/serve.ts +180 -0
- package/src/vault.ts +23 -9
package/src/graph.ts
CHANGED
|
@@ -608,7 +608,7 @@ function aliasesFromMetadata(metadata: MetadataObject): readonly string[] {
|
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
function frontmatterOf(content: string, path: string): Frontmatter {
|
|
611
|
-
const lines = content.split(
|
|
611
|
+
const lines = content.split(/\r?\n/u);
|
|
612
612
|
if (lines[0]?.trim() !== "---") {
|
|
613
613
|
return {
|
|
614
614
|
values: new Map(),
|
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";
|
package/src/oh/authority.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { isProxy } from "node:util/types";
|
|
|
3
3
|
import { canonicalJson, canonicalSha256, type KnowledgeGraphChangeV1, type KnowledgeGraphRecordV1 } from "@hraness/oh";
|
|
4
4
|
import {
|
|
5
5
|
createOhProjectionDatasetV1, createOhProjectionFactV1, createOhProjectionSnapshotV1,
|
|
6
|
-
|
|
6
|
+
parseOhProjectionResultV1, type OhProjectionProofV1,
|
|
7
7
|
} from "@hraness/oh/projection";
|
|
8
8
|
import { createOhSqliteStoreAuthorityV1 } from "@hraness/oh/sqlite";
|
|
9
9
|
import { createOhStoreProfileV1, OH_WORKING_STORE_PROFILE_V1, type OhStoreV1 } from "@hraness/oh/store";
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
GRAPH_LIMITS, GraphAuthorityError, type GraphAuthority, type GraphProof,
|
|
12
12
|
type GraphQueryLimits, type GraphQueryRequest, type GraphQueryResult, type GraphSnapshot,
|
|
13
13
|
} from "../graph-authority-model";
|
|
14
|
+
import { evaluateOhProjectionWithFallbackV1, loadProjectionRustEngineV1 } from "./projection-rust";
|
|
14
15
|
import { compileGraphProgram, normalizeGraphQueryRequest } from "./programs";
|
|
15
16
|
import { GRAPH_EXTRACTOR_SHA256, GRAPH_MANIFEST_KEY, manifestGraphRecord, parseGraphSnapshot, sourceGraphRecord } from "./snapshot";
|
|
16
17
|
import { deepFreeze, detachedData, exactKeys } from "./validation";
|
|
@@ -95,6 +96,7 @@ export async function openOhGraphAdapter(input: GraphSnapshot, options: OhGraphA
|
|
|
95
96
|
const materialized = await selectedStore.snapshot({ maximumRecords: GRAPH_LIMITS.notes + 1 });
|
|
96
97
|
if (canonicalJson(materialized.records) !== canonicalJson(expected)) throw new GraphAuthorityError("corrupt-cache", "Graph reconciliation did not retain exact source records.");
|
|
97
98
|
const projectionSnapshot = createOhProjectionSnapshotV1({ ...materialized, spaceId });
|
|
99
|
+
const rustEngine = await loadProjectionRustEngineV1();
|
|
98
100
|
const recordMap = new Map(records.map((record, index) => [record.key, { record, source: snapshot.records[index]! }]));
|
|
99
101
|
const assertOpen = () => { if (closed) throw new GraphAuthorityError("closed", "The graph authority is closed."); };
|
|
100
102
|
const assertCurrent = async () => {
|
|
@@ -141,10 +143,17 @@ export async function openOhGraphAdapter(input: GraphSnapshot, options: OhGraphA
|
|
|
141
143
|
});
|
|
142
144
|
try {
|
|
143
145
|
const dataset = createOhProjectionDatasetV1({ extractorSha256: GRAPH_EXTRACTOR_SHA256, factPackId: "wordcell.graph-facts", factPackRevision: 1, facts, snapshot: projectionSnapshot });
|
|
144
|
-
const result =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
const result = evaluateOhProjectionWithFallbackV1({
|
|
147
|
+
dataset,
|
|
148
|
+
engine: rustEngine,
|
|
149
|
+
rulePack: program.rulePack,
|
|
150
|
+
query: program.query,
|
|
151
|
+
snapshot: projectionSnapshot,
|
|
152
|
+
options: {
|
|
153
|
+
maximumDerivedTuples: limits.derivedTuples, maximumRounds: limits.rounds, maximumWorkUnits: limits.workUnits,
|
|
154
|
+
maximumProofDepth: limits.proofDepth, maximumProofNodes: limits.proofNodes, maximumTotalProofNodes: limits.totalProofNodes, maximumResultBytes: limits.resultBytes,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
148
157
|
if (parseOhProjectionResultV1(result, result.identity.projectionSha256) === null) throw new GraphAuthorityError("corrupt-cache", "Oh returned an invalid bounded projection result.");
|
|
149
158
|
await assertCurrent();
|
|
150
159
|
const proofBudget = { remaining: limits.resultBytes };
|
|
@@ -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
|
+
}
|
package/src/oh-adoption.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|