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 CHANGED
@@ -1,13 +1,20 @@
1
1
  // rola.ts — Radix On-Ledger Authentication: challenge, proof, session.
2
2
  //
3
- // Both Radix wikis had written this file, and the two copies were 94% identical
4
- // — the whole difference was a cookie name, plus the half radix-wiki had learned
5
- // since and miow had not: persona (identity_*) proofs, which miow rejected
6
- // because it passed `type: 'account'` unconditionally, and the error logging
7
- // that makes a failed verification diagnosable at all.
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 differs in: the cookie name and its dApp identity.
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
@@ -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 `&nbsp;` 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&nbsp;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 = "<[^>]*>|&nbsp;|\\\\[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 `&nbsp;` 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&nbsp;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 = '<[^>]*>|&nbsp;|\\\\[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.16.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"