@hawkeyexl/manni 4.1.0 → 4.3.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 (51) hide show
  1. package/README.md +2 -2
  2. package/dist/batch-7JJWXWSF.js +18 -0
  3. package/dist/batch-7JJWXWSF.js.map +1 -0
  4. package/dist/calibrate-IA2ARFMH.js +13 -0
  5. package/dist/calibrate-IA2ARFMH.js.map +1 -0
  6. package/dist/capture-C32JXIFN.js +15 -0
  7. package/dist/capture-C32JXIFN.js.map +1 -0
  8. package/dist/chunk-3R23C5DT.js +120 -0
  9. package/dist/chunk-3R23C5DT.js.map +1 -0
  10. package/dist/chunk-CI2GOU3A.js +4447 -0
  11. package/dist/chunk-CI2GOU3A.js.map +1 -0
  12. package/dist/{chunk-I57KBF45.js → chunk-DP2ZA3WH.js} +1203 -142
  13. package/dist/chunk-DP2ZA3WH.js.map +1 -0
  14. package/dist/chunk-ESJ5W7EV.js +297 -0
  15. package/dist/chunk-ESJ5W7EV.js.map +1 -0
  16. package/dist/chunk-IOYXPBCL.js +37 -0
  17. package/dist/chunk-IOYXPBCL.js.map +1 -0
  18. package/dist/chunk-KTSJ2ZHA.js +40 -0
  19. package/dist/chunk-KTSJ2ZHA.js.map +1 -0
  20. package/dist/{chunk-JODRT4NF.js → chunk-MFDD3YGU.js} +8 -37
  21. package/dist/chunk-MFDD3YGU.js.map +1 -0
  22. package/dist/chunk-MOQXWXSG.js +1941 -0
  23. package/dist/chunk-MOQXWXSG.js.map +1 -0
  24. package/dist/{chunk-S3NDEIOW.js → chunk-O6RVQKK6.js} +10502 -3017
  25. package/dist/chunk-O6RVQKK6.js.map +1 -0
  26. package/dist/chunk-QROCFW5S.js +174 -0
  27. package/dist/chunk-QROCFW5S.js.map +1 -0
  28. package/dist/chunk-RVVCA4WL.js +379 -0
  29. package/dist/chunk-RVVCA4WL.js.map +1 -0
  30. package/dist/cli.js +1293 -140
  31. package/dist/cli.js.map +1 -1
  32. package/dist/docmeta.js +3 -2
  33. package/dist/docmeta.js.map +1 -1
  34. package/dist/graph/embed.d.ts +154 -0
  35. package/dist/graph/embed.js +137 -0
  36. package/dist/graph/embed.js.map +1 -0
  37. package/dist/graph/runtime.d.ts +751 -0
  38. package/dist/graph/runtime.js +3067 -0
  39. package/dist/graph/runtime.js.map +1 -0
  40. package/dist/index.d.ts +6149 -799
  41. package/dist/index.js +682 -7
  42. package/dist/index.js.map +1 -1
  43. package/dist/picker-6HRMETE7.js +12 -0
  44. package/dist/picker-6HRMETE7.js.map +1 -0
  45. package/package.json +32 -4
  46. package/plugin/tracevals/.claude-plugin/plugin.json +9 -0
  47. package/plugin/tracevals/hooks/hooks.json +17 -0
  48. package/shapes/graph/shapes-1.0.0.ttl +689 -0
  49. package/dist/chunk-I57KBF45.js.map +0 -1
  50. package/dist/chunk-JODRT4NF.js.map +0 -1
  51. package/dist/chunk-S3NDEIOW.js.map +0 -1
package/dist/docmeta.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildProgram
4
- } from "./chunk-JODRT4NF.js";
4
+ } from "./chunk-MFDD3YGU.js";
5
5
  import {
6
6
  runIfMain
7
- } from "./chunk-I57KBF45.js";
7
+ } from "./chunk-DP2ZA3WH.js";
8
+ import "./chunk-IOYXPBCL.js";
8
9
 
9
10
  // src/docmeta.ts
10
11
  runIfMain(import.meta.url, () => buildProgram().name("docmeta"));
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/docmeta.ts"],"sourcesContent":["/**\n * The `docmeta` bin: the metadata tool under the name it had before it became\n * `manni meta`. Same program, same options, same exit codes, so a script or\n * workflow written against `docmeta …` keeps working. Only the usage line and\n * the stderr prefix say `docmeta`.\n */\nimport { buildProgram } from \"./meta/cli.js\";\nimport { runIfMain } from \"./shared/run.js\";\n\nrunIfMain(import.meta.url, () => buildProgram().name(\"docmeta\"));\n"],"mappings":";;;;;;;;;AASA,UAAU,YAAY,KAAK,MAAM,aAAa,EAAE,KAAK,SAAS,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/docmeta.ts"],"sourcesContent":["/**\n * The `docmeta` bin: the metadata tool under the name it had before it became\n * `manni meta`. Same program, same options, same exit codes, so a script or\n * workflow written against `docmeta …` keeps working. Only the usage line and\n * the stderr prefix say `docmeta`.\n */\nimport { buildProgram } from \"./meta/cli.js\";\nimport { runIfMain } from \"./shared/run.js\";\n\nrunIfMain(import.meta.url, () => buildProgram().name(\"docmeta\"));\n"],"mappings":";;;;;;;;;;AASA,UAAU,YAAY,KAAK,MAAM,aAAa,EAAE,KAAK,SAAS,CAAC;","names":[]}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * The embedder seam (ADR 01020). Platform-neutral and dependency-free, so the
3
+ * runtime and the CLI can both speak it without either pulling in a model.
4
+ *
5
+ * Everything that computes embeddings — the local transformers.js embedder, the
6
+ * deterministic mock, or a host's own — implements `Embedder`.
7
+ */
8
+ interface Embedder {
9
+ /** Stable identity of the model; recorded in the sidecar header. */
10
+ readonly model: string;
11
+ /** Weight quantization the vectors were produced under. */
12
+ readonly dtype: string;
13
+ /** Output dimensions. Never assumed by callers — always read from here. */
14
+ readonly dims: number;
15
+ /**
16
+ * Embed one text. Single-text rather than batched **on purpose**: float
17
+ * addition is not associative, so a batched vector depends on what it was
18
+ * batched with, and adding one document would perturb its neighbours' vectors
19
+ * (ADR 01020). Determinism is worth more here than throughput.
20
+ */
21
+ embed(text: string): Promise<Float32Array>;
22
+ }
23
+ /**
24
+ * A model's quirks, applied on both the build and query sides so a user cannot
25
+ * get them wrong. Some models require a prefix on queries and/or passages and
26
+ * degrade *silently* without it.
27
+ */
28
+ interface ModelProfile {
29
+ /** Hugging Face repo id. */
30
+ id: string;
31
+ /** Prepended when embedding a search query. */
32
+ queryPrefix?: string;
33
+ /** Prepended when embedding indexed content. */
34
+ passagePrefix?: string;
35
+ /** Human note surfaced in docs and `--help`. */
36
+ note?: string;
37
+ }
38
+ /** What a text is being embedded *for* — decides which prefix applies. */
39
+ type EmbedRole = "query" | "passage";
40
+ declare const MODEL_PROFILES: readonly ModelProfile[];
41
+ declare const DEFAULT_MODEL: string;
42
+ /** The profile for a model id, or a bare profile when it is not a tested one. */
43
+ declare function profileFor(id: string): ModelProfile;
44
+ /** Apply the model's prefix convention for the role, if it has one. */
45
+ declare function withPrefix(profile: ModelProfile, role: EmbedRole, text: string): string;
46
+
47
+ /**
48
+ * A built feature-extraction pipeline, as this module calls it.
49
+ *
50
+ * One text, mean-pooled and normalized (disciplines 2 and 3 above), returning
51
+ * something array-like of numbers. `Tensor` carries far more than that; naming
52
+ * only what is read keeps the peer's real type out of the build.
53
+ */
54
+ type FeatureExtractionPipeline = (text: string, options: {
55
+ pooling: "mean";
56
+ normalize: boolean;
57
+ }) => Promise<{
58
+ data: ArrayLike<number>;
59
+ }>;
60
+ /**
61
+ * The slice of `@huggingface/transformers` this module touches.
62
+ *
63
+ * The package is an **optional peer**, so importing its types would make it a
64
+ * hard dependency for everyone — the thing the dynamic import exists to avoid.
65
+ * Declaring the used surface here instead keeps the boundary narrow rather
66
+ * than untyped: three members, each one a line the code below actually runs.
67
+ * `env` is optional all the way down because the Node build's
68
+ * `backends.onnx.wasm` may be absent or a stub, which is the mistake ADR 01025
69
+ * documents.
70
+ */
71
+ interface TransformersModule {
72
+ env?: {
73
+ backends?: {
74
+ onnx?: {
75
+ wasm?: {
76
+ numThreads?: number;
77
+ };
78
+ };
79
+ };
80
+ };
81
+ pipeline: (task: "feature-extraction", model: string, options: {
82
+ device?: string;
83
+ dtype: string;
84
+ }) => Promise<FeatureExtractionPipeline>;
85
+ }
86
+ interface LocalEmbedderOptions {
87
+ /** Hugging Face model id. Any id is accepted; the tested set is documented. */
88
+ model?: string;
89
+ /** Weight quantization. Default `q8` — see discipline 2 above. */
90
+ dtype?: string;
91
+ /**
92
+ * ONNX execution provider. **Omitted by default on purpose** — the accepted
93
+ * values differ by platform (Node: `dml | webgpu | cpu`; browser: `webgpu |
94
+ * wasm`), so any hardcoded value throws somewhere. Pass one only if you know
95
+ * which platform this embedder runs on.
96
+ */
97
+ device?: string;
98
+ /**
99
+ * What these embeddings are for. Decides which prefix convention applies for
100
+ * models that need one. Default "passage" (indexing); the query side passes
101
+ * "query".
102
+ */
103
+ role?: EmbedRole;
104
+ /**
105
+ * Inject the transformers.js module (tests, or a host that already imported
106
+ * it). Absent, it is imported dynamically.
107
+ */
108
+ transformers?: TransformersModule;
109
+ }
110
+ /** Thrown when the optional peer is not installed, with the fix in the message. */
111
+ declare class EmbedderUnavailableError extends Error {
112
+ constructor(detail: string);
113
+ }
114
+ /**
115
+ * Create an embedder backed by a local model.
116
+ *
117
+ * **Constructing it is the expensive step**: `pipeline()` fetches the model's
118
+ * config and weights (tens of megabytes on a cold cache) and builds the ONNX
119
+ * session before returning, so callers that may not need to embed anything —
120
+ * `manni graph embed` on an all-cache-hit run — should construct it lazily.
121
+ */
122
+ declare function createLocalEmbedder(options?: LocalEmbedderOptions): Promise<Embedder>;
123
+
124
+ /**
125
+ * A deterministic offline embedder (ADR 01020) — the counterpart to
126
+ * `MockProvider` for the LLM seam.
127
+ *
128
+ * CI must stay hermetic: real embedding downloads tens of megabytes of model
129
+ * weights, which the no-network rule forbids. This produces vectors from a hash
130
+ * of the text, so they are stable across runs, machines, and platforms — which
131
+ * makes the `vectors.bin` golden and the double-build determinism gate possible
132
+ * without a model.
133
+ *
134
+ * The vectors carry no semantics. They are useful for testing plumbing,
135
+ * ordering, and refusal paths — never for judging retrieval quality.
136
+ *
137
+ * Platform-neutral: no `node:` imports, no dependencies.
138
+ */
139
+
140
+ interface MockEmbedderOptions {
141
+ /** Output dimensions. Default 8 — small keeps fixtures readable. */
142
+ dims?: number;
143
+ /** Reported model id. Default "mock". */
144
+ model?: string;
145
+ /** Reported dtype. Default "mock". */
146
+ dtype?: string;
147
+ }
148
+ /**
149
+ * Build a deterministic embedder. The same text always yields the same vector;
150
+ * different texts yield different ones with high probability.
151
+ */
152
+ declare function createMockEmbedder(options?: MockEmbedderOptions): Embedder;
153
+
154
+ export { DEFAULT_MODEL, type EmbedRole, type Embedder, EmbedderUnavailableError, type LocalEmbedderOptions, MODEL_PROFILES, type MockEmbedderOptions, type ModelProfile, createLocalEmbedder, createMockEmbedder, profileFor, withPrefix };
@@ -0,0 +1,137 @@
1
+ // src/shared/errors.ts
2
+ function errorMessage(err) {
3
+ if (err instanceof Error) return err.message;
4
+ if (typeof err === "string") return err;
5
+ if (typeof err === "number" || typeof err === "bigint") return String(err);
6
+ if (typeof err === "object" && err !== null && "message" in err && typeof err.message === "string") {
7
+ return err.message;
8
+ }
9
+ try {
10
+ const json = JSON.stringify(err);
11
+ if (json !== void 0) return json;
12
+ } catch {
13
+ }
14
+ return String(err);
15
+ }
16
+
17
+ // src/graph/embed/types.ts
18
+ var DEFAULT_PROFILE = {
19
+ id: "onnx-community/granite-embedding-small-english-r2-ONNX",
20
+ note: "Default. 384-d, 8192-token context so sections are never truncated; no prefixes."
21
+ };
22
+ var MODEL_PROFILES = [
23
+ DEFAULT_PROFILE,
24
+ {
25
+ id: "Xenova/gte-small",
26
+ note: "Lighter (~34 MB). 384-d, 512-token context; no prefixes."
27
+ },
28
+ {
29
+ id: "Xenova/bge-small-en-v1.5",
30
+ queryPrefix: "Represent this sentence for searching relevant passages: ",
31
+ note: "384-d. Needs a query prefix \u2014 applied automatically here, since omitting it degrades retrieval silently."
32
+ },
33
+ {
34
+ id: "Xenova/all-MiniLM-L6-v2",
35
+ note: "Smallest (~23 MB) but truncates at 256 wordpieces (~190 words), so long sections lose their tail."
36
+ }
37
+ ];
38
+ var DEFAULT_MODEL = DEFAULT_PROFILE.id;
39
+ function profileFor(id) {
40
+ return MODEL_PROFILES.find((p) => p.id === id) ?? { id };
41
+ }
42
+ function withPrefix(profile, role, text) {
43
+ const prefix = role === "query" ? profile.queryPrefix : profile.passagePrefix;
44
+ return prefix ? `${prefix}${text}` : text;
45
+ }
46
+
47
+ // src/graph/embed/local.ts
48
+ var EmbedderUnavailableError = class extends Error {
49
+ constructor(detail) {
50
+ super(
51
+ `Local embeddings need @huggingface/transformers, which is an optional peer dependency.
52
+ Install it: npm install @huggingface/transformers
53
+ (${detail})`
54
+ );
55
+ this.name = "EmbedderUnavailableError";
56
+ }
57
+ };
58
+ async function loadTransformers(injected) {
59
+ if (injected) return injected;
60
+ try {
61
+ return await import("@huggingface/transformers");
62
+ } catch (e) {
63
+ throw new EmbedderUnavailableError(
64
+ errorMessage(e)
65
+ );
66
+ }
67
+ }
68
+ async function createLocalEmbedder(options = {}) {
69
+ const model = options.model ?? DEFAULT_MODEL;
70
+ const dtype = options.dtype ?? "q8";
71
+ const role = options.role ?? "passage";
72
+ const profile = profileFor(model);
73
+ const transformers = await loadTransformers(options.transformers);
74
+ if (transformers.env?.backends?.onnx?.wasm) {
75
+ transformers.env.backends.onnx.wasm.numThreads = 1;
76
+ }
77
+ const extractor = await transformers.pipeline("feature-extraction", model, {
78
+ // No `device`: its accepted values are disjoint across platforms, so any
79
+ // hardcoded choice throws on one of them (ADR 01025).
80
+ ...options.device === void 0 ? {} : { device: options.device },
81
+ dtype
82
+ });
83
+ let dims = 0;
84
+ return {
85
+ model,
86
+ dtype,
87
+ get dims() {
88
+ return dims;
89
+ },
90
+ async embed(text) {
91
+ const output = await extractor(withPrefix(profile, role, text), {
92
+ pooling: "mean",
93
+ normalize: true
94
+ });
95
+ const data = Float32Array.from(output.data);
96
+ dims = data.length;
97
+ return data;
98
+ }
99
+ };
100
+ }
101
+
102
+ // src/graph/embed/mock.ts
103
+ function fnv1a(text, seed) {
104
+ let hash = (2166136261 ^ seed) >>> 0;
105
+ for (let i = 0; i < text.length; i++) {
106
+ hash = (hash ^ text.charCodeAt(i)) >>> 0;
107
+ hash = hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)) >>> 0;
108
+ }
109
+ return hash >>> 0;
110
+ }
111
+ function createMockEmbedder(options = {}) {
112
+ const dims = options.dims ?? 8;
113
+ const model = options.model ?? "mock";
114
+ const dtype = options.dtype ?? "mock";
115
+ return {
116
+ model,
117
+ dtype,
118
+ dims,
119
+ embed(text) {
120
+ const vector = new Float32Array(dims);
121
+ for (let i = 0; i < dims; i++) {
122
+ vector[i] = fnv1a(text, i) / 4294967296 * 2 - 1;
123
+ }
124
+ return Promise.resolve(vector);
125
+ }
126
+ };
127
+ }
128
+ export {
129
+ DEFAULT_MODEL,
130
+ EmbedderUnavailableError,
131
+ MODEL_PROFILES,
132
+ createLocalEmbedder,
133
+ createMockEmbedder,
134
+ profileFor,
135
+ withPrefix
136
+ };
137
+ //# sourceMappingURL=embed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shared/errors.ts","../../src/graph/embed/types.ts","../../src/graph/embed/local.ts","../../src/graph/embed/mock.ts"],"sourcesContent":["/**\n * The error every tool under the umbrella throws for an operational failure.\n *\n * Each tool keeps its own subclass (`DocmetaError`, …) so its tests and\n * library callers can match on it. The base is what the shared bin runner\n * matches on: a `ToolError` is reported as one line and exit 2, anything else\n * is a bug and keeps its stack trace.\n */\nexport class ToolError extends Error {\n /** clig.dev: 2 is operational/usage; 1 is reserved for findings. */\n readonly exitCode: number = 2;\n\n constructor(message: string) {\n super(message);\n this.name = \"ToolError\";\n }\n}\n\n/**\n * The message a caught value carries, whatever was thrown.\n *\n * JavaScript can throw anything, so `(err as Error).message` is `undefined`\n * for a thrown string, number or `null`, and \"undefined\" is what the user\n * reads. An Error, or anything with a string `message`, gives that message.\n * A string is its own message, and so is a number or a bigint, as `String()`\n * writes it: JSON would turn `NaN` and the infinities into `null`. Anything\n * else gives its JSON text, or its `String()` form when it will not\n * serialise.\n */\nexport function errorMessage(err: unknown): string {\n if (err instanceof Error) return err.message;\n if (typeof err === \"string\") return err;\n // Before the JSON step, which writes NaN and the infinities as \"null\" and\n // throws on a bigint.\n if (typeof err === \"number\" || typeof err === \"bigint\") return String(err);\n if (\n typeof err === \"object\" &&\n err !== null &&\n \"message\" in err &&\n typeof err.message === \"string\"\n ) {\n return err.message;\n }\n try {\n // Typed as returning a string, but `undefined`, a function or a symbol\n // gives `undefined`.\n const json = JSON.stringify(err) as string | undefined;\n if (json !== undefined) return json;\n } catch {\n // A cycle, or a bigint inside an object: fall through to the one form\n // that always exists.\n }\n return String(err);\n}\n","/**\n * The embedder seam (ADR 01020). Platform-neutral and dependency-free, so the\n * runtime and the CLI can both speak it without either pulling in a model.\n *\n * Everything that computes embeddings — the local transformers.js embedder, the\n * deterministic mock, or a host's own — implements `Embedder`.\n */\n\nexport interface Embedder {\n /** Stable identity of the model; recorded in the sidecar header. */\n readonly model: string;\n /** Weight quantization the vectors were produced under. */\n readonly dtype: string;\n /** Output dimensions. Never assumed by callers — always read from here. */\n readonly dims: number;\n /**\n * Embed one text. Single-text rather than batched **on purpose**: float\n * addition is not associative, so a batched vector depends on what it was\n * batched with, and adding one document would perturb its neighbours' vectors\n * (ADR 01020). Determinism is worth more here than throughput.\n */\n embed(text: string): Promise<Float32Array>;\n}\n\n/**\n * A model's quirks, applied on both the build and query sides so a user cannot\n * get them wrong. Some models require a prefix on queries and/or passages and\n * degrade *silently* without it.\n */\nexport interface ModelProfile {\n /** Hugging Face repo id. */\n id: string;\n /** Prepended when embedding a search query. */\n queryPrefix?: string;\n /** Prepended when embedding indexed content. */\n passagePrefix?: string;\n /** Human note surfaced in docs and `--help`. */\n note?: string;\n}\n\n/** What a text is being embedded *for* — decides which prefix applies. */\nexport type EmbedRole = \"query\" | \"passage\";\n\n/**\n * Models dockg has been tested against. This is the *tested* set, not the\n * permitted set: `embed.model` accepts any id, and an unknown one is used\n * as-is with no prefixes.\n */\n/**\n * The default, named rather than found at index 0: `DEFAULT_MODEL` below then\n * reads it directly instead of asserting that the list has a first element.\n */\nconst DEFAULT_PROFILE: ModelProfile = {\n id: \"onnx-community/granite-embedding-small-english-r2-ONNX\",\n note: \"Default. 384-d, 8192-token context so sections are never truncated; no prefixes.\",\n};\n\nexport const MODEL_PROFILES: readonly ModelProfile[] = [\n DEFAULT_PROFILE,\n {\n id: \"Xenova/gte-small\",\n note: \"Lighter (~34 MB). 384-d, 512-token context; no prefixes.\",\n },\n {\n id: \"Xenova/bge-small-en-v1.5\",\n queryPrefix: \"Represent this sentence for searching relevant passages: \",\n note: \"384-d. Needs a query prefix — applied automatically here, since omitting it degrades retrieval silently.\",\n },\n {\n id: \"Xenova/all-MiniLM-L6-v2\",\n note: \"Smallest (~23 MB) but truncates at 256 wordpieces (~190 words), so long sections lose their tail.\",\n },\n];\n\nexport const DEFAULT_MODEL = DEFAULT_PROFILE.id;\n\n/** The profile for a model id, or a bare profile when it is not a tested one. */\nexport function profileFor(id: string): ModelProfile {\n return MODEL_PROFILES.find((p) => p.id === id) ?? { id };\n}\n\n/** Apply the model's prefix convention for the role, if it has one. */\nexport function withPrefix(\n profile: ModelProfile,\n role: EmbedRole,\n text: string,\n): string {\n const prefix = role === \"query\" ? profile.queryPrefix : profile.passagePrefix;\n return prefix ? `${prefix}${text}` : text;\n}\n","/**\n * The local embedder ([ADR 01020](../../adrs/01020-local-embeddings.md), corrected\n * by [ADR 01025](../../adrs/01025-embedder-cross-platform-reality.md)).\n *\n * Node and the browser **do not** run the same implementation, and cannot be made\n * to. transformers.js v4 takes disjoint `device` vocabularies per platform — Node\n * accepts `dml | webgpu | cpu`, the browser accepts `webgpu | wasm` — so no single\n * value selects one backend everywhere. An earlier version of this file forced\n * `device: \"wasm\"`, which threw on every real Node run; see ADR 01025 for the\n * measurements.\n *\n * What that costs, measured on granite q8: Node native vs browser WASM agree to\n * cosine 0.999914 (max component diff 2.2e-3). dockg's guarantee is therefore\n * **ranking agreement**, gated by `test/real/cross-platform.mjs`, not float\n * equality.\n *\n * Three disciplines are real and pinned here:\n *\n * 1. `numThreads = 1` — binds on the WASM side, where the default follows\n * `hardwareConcurrency` and would vary reduction splits per machine. Inert\n * on the Node side, whose env object does not carry it.\n * 2. `dtype: \"q8\"` — int8 GEMM accumulates in int32, and integer addition is\n * associative.\n * 3. One text per call — a batched vector depends on what it was batched with,\n * so adding a document would perturb its neighbours.\n *\n * `device` is deliberately **not** passed: transformers.js then picks its platform\n * default, which in Node is bit-identical to an explicit `cpu` (measured) and in\n * the browser is WASM. A caller wanting `webgpu` can pass it.\n *\n * `@huggingface/transformers` is an **optional peer dependency**, imported\n * dynamically: it hard-depends on both ONNX runtimes plus native `sharp`, and\n * most dockg users never enable embeddings. Behind this subpath, a consumer who\n * never imports `@hawkeyexl/manni/graph/embed` never resolves it.\n */\nimport { errorMessage } from \"../../shared/errors.js\";\nimport {\n profileFor,\n withPrefix,\n type EmbedRole,\n type Embedder,\n type ModelProfile,\n} from \"./types.js\";\nimport { DEFAULT_MODEL } from \"./types.js\";\n\n/**\n * A built feature-extraction pipeline, as this module calls it.\n *\n * One text, mean-pooled and normalized (disciplines 2 and 3 above), returning\n * something array-like of numbers. `Tensor` carries far more than that; naming\n * only what is read keeps the peer's real type out of the build.\n */\nexport type FeatureExtractionPipeline = (\n text: string,\n options: { pooling: \"mean\"; normalize: boolean },\n) => Promise<{ data: ArrayLike<number> }>;\n\n/**\n * The slice of `@huggingface/transformers` this module touches.\n *\n * The package is an **optional peer**, so importing its types would make it a\n * hard dependency for everyone — the thing the dynamic import exists to avoid.\n * Declaring the used surface here instead keeps the boundary narrow rather\n * than untyped: three members, each one a line the code below actually runs.\n * `env` is optional all the way down because the Node build's\n * `backends.onnx.wasm` may be absent or a stub, which is the mistake ADR 01025\n * documents.\n */\nexport interface TransformersModule {\n env?: {\n backends?: {\n onnx?: { wasm?: { numThreads?: number } };\n };\n };\n pipeline: (\n task: \"feature-extraction\",\n model: string,\n options: { device?: string; dtype: string },\n ) => Promise<FeatureExtractionPipeline>;\n}\n\nexport interface LocalEmbedderOptions {\n /** Hugging Face model id. Any id is accepted; the tested set is documented. */\n model?: string;\n /** Weight quantization. Default `q8` — see discipline 2 above. */\n dtype?: string;\n /**\n * ONNX execution provider. **Omitted by default on purpose** — the accepted\n * values differ by platform (Node: `dml | webgpu | cpu`; browser: `webgpu |\n * wasm`), so any hardcoded value throws somewhere. Pass one only if you know\n * which platform this embedder runs on.\n */\n device?: string;\n /**\n * What these embeddings are for. Decides which prefix convention applies for\n * models that need one. Default \"passage\" (indexing); the query side passes\n * \"query\".\n */\n role?: EmbedRole;\n /**\n * Inject the transformers.js module (tests, or a host that already imported\n * it). Absent, it is imported dynamically.\n */\n transformers?: TransformersModule;\n}\n\n/** Thrown when the optional peer is not installed, with the fix in the message. */\nexport class EmbedderUnavailableError extends Error {\n constructor(detail: string) {\n super(\n `Local embeddings need @huggingface/transformers, which is an optional peer dependency.\\n` +\n ` Install it: npm install @huggingface/transformers\\n` +\n ` (${detail})`,\n );\n this.name = \"EmbedderUnavailableError\";\n }\n}\n\nasync function loadTransformers(\n injected?: TransformersModule,\n): Promise<TransformersModule> {\n if (injected) return injected;\n try {\n // A literal specifier, so a consumer's bundler can resolve and chunk it\n // normally when they *have* opted in. Nothing is lost by leaving it\n // untyped: this boundary is untyped by design (see the note above), and\n // the missing-package path is what the catch handles.\n //\n // `@ts-ignore`, not `@ts-expect-error`, precisely because whether this\n // line errors depends on whether the optional peer happens to be\n // installed. `@ts-expect-error` fails with TS2578 (\"unused directive\") the\n // moment someone follows the README and installs it — which is exactly\n // what the `embed-real` CI job does, and how this was caught.\n // @ts-ignore optional peer dependency, may or may not be installed\n return (await import(\"@huggingface/transformers\")) as TransformersModule;\n } catch (e) {\n throw new EmbedderUnavailableError(\n errorMessage(e),\n );\n }\n}\n\n/**\n * Create an embedder backed by a local model.\n *\n * **Constructing it is the expensive step**: `pipeline()` fetches the model's\n * config and weights (tens of megabytes on a cold cache) and builds the ONNX\n * session before returning, so callers that may not need to embed anything —\n * `manni graph embed` on an all-cache-hit run — should construct it lazily.\n */\nexport async function createLocalEmbedder(\n options: LocalEmbedderOptions = {},\n): Promise<Embedder> {\n const model = options.model ?? DEFAULT_MODEL;\n const dtype = options.dtype ?? \"q8\";\n const role: EmbedRole = options.role ?? \"passage\";\n const profile: ModelProfile = profileFor(model);\n\n const transformers = await loadTransformers(options.transformers);\n\n // Discipline 1, set before the pipeline is built. Optional-chained because\n // the Node build's `env.backends.onnx.wasm` may be absent or a stub — this\n // pins the browser side, and asserting it pinned both is the mistake ADR\n // 01025 documents.\n if (transformers.env?.backends?.onnx?.wasm) {\n transformers.env.backends.onnx.wasm.numThreads = 1;\n }\n\n const extractor = await transformers.pipeline(\"feature-extraction\", model, {\n // No `device`: its accepted values are disjoint across platforms, so any\n // hardcoded choice throws on one of them (ADR 01025).\n ...(options.device === undefined ? {} : { device: options.device }),\n dtype,\n });\n\n let dims = 0;\n\n return {\n model,\n dtype,\n get dims() {\n return dims;\n },\n async embed(text: string): Promise<Float32Array> {\n // Discipline 3: one text per call, never a batch.\n const output = await extractor(withPrefix(profile, role, text), {\n pooling: \"mean\",\n normalize: true,\n });\n const data = Float32Array.from(output.data);\n dims = data.length;\n return data;\n },\n };\n}\n","/**\n * A deterministic offline embedder (ADR 01020) — the counterpart to\n * `MockProvider` for the LLM seam.\n *\n * CI must stay hermetic: real embedding downloads tens of megabytes of model\n * weights, which the no-network rule forbids. This produces vectors from a hash\n * of the text, so they are stable across runs, machines, and platforms — which\n * makes the `vectors.bin` golden and the double-build determinism gate possible\n * without a model.\n *\n * The vectors carry no semantics. They are useful for testing plumbing,\n * ordering, and refusal paths — never for judging retrieval quality.\n *\n * Platform-neutral: no `node:` imports, no dependencies.\n */\nimport type { Embedder } from \"./types.js\";\n\n/** FNV-1a, 32-bit. Small, fast, and identical everywhere. */\nfunction fnv1a(text: string, seed: number): number {\n let hash = (0x81_1c_9d_c5 ^ seed) >>> 0;\n for (let i = 0; i < text.length; i++) {\n hash = (hash ^ text.charCodeAt(i)) >>> 0;\n // hash *= 16777619, via shifts to stay in 32-bit integer range.\n hash =\n (hash +\n ((hash << 1) +\n (hash << 4) +\n (hash << 7) +\n (hash << 8) +\n (hash << 24))) >>>\n 0;\n }\n return hash >>> 0;\n}\n\nexport interface MockEmbedderOptions {\n /** Output dimensions. Default 8 — small keeps fixtures readable. */\n dims?: number;\n /** Reported model id. Default \"mock\". */\n model?: string;\n /** Reported dtype. Default \"mock\". */\n dtype?: string;\n}\n\n/**\n * Build a deterministic embedder. The same text always yields the same vector;\n * different texts yield different ones with high probability.\n */\nexport function createMockEmbedder(\n options: MockEmbedderOptions = {},\n): Embedder {\n const dims = options.dims ?? 8;\n const model = options.model ?? \"mock\";\n const dtype = options.dtype ?? \"mock\";\n\n return {\n model,\n dtype,\n dims,\n embed(text: string): Promise<Float32Array> {\n const vector = new Float32Array(dims);\n for (let i = 0; i < dims; i++) {\n // Map each hash into [-1, 1) so vectors spread over the sphere rather\n // than crowding one orthant.\n vector[i] = (fnv1a(text, i) / 0x1_00_00_00_00) * 2 - 1;\n }\n return Promise.resolve(vector);\n },\n };\n}\n"],"mappings":"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","names":[]}