@tangleai/pipeline 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @tangleai/pipeline
2
+
3
+ ## 0.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Establish the coordinated 0.20.0 release with JavaScript and TypeScript declaration distributions, preserved public subpaths and JSON schemas, and verified Node and Bun consumers. Use published JarenJS 0.83.3 fixes without a consumer installation patch. Prepare versions before release commits, verify locally, push directly to main and publish CI-verified tarballs. Deploy the website independently from local Tangle workspace source with JarenJS packages from npm, verifying dependency sources and the live commit.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @tangleai/core@0.20.0
13
+ - @tangleai/memory@0.20.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joham (jklarenbeek@gmail.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @tangleai/pipeline
2
+
3
+ Tangle AI pipeline — the memory loop as a jaren-dag document, executed by @jarenjs/flow, projected to mermaid for display
4
+
5
+ Install with `npm install @tangleai/pipeline`. The npm distribution provides ESM JavaScript, TypeScript declarations, and the documented package subpaths for Node 24 and Bun 1.4 or newer.
6
+
7
+ See the [Tangle documentation](https://github.com/jklarenbeek/tangleai#readme) for architecture, examples, and runtime requirements. All public Tangle packages use one coordinated version.
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@tangleai/pipeline",
3
+ "version": "0.20.0",
4
+ "description": "Tangle AI pipeline — the memory loop as a jaren-dag document, executed by @jarenjs/flow, projected to mermaid for display",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./src/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.js",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./dag": {
15
+ "types": "./src/dag.d.ts",
16
+ "import": "./src/dag.js",
17
+ "default": "./src/dag.js"
18
+ },
19
+ "./run": {
20
+ "types": "./src/run.d.ts",
21
+ "import": "./src/run.js",
22
+ "default": "./src/run.js"
23
+ },
24
+ "./mermaid": {
25
+ "types": "./src/mermaid.d.ts",
26
+ "import": "./src/mermaid.js",
27
+ "default": "./src/mermaid.js"
28
+ },
29
+ "./standins": {
30
+ "types": "./src/standins.d.ts",
31
+ "import": "./src/standins.js",
32
+ "default": "./src/standins.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "engines": {
37
+ "node": ">=24"
38
+ },
39
+ "sideEffects": false,
40
+ "dependencies": {
41
+ "@tangleai/core": "^0.20.0",
42
+ "@tangleai/memory": "^0.20.0",
43
+ "@jarenjs/ai": "0.83.3",
44
+ "@jarenjs/flow": "0.83.3",
45
+ "@jarenjs/json": "0.83.3",
46
+ "@jarenjs/mermaid": "0.83.3"
47
+ },
48
+ "private": false,
49
+ "types": "./src/index.d.ts",
50
+ "files": [
51
+ "src/**/*.js",
52
+ "src/**/*.d.ts",
53
+ "schemas/**/*.json",
54
+ "README.md",
55
+ "LICENSE",
56
+ "CHANGELOG.md"
57
+ ],
58
+ "publishConfig": {
59
+ "access": "public",
60
+ "registry": "https://registry.npmjs.org/"
61
+ },
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "git+https://github.com/jklarenbeek/tangleai.git",
65
+ "directory": "packages/pipeline"
66
+ }
67
+ }
package/src/dag.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The Tangle memory loop as a `jaren-dag` 0.1 document.
3
+ *
4
+ * This is the SAME loop the walking skeleton runs inline, lifted into a
5
+ * flow document so that (a) @jarenjs/flow executes it with real
6
+ * per-node observability (`onNode` → the run log → the desktop DAG
7
+ * page, current and historical), and (b) @jarenjs/mermaid can project
8
+ * the topology for display from the document itself — the picture the
9
+ * user sees IS the thing that runs, not an illustration of it.
10
+ *
11
+ * Stage order is the twice-learned design rule: the novelty gate only
12
+ * filters near-verbatim repeats, contradictions are resolved BEFORE
13
+ * crystallization (a contradiction is ~0.95-similar to what it
14
+ * contradicts), and the crystallizer merges only what survived with its
15
+ * meaning intact.
16
+ *
17
+ * Each stage hands its summary to the `report` output node through a
18
+ * ported edge; the unported spine carries the data each next stage
19
+ * needs. Handlers are injected at compile time (`createPipeline`) — the
20
+ * document names them, the host provides them, jarenjs's DI rule.
21
+ */
22
+ export declare const PIPELINE_DAG: {
23
+ $dag: string;
24
+ nodes: {
25
+ observations: {
26
+ kind: string;
27
+ };
28
+ embed: {
29
+ kind: string;
30
+ run: string;
31
+ };
32
+ novelty: {
33
+ kind: string;
34
+ run: string;
35
+ };
36
+ contradiction: {
37
+ kind: string;
38
+ run: string;
39
+ };
40
+ crystallize: {
41
+ kind: string;
42
+ run: string;
43
+ };
44
+ report: {
45
+ kind: string;
46
+ };
47
+ };
48
+ edges: ({
49
+ from: string;
50
+ to: string;
51
+ port?: undefined;
52
+ } | {
53
+ from: string;
54
+ to: string;
55
+ port: string;
56
+ })[];
57
+ };
58
+ /** The node ids, in pipeline order — what the UI renders as the status strip. */
59
+ export declare const PIPELINE_NODES: readonly ["observations", "embed", "novelty", "contradiction", "crystallize", "report"];
package/src/dag.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The Tangle memory loop as a `jaren-dag` 0.1 document.
3
+ *
4
+ * This is the SAME loop the walking skeleton runs inline, lifted into a
5
+ * flow document so that (a) @jarenjs/flow executes it with real
6
+ * per-node observability (`onNode` → the run log → the desktop DAG
7
+ * page, current and historical), and (b) @jarenjs/mermaid can project
8
+ * the topology for display from the document itself — the picture the
9
+ * user sees IS the thing that runs, not an illustration of it.
10
+ *
11
+ * Stage order is the twice-learned design rule: the novelty gate only
12
+ * filters near-verbatim repeats, contradictions are resolved BEFORE
13
+ * crystallization (a contradiction is ~0.95-similar to what it
14
+ * contradicts), and the crystallizer merges only what survived with its
15
+ * meaning intact.
16
+ *
17
+ * Each stage hands its summary to the `report` output node through a
18
+ * ported edge; the unported spine carries the data each next stage
19
+ * needs. Handlers are injected at compile time (`createPipeline`) — the
20
+ * document names them, the host provides them, jarenjs's DI rule.
21
+ */
22
+ export const PIPELINE_DAG = {
23
+ $dag: '0.1',
24
+ nodes: {
25
+ observations: { kind: 'input' },
26
+ embed: { kind: 'task', run: 'embed' },
27
+ novelty: { kind: 'task', run: 'novelty' },
28
+ contradiction: { kind: 'task', run: 'contradiction' },
29
+ crystallize: { kind: 'task', run: 'crystallize' },
30
+ report: { kind: 'output' },
31
+ },
32
+ edges: [
33
+ { from: 'observations', to: 'embed' },
34
+ { from: 'embed', to: 'novelty' },
35
+ { from: 'novelty', to: 'contradiction' },
36
+ { from: 'contradiction', to: 'crystallize' },
37
+ { from: 'embed', to: 'report', port: 'embed' },
38
+ { from: 'novelty', to: 'report', port: 'novelty' },
39
+ { from: 'contradiction', to: 'report', port: 'contradiction' },
40
+ { from: 'crystallize', to: 'report', port: 'crystallize' },
41
+ ],
42
+ };
43
+ /** The node ids, in pipeline order — what the UI renders as the status strip. */
44
+ export const PIPELINE_NODES = ['observations', 'embed', 'novelty', 'contradiction', 'crystallize', 'report'];
package/src/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /** @tangleai/pipeline barrel. */
2
+ export { PIPELINE_DAG, PIPELINE_NODES } from './dag.ts';
3
+ export { createPipeline, DEFAULT_THRESHOLDS } from './run.ts';
4
+ export type { Pipeline, PipelineOptions, PipelineReport, PipelineThresholds, DagNodeRecord, } from './run.ts';
5
+ export { dagToMermaid } from './mermaid.ts';
6
+ export { createOfflineEmbedder, OFFLINE_EMBEDDER_DIMS, numericContrastJudge } from './standins.ts';
7
+ export type { Judge } from './standins.ts';
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /** @tangleai/pipeline barrel. */
2
+ export { PIPELINE_DAG, PIPELINE_NODES } from "./dag.js";
3
+ export { createPipeline, DEFAULT_THRESHOLDS } from "./run.js";
4
+ export { dagToMermaid } from "./mermaid.js";
5
+ export { createOfflineEmbedder, OFFLINE_EMBEDDER_DIMS, numericContrastJudge } from "./standins.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The pipeline document, projected to Mermaid text.
3
+ *
4
+ * Uses the suite's own projection chain — the `dag-to-flowchart` JSLT
5
+ * stylesheet shipped by @jarenjs/mermaid, `transformJson` from
6
+ * @jarenjs/json, `toMermaid` over a diagram document — so the drawing
7
+ * is DERIVED from the executable document, never hand-drawn. Node
8
+ * statuses are deliberately NOT painted into the diagram; the UI
9
+ * renders them as a strip beside it (data changes fast, topology
10
+ * doesn't).
11
+ */
12
+ export declare function dagToMermaid(doc?: any): string;
package/src/mermaid.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The pipeline document, projected to Mermaid text.
3
+ *
4
+ * Uses the suite's own projection chain — the `dag-to-flowchart` JSLT
5
+ * stylesheet shipped by @jarenjs/mermaid, `transformJson` from
6
+ * @jarenjs/json, `toMermaid` over a diagram document — so the drawing
7
+ * is DERIVED from the executable document, never hand-drawn. Node
8
+ * statuses are deliberately NOT painted into the diagram; the UI
9
+ * renders them as a strip beside it (data changes fast, topology
10
+ * doesn't).
11
+ */
12
+ import { toMermaid, diagramDocument } from '@jarenjs/mermaid';
13
+ import { transformJson } from '@jarenjs/json/jslt';
14
+ import dagToFlowchart from '@jarenjs/mermaid/stylesheets/dag-to-flowchart.jslt.json' with { type: 'json' };
15
+ import { PIPELINE_DAG } from "./dag.js";
16
+ export function dagToMermaid(doc = PIPELINE_DAG) {
17
+ const ast = transformJson(dagToFlowchart, doc);
18
+ return toMermaid(diagramDocument('flowchart', {}, ast, { hash: '0', direction: 'TD', title: null }));
19
+ }
package/src/run.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Compiling and running the pipeline document.
3
+ *
4
+ * `createPipeline` binds the DAG's named task handlers to injected
5
+ * seams — the store (any MemoryStore: in-memory for tests and the pages
6
+ * demo, SQLite in the desktop app), an embedder (the @jarenjs/ai seam:
7
+ * `{ embed, model, dims }` — `createEmbeddingClient` for a wire,
8
+ * `createHashEmbedder` as the offline default), a contradiction judge,
9
+ * a clock, and the tuned thresholds (salvaged memflow defaults). The
10
+ * document stays pure JSON; everything replaceable arrives here.
11
+ *
12
+ * Every vector the embed node writes carries its identity
13
+ * (`embeddedBy: { model, dims }`), the jarenjs rule that lets every
14
+ * later policy refuse to compare vectors from two models.
15
+ *
16
+ * `run()` returns a compact report; the units themselves live in the
17
+ * store, not in the return value. Per-node timing/status records stream
18
+ * through `onNode` exactly as @jarenjs/flow emits them.
19
+ */
20
+ import type { Embedder } from '@jarenjs/ai/embed';
21
+ import { type MemoryStore, type MemoryUnitInput } from '@tangleai/memory';
22
+ import { type Judge } from './standins.ts';
23
+ export interface PipelineThresholds {
24
+ /** Near-verbatim repeat filter; keep HIGH so contradictions survive. */
25
+ novelty?: number;
26
+ /** Similar-pair candidate threshold for the judge. */
27
+ contradiction?: number;
28
+ /** Paraphrase merge threshold. */
29
+ crystallize?: number;
30
+ }
31
+ export interface PipelineOptions {
32
+ store: MemoryStore;
33
+ embedder?: Embedder;
34
+ judge?: Judge;
35
+ now?: () => string;
36
+ thresholds?: PipelineThresholds;
37
+ }
38
+ export interface DagNodeRecord {
39
+ id: string;
40
+ status: 'ok' | 'error' | 'aborted' | 'restored';
41
+ ms: number;
42
+ }
43
+ export interface PipelineReport {
44
+ observations: number;
45
+ embedded: number;
46
+ model: string;
47
+ novelty: {
48
+ admitted: number;
49
+ filtered: number;
50
+ };
51
+ /** Every attempt is judged or failed; every confirmed verdict is applied or skipped. */
52
+ contradiction: {
53
+ attempted: number;
54
+ judged: number;
55
+ judgeFailures: number;
56
+ confirmed: number;
57
+ contradictions: number;
58
+ applicationSkips: number;
59
+ resolutions: string[];
60
+ };
61
+ /** Every planned merge is written or skipped. */
62
+ crystallize: {
63
+ examined: number;
64
+ planned: number;
65
+ merged: number;
66
+ applicationSkips: number;
67
+ };
68
+ memories: {
69
+ live: number;
70
+ total: number;
71
+ };
72
+ }
73
+ export interface Pipeline {
74
+ run(observations: MemoryUnitInput[], options?: {
75
+ signal?: AbortSignal;
76
+ onNode?: (record: DagNodeRecord) => void;
77
+ }): Promise<PipelineReport>;
78
+ }
79
+ export declare const DEFAULT_THRESHOLDS: Required<PipelineThresholds>;
80
+ export declare function createPipeline(options: PipelineOptions): Pipeline;
package/src/run.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Compiling and running the pipeline document.
3
+ *
4
+ * `createPipeline` binds the DAG's named task handlers to injected
5
+ * seams — the store (any MemoryStore: in-memory for tests and the pages
6
+ * demo, SQLite in the desktop app), an embedder (the @jarenjs/ai seam:
7
+ * `{ embed, model, dims }` — `createEmbeddingClient` for a wire,
8
+ * `createHashEmbedder` as the offline default), a contradiction judge,
9
+ * a clock, and the tuned thresholds (salvaged memflow defaults). The
10
+ * document stays pure JSON; everything replaceable arrives here.
11
+ *
12
+ * Every vector the embed node writes carries its identity
13
+ * (`embeddedBy: { model, dims }`), the jarenjs rule that lets every
14
+ * later policy refuse to compare vectors from two models.
15
+ *
16
+ * `run()` returns a compact report; the units themselves live in the
17
+ * store, not in the return value. Per-node timing/status records stream
18
+ * through `onNode` exactly as @jarenjs/flow emits them.
19
+ */
20
+ import { compileDag } from '@jarenjs/flow';
21
+ import { createMemoryUnit, noveltyGate, planContradictionPairs, resolveContradictions, planCrystallization, applyCrystallization, } from '@tangleai/memory';
22
+ import { createOfflineEmbedder, numericContrastJudge } from "./standins.js";
23
+ import { PIPELINE_DAG } from "./dag.js";
24
+ export const DEFAULT_THRESHOLDS = {
25
+ novelty: 0.97,
26
+ contradiction: 0.8,
27
+ crystallize: 0.9,
28
+ };
29
+ export function createPipeline(options) {
30
+ const store = options.store;
31
+ const embedder = options.embedder ?? createOfflineEmbedder();
32
+ const judge = options.judge ?? numericContrastJudge();
33
+ const now = options.now ?? (() => new Date().toISOString());
34
+ const thresholds = { ...DEFAULT_THRESHOLDS, ...options.thresholds };
35
+ const tasks = {
36
+ async embed({ input }, signal) {
37
+ const observations = input;
38
+ const missing = observations.filter((o) => o.embedding === undefined);
39
+ const vectors = missing.length > 0
40
+ ? await embedder.embed(missing.map((o) => o.text), { signal })
41
+ : [];
42
+ // the identity: the embedder's settled width, or the reply's
43
+ const dims = embedder.dims ?? vectors[0]?.length;
44
+ const embeddedBy = dims === undefined ? undefined : { model: embedder.model, dims };
45
+ let next = 0;
46
+ const units = observations.map((o) => {
47
+ if (o.embedding !== undefined)
48
+ return createMemoryUnit({ ...o, confidence: o.confidence ?? 0.5 });
49
+ // the seam answers Float32Array; the record stores plain numbers
50
+ const vector = Array.from(vectors[next++]);
51
+ return createMemoryUnit({ ...o, embedding: vector, embeddedBy, confidence: o.confidence ?? 0.5 });
52
+ });
53
+ return { units, embedded: missing.length, model: embedder.model };
54
+ },
55
+ async novelty({ input }) {
56
+ const units = input.units;
57
+ const existing = await store.list();
58
+ const { novel, filtered } = noveltyGate(units, existing, { threshold: thresholds.novelty });
59
+ for (const unit of novel)
60
+ await store.put(unit);
61
+ return { admitted: novel.length, filtered: filtered.length };
62
+ },
63
+ async contradiction() {
64
+ const units = await store.list();
65
+ const pairs = planContradictionPairs(units, { threshold: thresholds.contradiction });
66
+ const outcome = await resolveContradictions(store, pairs, { now, judge });
67
+ return {
68
+ attempted: outcome.attempted,
69
+ judged: outcome.judged,
70
+ judgeFailures: outcome.judgeFailures,
71
+ confirmed: outcome.confirmed,
72
+ contradictions: outcome.contradictions,
73
+ applicationSkips: outcome.applicationSkips,
74
+ resolutions: outcome.resolutions.map((r) => r.text),
75
+ };
76
+ },
77
+ async crystallize() {
78
+ const plan = planCrystallization(await store.list(), { threshold: thresholds.crystallize });
79
+ const outcome = await applyCrystallization(store, plan, { now });
80
+ return {
81
+ examined: plan.examined,
82
+ planned: outcome.planned,
83
+ merged: outcome.crystallized,
84
+ applicationSkips: outcome.applicationSkips,
85
+ };
86
+ },
87
+ };
88
+ const dag = compileDag(PIPELINE_DAG, { tasks });
89
+ return {
90
+ async run(observations, runOptions = {}) {
91
+ const result = await dag.run(observations, {
92
+ signal: runOptions.signal,
93
+ onNode: runOptions.onNode,
94
+ });
95
+ const all = await store.list();
96
+ const live = all.filter((u) => u.supersededBy === undefined);
97
+ return {
98
+ observations: observations.length,
99
+ embedded: result.embed.embedded,
100
+ model: result.embed.model,
101
+ novelty: result.novelty,
102
+ contradiction: result.contradiction,
103
+ crystallize: result.crystallize,
104
+ memories: { live: live.length, total: all.length },
105
+ };
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The two offline stand-ins — an embedder and a contradiction judge —
3
+ * that let the whole pipeline run with zero network, promoted out of
4
+ * `examples/skeleton.ts` because the desktop app and the pages demo
5
+ * need them as real seams.
6
+ *
7
+ * `createOfflineEmbedder` is @jarenjs/ai's `createHashEmbedder` — the
8
+ * suite's own deterministic hashed-trigram reference, behind the same
9
+ * `{ embed, model, dims }` seam a wire client fills — at the width the
10
+ * pipeline's thresholds were measured against. Measured over the
11
+ * skeleton corpus (2026-08-26, `hash-trigram-<dims>`, cosine), against
12
+ * the defaults novelty 0.97 / crystallize 0.9 / contradiction 0.8:
13
+ *
14
+ * dims repeat paraphrase contradiction max-unrelated
15
+ * 64 0.989 0.931 0.964 0.606 ← every pair on its side of every threshold
16
+ * 128 0.985 0.902 0.954 0.424 ← paraphrase a hair over the 0.9 merge line
17
+ * 256 0.984 0.888 0.941 0.323 ← paraphrase under the merge line: never crystallized
18
+ * 512 0.983 0.884 0.936 0.225
19
+ *
20
+ * 64 — the suite's own default — is the width with margin on all four:
21
+ * wider buckets spread the shared trigrams thinner, so the paraphrase
22
+ * loses its merge long before an unrelated pair threatens the 0.8
23
+ * judge line. It stays demo-grade and lexical — a real model behind
24
+ * the same seam is the upgrade.
25
+ *
26
+ * `numericContrastJudge` flags two records that agree in words but
27
+ * disagree in figures — the rule stand-in for an LLM contradiction
28
+ * judge (`createStructuredOutput` over `contradictionMessages`).
29
+ */
30
+ import { type Embedder } from '@jarenjs/ai/embed';
31
+ import type { MemoryUnit } from '@tangleai/core/schemas/memory';
32
+ import type { ContradictionVerdict } from '@tangleai/memory';
33
+ /** The measured width (see the header). */
34
+ export declare const OFFLINE_EMBEDDER_DIMS = 64;
35
+ /** The offline embedder: the suite's hash reference at the measured width. */
36
+ export declare function createOfflineEmbedder(): Embedder & {
37
+ dims: number;
38
+ };
39
+ export type Judge = (a: MemoryUnit, b: MemoryUnit) => Promise<ContradictionVerdict>;
40
+ /** Same subject, different figures → contradiction; newer text wins. */
41
+ export declare function numericContrastJudge(): Judge;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The two offline stand-ins — an embedder and a contradiction judge —
3
+ * that let the whole pipeline run with zero network, promoted out of
4
+ * `examples/skeleton.ts` because the desktop app and the pages demo
5
+ * need them as real seams.
6
+ *
7
+ * `createOfflineEmbedder` is @jarenjs/ai's `createHashEmbedder` — the
8
+ * suite's own deterministic hashed-trigram reference, behind the same
9
+ * `{ embed, model, dims }` seam a wire client fills — at the width the
10
+ * pipeline's thresholds were measured against. Measured over the
11
+ * skeleton corpus (2026-08-26, `hash-trigram-<dims>`, cosine), against
12
+ * the defaults novelty 0.97 / crystallize 0.9 / contradiction 0.8:
13
+ *
14
+ * dims repeat paraphrase contradiction max-unrelated
15
+ * 64 0.989 0.931 0.964 0.606 ← every pair on its side of every threshold
16
+ * 128 0.985 0.902 0.954 0.424 ← paraphrase a hair over the 0.9 merge line
17
+ * 256 0.984 0.888 0.941 0.323 ← paraphrase under the merge line: never crystallized
18
+ * 512 0.983 0.884 0.936 0.225
19
+ *
20
+ * 64 — the suite's own default — is the width with margin on all four:
21
+ * wider buckets spread the shared trigrams thinner, so the paraphrase
22
+ * loses its merge long before an unrelated pair threatens the 0.8
23
+ * judge line. It stays demo-grade and lexical — a real model behind
24
+ * the same seam is the upgrade.
25
+ *
26
+ * `numericContrastJudge` flags two records that agree in words but
27
+ * disagree in figures — the rule stand-in for an LLM contradiction
28
+ * judge (`createStructuredOutput` over `contradictionMessages`).
29
+ */
30
+ import { createHashEmbedder } from '@jarenjs/ai/embed';
31
+ /** The measured width (see the header). */
32
+ export const OFFLINE_EMBEDDER_DIMS = 64;
33
+ /** The offline embedder: the suite's hash reference at the measured width. */
34
+ export function createOfflineEmbedder() {
35
+ return createHashEmbedder({ dims: OFFLINE_EMBEDDER_DIMS });
36
+ }
37
+ /** Same subject, different figures → contradiction; newer text wins. */
38
+ export function numericContrastJudge() {
39
+ const numbers = (t) => t.match(/\d+(?:\.\d+)?/g)?.join(',') ?? '';
40
+ return async (a, b) => {
41
+ if (numbers(a.text) !== numbers(b.text)) {
42
+ const newer = a.at <= b.at ? b : a;
43
+ return { contradiction: true, reason: 'same subject, different figures', resolution: newer.text };
44
+ }
45
+ return { contradiction: false };
46
+ };
47
+ }