@superinstance/live-canon 0.1.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/README.md +112 -0
- package/index.js +226 -0
- package/package.json +27 -0
- package/test/test.js +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @superinstance/live-canon
|
|
2
|
+
|
|
3
|
+
**Live Canon — read the AI-Writings canon as a navigable cell fabric.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@superinstance/live-canon)
|
|
6
|
+
[](https://live-canon.superinstance.dev)
|
|
7
|
+
[](https://github.com/SuperInstance/quilt-cowboy)
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Live Canon reads the AI-Writings canon (1700+ papers) as a navigable
|
|
12
|
+
cell fabric. Each paper = 1 cell. Each citation = 1 edge. The canon
|
|
13
|
+
exposes 5 novel operations:
|
|
14
|
+
|
|
15
|
+
1. **NAVIGATE** — BFS through citations
|
|
16
|
+
2. **CONFLUENCE** — join 2+ papers, suggest a synthesis
|
|
17
|
+
3. **LINEAGE** — trace a concept (F-number) through time
|
|
18
|
+
4. **GHOST** — find a paper that should exist by shape proximity
|
|
19
|
+
5. **TICK** — re-balance the canon
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @superinstance/live-canon
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
const { LiveCanon } = require('@superinstance/live-canon');
|
|
31
|
+
|
|
32
|
+
const canon = new LiveCanon();
|
|
33
|
+
|
|
34
|
+
// State hash (byte-exact with Python/C/Rust/Verilog/VHDL/JS-Worker)
|
|
35
|
+
console.log(canon.stateHashString); // 0xbf27a3631cdee337
|
|
36
|
+
|
|
37
|
+
// 1. NAVIGATE — BFS from a paper
|
|
38
|
+
const path = canon.navigate(425, 2);
|
|
39
|
+
console.log(`Found ${path.length} cells in the citation graph`);
|
|
40
|
+
|
|
41
|
+
// 2. CONFLUENCE — join 2+ papers
|
|
42
|
+
const synth = canon.confluence([425, 432, 439]);
|
|
43
|
+
console.log(`Suggested: ${synth.suggested_title}`);
|
|
44
|
+
|
|
45
|
+
// 3. LINEAGE — trace F115 through time
|
|
46
|
+
const lineage = canon.lineage(115);
|
|
47
|
+
console.log(`${lineage.length} papers cite F115`);
|
|
48
|
+
|
|
49
|
+
// 4. GHOST — find a paper that should exist
|
|
50
|
+
const ghost = canon.ghost(425, 5);
|
|
51
|
+
console.log(`Top neighbor: ${ghost.neighbors[0].id} (score=${ghost.neighbors[0].score})`);
|
|
52
|
+
|
|
53
|
+
// 5. TICK — re-balance the canon
|
|
54
|
+
console.log(canon.tick());
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Live data
|
|
58
|
+
|
|
59
|
+
The package bundles 9 papers from the polyformalism cascade (F115-F130).
|
|
60
|
+
For the full canon, fetch from the live URL:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const canon = await LiveCanon.fromUrl('https://live-canon.superinstance.dev/api/canon');
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Polyformalism
|
|
67
|
+
|
|
68
|
+
The Live Canon is byte-exact across 6 substrates:
|
|
69
|
+
|
|
70
|
+
| Substrate | Status | State hash |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| Python (this) | reference | `0xbf27a3631cdee337` |
|
|
73
|
+
| JavaScript (npm) | this package | `0xbf27a3631cdee337` |
|
|
74
|
+
| JavaScript (Cloudflare Worker) | live | `0xbf27a3631cdee337` |
|
|
75
|
+
| C99 | `live_canon.c` | `0xbf27a3631cdee337` |
|
|
76
|
+
| Rust | `live-canon` crate | (same) |
|
|
77
|
+
| Verilog-2005 | `live_canon.v` | (same) |
|
|
78
|
+
| VHDL-2008 | `live_canon.vhdl` | (same) |
|
|
79
|
+
|
|
80
|
+
The 16-dial encoding is shared: `num_q = number*131`, `f_q = f*218`,
|
|
81
|
+
`phase_q = phase*218`, `year_q = (year-1970)*546`, `title_lo/hi = FNV-1a(title)`.
|
|
82
|
+
|
|
83
|
+
## Live API
|
|
84
|
+
|
|
85
|
+
The Cloudflare Worker exposes the same operations as a REST API:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
GET https://live-canon.superinstance.dev/api/canon
|
|
89
|
+
GET https://live-canon.superinstance.dev/api/canon/navigate?paper=425&depth=2
|
|
90
|
+
GET https://live-canon.superinstance.dev/api/canon/confluence?papers=425,432,439
|
|
91
|
+
GET https://live-canon.superinstance.dev/api/canon/lineage?f=115
|
|
92
|
+
GET https://live-canon.superinstance.dev/api/canon/ghost?paper=425&k=5
|
|
93
|
+
GET https://live-canon.superinstance.dev/api/canon/tick
|
|
94
|
+
GET https://live-canon.superinstance.dev/api/canon/hash
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Related
|
|
98
|
+
|
|
99
|
+
- **F129 (paper-439)**: The Live Canon: Papers as Cells, Reading as Navigation
|
|
100
|
+
- **F130 (paper-440)**: The Polyformal Live Canon: One Cell, Five Substrates
|
|
101
|
+
- **F131 (paper-441)**: The 6-Package Polyformalism (this package)
|
|
102
|
+
- **GitHub**: github.com/SuperInstance/quilt-live-canon
|
|
103
|
+
- **Live URL**: live-canon.superinstance.dev
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
108
|
+
|
|
109
|
+
## Phase 251 of the polyformalism canon.
|
|
110
|
+
|
|
111
|
+
The cell is the unit. The hash is the address. The chart grows because
|
|
112
|
+
the cowboy rides.
|
package/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// index.js — Live Canon for Node.js
|
|
2
|
+
//
|
|
3
|
+
// Live Canon: read the AI-Writings canon as a navigable cell fabric.
|
|
4
|
+
// 5 novel operations:
|
|
5
|
+
// 1. NAVIGATE - BFS through citations
|
|
6
|
+
// 2. CONFLUENCE - join 2+ papers, suggest synthesis
|
|
7
|
+
// 3. LINEAGE - trace F-number through time
|
|
8
|
+
// 4. GHOST - find paper that should exist by shape proximity
|
|
9
|
+
// 5. TICK - re-balance the canon
|
|
10
|
+
//
|
|
11
|
+
// Polyformal: byte-exact with Python, C99, Rust, Verilog, VHDL.
|
|
12
|
+
// State hash = 0xbf27a3631cdee337 for the 9 bundled papers.
|
|
13
|
+
//
|
|
14
|
+
// Phase 251 of the polyformalism canon.
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
// ===== FNV-1a 64-bit hash (UTF-8 byte-exact with Python) =====
|
|
19
|
+
function fnv1a_64(s) {
|
|
20
|
+
let h = 0xCBF29CE484222325n;
|
|
21
|
+
const bytes = new TextEncoder().encode(s);
|
|
22
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
23
|
+
h ^= BigInt(bytes[i]);
|
|
24
|
+
h = (h * 0x00000100000001B3n) & 0xFFFFFFFFFFFFFFFFn;
|
|
25
|
+
}
|
|
26
|
+
return h;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ===== Cell encoding (16 x Q1.15 dials) =====
|
|
30
|
+
function cellToDials(paper) {
|
|
31
|
+
const year = parseInt(paper.date.substring(0, 4)) || 1970;
|
|
32
|
+
const year_q = (year - 1970) * 546;
|
|
33
|
+
const phase_q = paper.phase * 218;
|
|
34
|
+
const f_q = paper.f_number * 218;
|
|
35
|
+
const n_refs = (paper.ref_papers?.length || 0) + (paper.ref_f_numbers?.length || 0);
|
|
36
|
+
const n_refs_q = Math.min(0x7FFF, n_refs * 256);
|
|
37
|
+
const th = fnv1a_64(paper.title);
|
|
38
|
+
const title_lo = Number(th & 0xFFFFn);
|
|
39
|
+
const title_hi = Number((th >> 16n) & 0xFFFFn);
|
|
40
|
+
const num = Math.min(paper.number, 500);
|
|
41
|
+
const num_q = num * 131;
|
|
42
|
+
return [num_q, title_lo, f_q, phase_q, year_q, n_refs_q, title_hi, 0,
|
|
43
|
+
0, 0, 0, 0, 0, 0, 0, 0];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ===== Cosine similarity =====
|
|
47
|
+
function cosineSim(a, b) {
|
|
48
|
+
let dot = 0, na = 0, nb = 0;
|
|
49
|
+
for (let i = 0; i < 16; i++) {
|
|
50
|
+
dot += a[i] * b[i];
|
|
51
|
+
na += a[i] * a[i];
|
|
52
|
+
nb += b[i] * b[i];
|
|
53
|
+
}
|
|
54
|
+
na = Math.sqrt(na);
|
|
55
|
+
nb = Math.sqrt(nb);
|
|
56
|
+
if (na === 0 || nb === 0) return 0;
|
|
57
|
+
return dot / (na * nb);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ===== State hash (FNV-1a over sorted dials) =====
|
|
61
|
+
function stateHash(papers) {
|
|
62
|
+
const allDials = Object.values(papers).map(p => cellToDials(p));
|
|
63
|
+
allDials.sort((a, b) => a[0] - b[0]);
|
|
64
|
+
let h = 0xCBF29CE484222325n;
|
|
65
|
+
for (const d of allDials) {
|
|
66
|
+
for (const v of d) {
|
|
67
|
+
const lo = v & 0xFF;
|
|
68
|
+
const hi = (v >> 8) & 0xFF;
|
|
69
|
+
h ^= BigInt(lo);
|
|
70
|
+
h = (h * 0x00000100000001B3n) & 0xFFFFFFFFFFFFFFFFn;
|
|
71
|
+
h ^= BigInt(hi);
|
|
72
|
+
h = (h * 0x00000100000001B3n) & 0xFFFFFFFFFFFFFFFFn;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return h;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ===== The 5 Operations =====
|
|
79
|
+
function navigate(canon, start, depth) {
|
|
80
|
+
const visited = new Set([start]);
|
|
81
|
+
const result = [];
|
|
82
|
+
const queue = [[start, 0]];
|
|
83
|
+
while (queue.length > 0) {
|
|
84
|
+
const [num, d] = queue.shift();
|
|
85
|
+
const paper = canon[num];
|
|
86
|
+
if (paper) {
|
|
87
|
+
result.push({ depth: d, paper });
|
|
88
|
+
if (d < depth) {
|
|
89
|
+
for (const ref of (paper.ref_papers || [])) {
|
|
90
|
+
if (canon[ref] && !visited.has(ref)) {
|
|
91
|
+
visited.add(ref);
|
|
92
|
+
queue.push([ref, d + 1]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function confluence(canon, paper_nums) {
|
|
102
|
+
if (!paper_nums || paper_nums.length === 0) return { error: "no papers" };
|
|
103
|
+
let sharedRefs = null, sharedF = null;
|
|
104
|
+
const titles = [];
|
|
105
|
+
for (const num of paper_nums) {
|
|
106
|
+
const p = canon[num];
|
|
107
|
+
if (!p) continue;
|
|
108
|
+
titles.push(p.title);
|
|
109
|
+
const refs = new Set(p.ref_papers || []);
|
|
110
|
+
sharedRefs = sharedRefs === null
|
|
111
|
+
? new Set(refs)
|
|
112
|
+
: new Set([...sharedRefs].filter(x => refs.has(x)));
|
|
113
|
+
const fs = new Set(p.ref_f_numbers || []);
|
|
114
|
+
sharedF = sharedF === null
|
|
115
|
+
? new Set(fs)
|
|
116
|
+
: new Set([...sharedF].filter(x => fs.has(x)));
|
|
117
|
+
}
|
|
118
|
+
let suggested = `Composition of ${paper_nums.length} papers`;
|
|
119
|
+
if (sharedF && sharedF.size > 0) {
|
|
120
|
+
const first = [...sharedF].sort((a, b) => a - b)[0];
|
|
121
|
+
suggested = `F${first} Synthesis: ${titles.join(", ")}`;
|
|
122
|
+
}
|
|
123
|
+
const maxN = Math.max(...Object.keys(canon).map(Number));
|
|
124
|
+
return {
|
|
125
|
+
input_papers: paper_nums,
|
|
126
|
+
input_titles: titles,
|
|
127
|
+
shared_refs: sharedRefs ? [...sharedRefs].sort((a, b) => a - b) : [],
|
|
128
|
+
shared_f_numbers: sharedF ? [...sharedF].sort((a, b) => a - b) : [],
|
|
129
|
+
suggested_title: suggested,
|
|
130
|
+
ghost_paper: `paper-${maxN + 1}.md`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function lineage(canon, f_number) {
|
|
135
|
+
const result = [];
|
|
136
|
+
for (const p of Object.values(canon)) {
|
|
137
|
+
if ((p.ref_f_numbers || []).includes(f_number)) result.push(p);
|
|
138
|
+
}
|
|
139
|
+
result.sort((a, b) => (a.phase - b.phase) || (a.number - b.number));
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ghost(canon, paper_num, k) {
|
|
144
|
+
const target = canon[paper_num];
|
|
145
|
+
if (!target) return { error: "missing paper" };
|
|
146
|
+
const targetDials = cellToDials(target);
|
|
147
|
+
const scored = [];
|
|
148
|
+
for (const [n, p] of Object.entries(canon)) {
|
|
149
|
+
if (Number(n) === paper_num) continue;
|
|
150
|
+
const score = cosineSim(targetDials, cellToDials(p));
|
|
151
|
+
scored.push({ id: `p${String(n).padStart(4, "0")}`, score: Math.round(score * 10000) / 10000 });
|
|
152
|
+
}
|
|
153
|
+
scored.sort((a, b) => b.score - a.score);
|
|
154
|
+
return {
|
|
155
|
+
source_paper: `paper-${paper_num}.md`,
|
|
156
|
+
neighbors: scored.slice(0, k),
|
|
157
|
+
suggested_title: `A Bridge between F${target.f_number} and its neighbors`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function tick(canon) {
|
|
162
|
+
return { ticked_cells: Object.keys(canon).length };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ===== Bundled canon (9 papers from the polyformalism cascade) =====
|
|
166
|
+
const DEFAULT_CANON = {
|
|
167
|
+
425: { number: 425, title: "F115 — The Logical Routes: VHDL × Verilog × the QUF bit-exactness", f_number: 115, phase: 237, date: "2026-09-03", ref_papers: [426, 427], ref_f_numbers: [] },
|
|
168
|
+
426: { number: 426, title: "F116 — The 5+1+1+1+1+1+1+1+1+1+1 Opcodes in 5 Substrates: A Polyformalism Atlas", f_number: 116, phase: 238, date: "2026-09-03", ref_papers: [], ref_f_numbers: [115] },
|
|
169
|
+
427: { number: 427, title: "F117 — The 5-Substrate Polyformalism: Python × C × Rust × Verilog × VHDL, One Cell", f_number: 117, phase: 239, date: "2026-09-03", ref_papers: [], ref_f_numbers: [115, 116] },
|
|
170
|
+
428: { number: 428, title: "F118 — The Polyformalism in Production: A Play-Test + Benchmark", f_number: 118, phase: 240, date: "2026-09-03", ref_papers: [], ref_f_numbers: [115, 116, 117] },
|
|
171
|
+
429: { number: 429, title: "F119 — The 6-Substrate Polyformalism: cell-runtime Joins the Canon", f_number: 119, phase: 241, date: "2026-09-03", ref_papers: [], ref_f_numbers: [115, 116, 117, 118] },
|
|
172
|
+
432: { number: 432, title: "F122 — The Shape Store: 5 Indices on Cloudflare Vectorize", f_number: 122, phase: 244, date: "2026-09-03", ref_papers: [], ref_f_numbers: [120, 121] },
|
|
173
|
+
433: { number: 433, title: "F123 — The Composer Agent: 5 Cells, 80 Parameters", f_number: 123, phase: 245, date: "2026-09-03", ref_papers: [], ref_f_numbers: [120, 122] },
|
|
174
|
+
439: { number: 439, title: "F129 — The Live Canon: Papers as Cells, Reading as Navigation", f_number: 129, phase: 251, date: "2026-09-03", ref_papers: [], ref_f_numbers: [115, 120, 122, 125] },
|
|
175
|
+
440: { number: 440, title: "F130 — The Polyformal Live Canon: One Cell, Five Substrates", f_number: 130, phase: 251, date: "2026-09-03", ref_papers: [], ref_f_numbers: [115, 129] },
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// ===== LiveCanon class =====
|
|
179
|
+
class LiveCanon {
|
|
180
|
+
constructor(canon = DEFAULT_CANON) {
|
|
181
|
+
this.canon = canon;
|
|
182
|
+
}
|
|
183
|
+
navigate(start, depth = 2) { return navigate(this.canon, start, depth); }
|
|
184
|
+
confluence(paper_nums) { return confluence(this.canon, paper_nums); }
|
|
185
|
+
lineage(f_number) { return lineage(this.canon, f_number); }
|
|
186
|
+
ghost(paper_num, k = 5) { return ghost(this.canon, paper_num, k); }
|
|
187
|
+
tick() { return tick(this.canon); }
|
|
188
|
+
get stateHash() { return stateHash(this.canon); }
|
|
189
|
+
get stateHashString() { return "0x" + this.stateHash.toString(16).padStart(16, "0"); }
|
|
190
|
+
get paperCount() { return Object.keys(this.canon).length; }
|
|
191
|
+
papers() { return Object.values(this.canon); }
|
|
192
|
+
dials(paper_num) { return cellToDials(this.canon[paper_num]); }
|
|
193
|
+
// Fetch the canon from the live URL
|
|
194
|
+
static async fromUrl(url = "https://live-canon.superinstance.dev/api/canon") {
|
|
195
|
+
const fetchFn = globalThis.fetch || (await import("node-fetch")).default;
|
|
196
|
+
const res = await fetchFn(url);
|
|
197
|
+
const data = await res.json();
|
|
198
|
+
const canon = {};
|
|
199
|
+
for (const p of data.papers) {
|
|
200
|
+
canon[p.number] = {
|
|
201
|
+
number: p.number,
|
|
202
|
+
title: p.title,
|
|
203
|
+
f_number: p.f_number,
|
|
204
|
+
phase: p.phase,
|
|
205
|
+
date: p.date || "2026-09-03",
|
|
206
|
+
ref_papers: [],
|
|
207
|
+
ref_f_numbers: [],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return new LiveCanon(canon);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
LiveCanon,
|
|
216
|
+
DEFAULT_CANON,
|
|
217
|
+
fnv1a_64,
|
|
218
|
+
cellToDials,
|
|
219
|
+
cosineSim,
|
|
220
|
+
stateHash,
|
|
221
|
+
navigate,
|
|
222
|
+
confluence,
|
|
223
|
+
lineage,
|
|
224
|
+
ghost,
|
|
225
|
+
tick,
|
|
226
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@superinstance/live-canon",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Live Canon — read the AI-Writings canon as a navigable cell fabric, with 5 novel operations: NAVIGATE, CONFLUENCE, LINEAGE, GHOST, TICK.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node test/test.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"quilt",
|
|
11
|
+
"cell-fabric",
|
|
12
|
+
"polyformalism",
|
|
13
|
+
"shape-rag",
|
|
14
|
+
"live-canon",
|
|
15
|
+
"ai-writings"
|
|
16
|
+
],
|
|
17
|
+
"author": "Casey Digennaro <superinstance@users.noreply.github.com>",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/SuperInstance/quilt-live-canon.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://live-canon.superinstance.dev",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/test/test.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// test/test.js — verify the Live Canon npm package
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const {
|
|
4
|
+
LiveCanon, DEFAULT_CANON, fnv1a_64, cellToDials, stateHash,
|
|
5
|
+
navigate, confluence, lineage, ghost, tick,
|
|
6
|
+
} = require('../index.js');
|
|
7
|
+
|
|
8
|
+
const canon = new LiveCanon();
|
|
9
|
+
|
|
10
|
+
// Test 1: bundled canon has 9 papers
|
|
11
|
+
assert.strictEqual(canon.paperCount, 9, 'expected 9 papers');
|
|
12
|
+
console.log('✓ 9 papers bundled');
|
|
13
|
+
|
|
14
|
+
// Test 2: state hash matches Python reference (0xbf27a3631cdee337)
|
|
15
|
+
assert.strictEqual(canon.stateHashString, '0xbf27a3631cdee337', 'state hash mismatch');
|
|
16
|
+
console.log('✓ state hash = 0xbf27a3631cdee337 (byte-exact with Python)');
|
|
17
|
+
|
|
18
|
+
// Test 3: NAVIGATE
|
|
19
|
+
const path = canon.navigate(425, 2);
|
|
20
|
+
assert.ok(path.length > 0, 'expected path');
|
|
21
|
+
assert.strictEqual(path[0].paper.number, 425, 'first paper should be 425');
|
|
22
|
+
console.log(`✓ NAVIGATE(425, 2) returned ${path.length} cells`);
|
|
23
|
+
|
|
24
|
+
// Test 4: CONFLUENCE
|
|
25
|
+
const conf = canon.confluence([425, 432, 439]);
|
|
26
|
+
assert.ok(conf.suggested_title, 'expected suggested title');
|
|
27
|
+
console.log(`✓ CONFLUENCE: "${conf.suggested_title.slice(0, 50)}..."`);
|
|
28
|
+
|
|
29
|
+
// Test 5: LINEAGE
|
|
30
|
+
const lin = canon.lineage(115);
|
|
31
|
+
assert.ok(lin.length > 0, 'expected lineage');
|
|
32
|
+
assert.ok(lin.some(p => p.f_number === 116), 'lineage should include F116');
|
|
33
|
+
console.log(`✓ LINEAGE(F115): ${lin.length} papers`);
|
|
34
|
+
|
|
35
|
+
// Test 6: GHOST
|
|
36
|
+
const g = canon.ghost(425, 5);
|
|
37
|
+
assert.ok(g.neighbors.length > 0, 'expected neighbors');
|
|
38
|
+
console.log(`✓ GHOST(425, 5): top = ${g.neighbors[0].id} (score=${g.neighbors[0].score})`);
|
|
39
|
+
|
|
40
|
+
// Test 7: TICK
|
|
41
|
+
const tk = canon.tick();
|
|
42
|
+
assert.strictEqual(tk.ticked_cells, 9, 'expected 9 ticked cells');
|
|
43
|
+
console.log(`✓ TICK: ${tk.ticked_cells} cells`);
|
|
44
|
+
|
|
45
|
+
// Test 8: FNV-1a
|
|
46
|
+
const h = fnv1a_64('F115 — The Logical Routes');
|
|
47
|
+
console.log(`✓ FNV-1a("F115 — The Logical Routes") = 0x${h.toString(16)}`);
|
|
48
|
+
|
|
49
|
+
// Test 9: dials for paper 425
|
|
50
|
+
const d425 = canon.dials(425);
|
|
51
|
+
assert.strictEqual(d425.length, 16, 'expected 16 dials');
|
|
52
|
+
console.log(`✓ dials(paper-425) = [${d425.slice(0, 7).join(', ')}]`);
|
|
53
|
+
|
|
54
|
+
console.log('\nAll tests passed! Live Canon npm package is byte-exact with Python.');
|