wiki-formant 0.16.0 → 0.17.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/dist/rola.js +13 -6
- package/dist/search.d.ts +60 -0
- package/dist/search.js +90 -0
- package/package.json +5 -1
package/dist/rola.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
// rola.ts — Radix On-Ledger Authentication: challenge, proof, session.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Two Radix wikis had written this file, and the copies were 94% identical — the
|
|
4
|
+
// whole difference was a cookie name, plus the half radix-wiki had learned and
|
|
5
|
+
// miow had not: persona (identity_*) proofs, which miow rejected because it
|
|
6
|
+
// passed `type: 'account'` unconditionally, and the error logging that makes a
|
|
7
|
+
// failed verification diagnosable at all.
|
|
8
8
|
//
|
|
9
9
|
// This is radix-wiki's version, parameterised on the two things a second wiki
|
|
10
|
-
// actually
|
|
10
|
+
// would actually differ in: the cookie name and its dApp identity.
|
|
11
|
+
//
|
|
12
|
+
// **One consumer, as of Sep 2026.** miow is retired, so the second wiki this was
|
|
13
|
+
// parameterised for no longer exists; caper's wallet auth never used this stack
|
|
14
|
+
// and acuiq gates only its wiki editor, on a shared secret. Keep it — the
|
|
15
|
+
// persona-proof fix and the error logging are worth not losing, and the ports
|
|
16
|
+
// below are what make it reusable at all — but do not mistake it for a proven
|
|
17
|
+
// shared abstraction. It is radix-wiki's auth that happens to live in a package.
|
|
11
18
|
//
|
|
12
19
|
// Storage and cookies are ports rather than imports. A Prisma client is
|
|
13
20
|
// generated per repo and cannot be shared, and taking `next/headers` here would
|
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every `text` value at any block depth, as prose.
|
|
3
|
+
*
|
|
4
|
+
* `$.**.text` rather than the raw JSON, so block ids and `type` discriminators
|
|
5
|
+
* cannot score as prose. Then three classes of non-prose are collapsed to
|
|
6
|
+
* spaces:
|
|
7
|
+
*
|
|
8
|
+
* - markup (`<[^>]*>`) and the ` ` entity
|
|
9
|
+
* - the JSON array literal's own syntax — `", "` between adjacent values,
|
|
10
|
+
* the `["` and `"]` at the ends, and `\n` / `\t` / `\"` / `\\` / `\/`
|
|
11
|
+
* escapes inside values
|
|
12
|
+
* - `chr(160)`, the literal NBSP, via `translate`, so a typed "534 KB"
|
|
13
|
+
* matches a stored "534 KB"
|
|
14
|
+
*
|
|
15
|
+
* Other HTML entities are deliberately left encoded: `ts_headline` runs on this
|
|
16
|
+
* same expression, and decoding belongs on the way out (see each repo's
|
|
17
|
+
* `summarizePage`), not in an expression a stored generated column depends on.
|
|
18
|
+
*
|
|
19
|
+
* @param content SQL expression for the JSON column — qualify it (`p.content`)
|
|
20
|
+
* when the query aliases its table.
|
|
21
|
+
*/
|
|
22
|
+
export declare function proseSql(content?: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* The `regexp_replace` pattern inside {@link proseSql}, as it must appear in
|
|
25
|
+
* SQL. Written for a single-quoted SQL string literal, so a backslash that
|
|
26
|
+
* Postgres' regex engine should see as an escape is doubled here.
|
|
27
|
+
*/
|
|
28
|
+
export declare const PROSE_PATTERN = "<[^>]*>| |\\\\[nrt\"\\\\/]|\", \"|\\[\"|\"\\]";
|
|
29
|
+
/**
|
|
30
|
+
* The generated column behind the full-text tier: title at weight A, prose at
|
|
31
|
+
* weight B, so a title hit outranks a body hit inside tier 3 as well as across
|
|
32
|
+
* tiers.
|
|
33
|
+
*/
|
|
34
|
+
export declare function searchTsvSql(content?: string, title?: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* `ts_headline` options for a search result snippet: one fragment, wide enough
|
|
37
|
+
* to read as a sentence, with no highlight markers (the caller styles it).
|
|
38
|
+
*/
|
|
39
|
+
export declare const HEADLINE_OPTIONS = "MaxWords=32, MinWords=16, ShortWord=3, MaxFragments=1, StartSel=\"\", StopSel=\"\"";
|
|
40
|
+
/**
|
|
41
|
+
* Normalisation flag for `ts_rank_cd` on the full-text tier: 32 is
|
|
42
|
+
* `rank/(rank+1)`.
|
|
43
|
+
*
|
|
44
|
+
* Tier 3 is reached only by queries the literal tiers could not answer, and it
|
|
45
|
+
* routinely matches a large share of the corpus — `what is a validator` and
|
|
46
|
+
* `what does a validator do` reduce to the same lexeme and return the identical
|
|
47
|
+
* rows — so ordering is the whole product on this tier. Normalisation 2 (divide
|
|
48
|
+
* by document length) was measured and is worse: it promotes one-line stubs
|
|
49
|
+
* above the long article the asker wants.
|
|
50
|
+
*/
|
|
51
|
+
export declare const FTS_RANK_NORMALIZATION = 32;
|
|
52
|
+
/**
|
|
53
|
+
* Escape a user's query for use inside a `LIKE`/`ILIKE` pattern.
|
|
54
|
+
*
|
|
55
|
+
* `%`, `_` and `\` are metacharacters there and are literal in what a person
|
|
56
|
+
* typed, so a search for "100%" or "snake_case" means what it says. Returns the
|
|
57
|
+
* empty string for a blank query — callers should treat that as "no search"
|
|
58
|
+
* rather than as a pattern matching everything.
|
|
59
|
+
*/
|
|
60
|
+
export declare function escapeLikeTerm(query: string): string;
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// search.ts — the SQL invariants behind ranked wiki search.
|
|
2
|
+
//
|
|
3
|
+
// Both wikis run the same four-tier search over block JSON, and both build a
|
|
4
|
+
// `search_tsv` generated column to back the fourth tier. The expression that
|
|
5
|
+
// defines "prose" therefore appears twice per repo at minimum — once in the
|
|
6
|
+
// DDL that generates the column, once in the query that reads it — and the
|
|
7
|
+
// literal tier and the full-text tier disagree about what a page says the
|
|
8
|
+
// moment those two drift.
|
|
9
|
+
//
|
|
10
|
+
// They did drift. In September 2026 caper learned that
|
|
11
|
+
// `jsonb_path_query_array(...)::text` renders a JSON array literal, so the
|
|
12
|
+
// array's own syntax arrives as prose: a search for the literal `", "` matched
|
|
13
|
+
// 262 of 262 pages, `\n` matched 245, and `ts_headline` windowed on the
|
|
14
|
+
// punctuation and showed it to readers mid-snippet. caper fixed its two copies
|
|
15
|
+
// together; radix-wiki's four (one DDL, three inline) kept the defect, and each
|
|
16
|
+
// of the six carried a comment asserting they must stay identical.
|
|
17
|
+
//
|
|
18
|
+
// So the expression lives here, once, and every copy is derived. Nothing in
|
|
19
|
+
// this module touches a database or a driver: it returns SQL text, and each
|
|
20
|
+
// repo interpolates it into its own tagged template with its own table, scope
|
|
21
|
+
// and select list. Those genuinely differ — radix-wiki returns ranked ids to
|
|
22
|
+
// hydrate, caper returns rows — and pretending otherwise would cost more than
|
|
23
|
+
// the drift did.
|
|
24
|
+
/**
|
|
25
|
+
* Every `text` value at any block depth, as prose.
|
|
26
|
+
*
|
|
27
|
+
* `$.**.text` rather than the raw JSON, so block ids and `type` discriminators
|
|
28
|
+
* cannot score as prose. Then three classes of non-prose are collapsed to
|
|
29
|
+
* spaces:
|
|
30
|
+
*
|
|
31
|
+
* - markup (`<[^>]*>`) and the ` ` entity
|
|
32
|
+
* - the JSON array literal's own syntax — `", "` between adjacent values,
|
|
33
|
+
* the `["` and `"]` at the ends, and `\n` / `\t` / `\"` / `\\` / `\/`
|
|
34
|
+
* escapes inside values
|
|
35
|
+
* - `chr(160)`, the literal NBSP, via `translate`, so a typed "534 KB"
|
|
36
|
+
* matches a stored "534 KB"
|
|
37
|
+
*
|
|
38
|
+
* Other HTML entities are deliberately left encoded: `ts_headline` runs on this
|
|
39
|
+
* same expression, and decoding belongs on the way out (see each repo's
|
|
40
|
+
* `summarizePage`), not in an expression a stored generated column depends on.
|
|
41
|
+
*
|
|
42
|
+
* @param content SQL expression for the JSON column — qualify it (`p.content`)
|
|
43
|
+
* when the query aliases its table.
|
|
44
|
+
*/
|
|
45
|
+
export function proseSql(content = 'content') {
|
|
46
|
+
return `regexp_replace(translate(jsonb_path_query_array(${content},'$.**.text')::text, chr(160),' '),'${PROSE_PATTERN}',' ','g')`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The `regexp_replace` pattern inside {@link proseSql}, as it must appear in
|
|
50
|
+
* SQL. Written for a single-quoted SQL string literal, so a backslash that
|
|
51
|
+
* Postgres' regex engine should see as an escape is doubled here.
|
|
52
|
+
*/
|
|
53
|
+
export const PROSE_PATTERN = '<[^>]*>| |\\\\[nrt"\\\\/]|", "|\\["|"\\]';
|
|
54
|
+
/**
|
|
55
|
+
* The generated column behind the full-text tier: title at weight A, prose at
|
|
56
|
+
* weight B, so a title hit outranks a body hit inside tier 3 as well as across
|
|
57
|
+
* tiers.
|
|
58
|
+
*/
|
|
59
|
+
export function searchTsvSql(content = 'content', title = 'title') {
|
|
60
|
+
return `setweight(to_tsvector('english', coalesce(${title},'')), 'A') || ` +
|
|
61
|
+
`setweight(to_tsvector('english', coalesce(${proseSql(content)}, '')), 'B')`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `ts_headline` options for a search result snippet: one fragment, wide enough
|
|
65
|
+
* to read as a sentence, with no highlight markers (the caller styles it).
|
|
66
|
+
*/
|
|
67
|
+
export const HEADLINE_OPTIONS = 'MaxWords=32, MinWords=16, ShortWord=3, MaxFragments=1, StartSel="", StopSel=""';
|
|
68
|
+
/**
|
|
69
|
+
* Normalisation flag for `ts_rank_cd` on the full-text tier: 32 is
|
|
70
|
+
* `rank/(rank+1)`.
|
|
71
|
+
*
|
|
72
|
+
* Tier 3 is reached only by queries the literal tiers could not answer, and it
|
|
73
|
+
* routinely matches a large share of the corpus — `what is a validator` and
|
|
74
|
+
* `what does a validator do` reduce to the same lexeme and return the identical
|
|
75
|
+
* rows — so ordering is the whole product on this tier. Normalisation 2 (divide
|
|
76
|
+
* by document length) was measured and is worse: it promotes one-line stubs
|
|
77
|
+
* above the long article the asker wants.
|
|
78
|
+
*/
|
|
79
|
+
export const FTS_RANK_NORMALIZATION = 32;
|
|
80
|
+
/**
|
|
81
|
+
* Escape a user's query for use inside a `LIKE`/`ILIKE` pattern.
|
|
82
|
+
*
|
|
83
|
+
* `%`, `_` and `\` are metacharacters there and are literal in what a person
|
|
84
|
+
* typed, so a search for "100%" or "snake_case" means what it says. Returns the
|
|
85
|
+
* empty string for a blank query — callers should treat that as "no search"
|
|
86
|
+
* rather than as a pattern matching everything.
|
|
87
|
+
*/
|
|
88
|
+
export function escapeLikeTerm(query) {
|
|
89
|
+
return (query ?? '').trim().replace(/[\\%_]/g, char => `\\${char}`);
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, and conditional-GET plumbing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,6 +76,10 @@
|
|
|
76
76
|
"types": "./dist/revisions.d.ts",
|
|
77
77
|
"import": "./dist/revisions.js"
|
|
78
78
|
},
|
|
79
|
+
"./search": {
|
|
80
|
+
"types": "./dist/search.d.ts",
|
|
81
|
+
"import": "./dist/search.js"
|
|
82
|
+
},
|
|
79
83
|
"./feed": {
|
|
80
84
|
"types": "./dist/feed.d.ts",
|
|
81
85
|
"import": "./dist/feed.js"
|