wiki-formant 0.48.0 → 0.50.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 +85 -27
- package/bin/maintenance-log.mjs +198 -0
- package/bin/mark-verified.mjs +75 -0
- package/bin/prisma-models.mjs +53 -0
- package/dist/analytics.d.ts +0 -25
- package/dist/analytics.js +10 -49
- package/dist/base.css +23 -2
- package/dist/blocks.d.ts +7 -6
- package/dist/blocks.js +11 -3
- package/dist/chart.d.ts +0 -13
- package/dist/chart.js +4 -0
- package/dist/combobox.d.ts +0 -4
- package/dist/combobox.js +2 -0
- package/dist/crawlers.d.ts +0 -24
- package/dist/crawlers.js +9 -3
- package/dist/dom.d.ts +0 -5
- package/dist/dom.js +2 -0
- package/dist/editor.d.ts +0 -43
- package/dist/editor.js +7 -1
- package/dist/feed.d.ts +0 -14
- package/dist/feed.js +4 -0
- package/dist/freshness.d.ts +0 -7
- package/dist/freshness.js +3 -0
- package/dist/headers.d.ts +0 -8
- package/dist/headers.js +3 -1
- package/dist/headings.d.ts +0 -8
- package/dist/headings.js +2 -0
- package/dist/history.d.ts +0 -6
- package/dist/history.js +2 -0
- package/dist/http.d.ts +0 -11
- package/dist/http.js +5 -0
- package/dist/license.d.ts +0 -8
- package/dist/license.js +2 -0
- package/dist/link-check.d.ts +0 -21
- package/dist/link-check.js +6 -2
- package/dist/links.d.ts +0 -5
- package/dist/links.js +2 -0
- package/dist/maintenance.d.ts +61 -0
- package/dist/maintenance.js +137 -0
- package/dist/maps.d.ts +0 -6
- package/dist/maps.js +2 -0
- package/dist/markdown.d.ts +0 -2
- package/dist/markdown.js +1 -1
- package/dist/mcp.d.ts +0 -14
- package/dist/mcp.js +1 -1
- package/dist/metadata.d.ts +14 -11
- package/dist/metadata.js +33 -0
- package/dist/pagination.d.ts +0 -5
- package/dist/pagination.js +0 -4
- package/dist/passkey.js +2 -15
- package/dist/rate-limit.d.ts +5 -16
- package/dist/rate-limit.js +10 -1
- package/dist/react-server.d.ts +0 -12
- package/dist/react-server.js +2 -0
- package/dist/revisions.d.ts +0 -7
- package/dist/revisions.js +2 -0
- package/dist/rola.d.ts +74 -0
- package/dist/rola.js +97 -0
- package/dist/search.d.ts +0 -12
- package/dist/search.js +4 -0
- package/dist/sidebar.d.ts +0 -16
- package/dist/sidebar.js +2 -0
- package/dist/stats.d.ts +0 -4
- package/dist/stats.js +2 -2
- package/dist/taxonomy.d.ts +0 -4
- package/dist/taxonomy.js +2 -2
- package/dist/text.d.ts +16 -22
- package/dist/text.js +39 -5
- package/dist/tiptap.d.ts +5 -4
- package/dist/tiptap.js +2 -1
- package/dist/upload.d.ts +45 -0
- package/dist/upload.js +106 -0
- package/dist/validation.d.ts +0 -6
- package/dist/validation.js +3 -3
- package/dist/well-known.d.ts +0 -20
- package/dist/well-known.js +3 -0
- package/package.json +37 -8
- package/prisma/analytics.prisma +57 -0
- package/prisma/passkey.prisma +21 -0
package/dist/headers.js
CHANGED
|
@@ -24,12 +24,14 @@ export const DEFAULT_IFRAME_HOSTS = [
|
|
|
24
24
|
'www.google.com', 'maps.google.com',
|
|
25
25
|
'embed.apple.com', 'maps.apple.com',
|
|
26
26
|
];
|
|
27
|
-
/** `frame-src` sources for a host list — the one a sanitiser was given. */
|
|
27
|
+
/** `frame-src` sources for a host list — the one a sanitiser was given. @internal */
|
|
28
28
|
export const frameSources = (hosts = DEFAULT_IFRAME_HOSTS) => hosts.length ? hosts.map(h => `https://${h}`) : ["'none'"];
|
|
29
29
|
/**
|
|
30
30
|
* The policy all four origins shared line for line. Everything a site adds is
|
|
31
31
|
* a source list it hands `contentSecurityPolicy`, which replaces the default
|
|
32
32
|
* for that directive rather than appending, so a reader sees the whole list.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
33
35
|
*/
|
|
34
36
|
export const BASE_CSP = {
|
|
35
37
|
'default-src': ["'self'"],
|
package/dist/headings.d.ts
CHANGED
|
@@ -53,14 +53,6 @@ export declare function injectHeadingIds(html: string, options?: HeadingIdOption
|
|
|
53
53
|
* consumers use only the injector above.
|
|
54
54
|
*/
|
|
55
55
|
export declare function headingsFrom(html: string): Heading[];
|
|
56
|
-
/**
|
|
57
|
-
* Every heading in `html`, in document order, whether or not it carries an id —
|
|
58
|
-
* the outline a crawler reads. `headingsFrom` above answers a different
|
|
59
|
-
* question: which headings a rail can link to, which is why it drops the ones
|
|
60
|
-
* with no id. An audit must not, because a chrome heading with no id still
|
|
61
|
-
* takes a level in the outline, and the levels are the thing under test.
|
|
62
|
-
*/
|
|
63
|
-
export declare function headingOutline(html: string): Heading[];
|
|
64
56
|
/**
|
|
65
57
|
* What is wrong with a page's heading outline.
|
|
66
58
|
*
|
package/dist/headings.js
CHANGED
|
@@ -92,6 +92,8 @@ export function headingsFrom(html) {
|
|
|
92
92
|
* question: which headings a rail can link to, which is why it drops the ones
|
|
93
93
|
* with no id. An audit must not, because a chrome heading with no id still
|
|
94
94
|
* takes a level in the outline, and the levels are the thing under test.
|
|
95
|
+
*
|
|
96
|
+
* @internal
|
|
95
97
|
*/
|
|
96
98
|
export function headingOutline(html) {
|
|
97
99
|
const out = [];
|
package/dist/history.d.ts
CHANGED
|
@@ -3,12 +3,6 @@ import { type BlockChange, type BlockGroup, type DiffBlock } from './revisions.j
|
|
|
3
3
|
export type DiffPart = [-1 | 0 | 1, string];
|
|
4
4
|
/** A change as a history view lists it: the diff it draws, not the two bodies behind it. */
|
|
5
5
|
export type HistoryChange = BlockChange<DiffPart[]>;
|
|
6
|
-
/**
|
|
7
|
-
* Two HTML bodies as the words struck and added between them, or `undefined`
|
|
8
|
-
* when their text is the same. Semantic cleanup is on: without it a rewritten
|
|
9
|
-
* sentence comes out as fragments of the letters the two versions share.
|
|
10
|
-
*/
|
|
11
|
-
export declare function textDiff(from: string, to: string): DiffPart[] | undefined;
|
|
12
6
|
export interface HistoryOptions<B> {
|
|
13
7
|
/** Defaults to `coreBlockGroups`, as in `diffBlocks`. */
|
|
14
8
|
containers?: (block: B) => BlockGroup<B>[] | null;
|
package/dist/history.js
CHANGED
|
@@ -20,6 +20,8 @@ const clip = (t) => (t.length > KEPT_MAX ? `${t.slice(0, 30)}…${t.slice(-30)}`
|
|
|
20
20
|
* Two HTML bodies as the words struck and added between them, or `undefined`
|
|
21
21
|
* when their text is the same. Semantic cleanup is on: without it a rewritten
|
|
22
22
|
* sentence comes out as fragments of the letters the two versions share.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
23
25
|
*/
|
|
24
26
|
export function textDiff(from, to) {
|
|
25
27
|
const a = stripHtml(from);
|
package/dist/http.d.ts
CHANGED
|
@@ -76,17 +76,6 @@ export declare function wantsMarkdown(request: {
|
|
|
76
76
|
export declare const VARY_ACCEPT: {
|
|
77
77
|
readonly Vary: "Accept";
|
|
78
78
|
};
|
|
79
|
-
/**
|
|
80
|
-
* Headers for a JSON descriptor — an agent card, an OpenAPI document, a
|
|
81
|
-
* registry manifest. These are the documents a client refetches most and the
|
|
82
|
-
* ones that had no validator at all: served as `Cache-Control: public` with no
|
|
83
|
-
* `max-age`, a caller falls back to heuristic freshness and can never
|
|
84
|
-
* revalidate, so a corrected card takes an unbounded time to reach anyone.
|
|
85
|
-
*/
|
|
86
|
-
export declare function descriptorHeaders(etag: string, opts?: {
|
|
87
|
-
maxAge?: number;
|
|
88
|
-
extra?: Record<string, string>;
|
|
89
|
-
}): Record<string, string>;
|
|
90
79
|
/**
|
|
91
80
|
* A JSON descriptor served with a validator, answering a conditional GET.
|
|
92
81
|
*
|
package/dist/http.js
CHANGED
|
@@ -141,6 +141,10 @@ export const VARY_ACCEPT = { Vary: 'Accept' };
|
|
|
141
141
|
* ones that had no validator at all: served as `Cache-Control: public` with no
|
|
142
142
|
* `max-age`, a caller falls back to heuristic freshness and can never
|
|
143
143
|
* revalidate, so a corrected card takes an unbounded time to reach anyone.
|
|
144
|
+
* Every one is public and read by browser agents too, so each allows any
|
|
145
|
+
* origin; six of the nine routes passed that by hand and three did not.
|
|
146
|
+
*
|
|
147
|
+
* @internal
|
|
144
148
|
*/
|
|
145
149
|
export function descriptorHeaders(etag, opts = {}) {
|
|
146
150
|
const maxAge = opts.maxAge ?? 86400;
|
|
@@ -148,6 +152,7 @@ export function descriptorHeaders(etag, opts = {}) {
|
|
|
148
152
|
'Content-Type': 'application/json; charset=utf-8',
|
|
149
153
|
'Cache-Control': `public, max-age=300, s-maxage=${maxAge}, stale-while-revalidate=${maxAge * 7}`,
|
|
150
154
|
ETag: etag,
|
|
155
|
+
'Access-Control-Allow-Origin': '*',
|
|
151
156
|
...opts.extra,
|
|
152
157
|
};
|
|
153
158
|
}
|
package/dist/license.d.ts
CHANGED
|
@@ -32,14 +32,6 @@ export interface LicenseBlockOptions {
|
|
|
32
32
|
/** `Licence` where the site's prose says so. Defaults to `License`. */
|
|
33
33
|
heading?: string;
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
* The block, as lines, ready to join into any text export.
|
|
37
|
-
*
|
|
38
|
-
* Returned as an array rather than a string because every caller splices it
|
|
39
|
-
* into a document it is already building line by line, and a caller that wants
|
|
40
|
-
* the string can join it.
|
|
41
|
-
*/
|
|
42
|
-
export declare function licenseLines({ license, scope, scopeVerb, excludes, heading, }: LicenseBlockOptions): string[];
|
|
43
35
|
export declare function licenseBlock(opts: LicenseBlockOptions): string;
|
|
44
36
|
/** The one-line form, for a frontmatter field or a feed's `<copyright>`. */
|
|
45
37
|
export declare function licenseNote(license: License): string;
|
package/dist/license.js
CHANGED
|
@@ -24,6 +24,8 @@ export function ccBy40(opts) {
|
|
|
24
24
|
* Returned as an array rather than a string because every caller splices it
|
|
25
25
|
* into a document it is already building line by line, and a caller that wants
|
|
26
26
|
* the string can join it.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
27
29
|
*/
|
|
28
30
|
export function licenseLines({ license, scope = 'This content', scopeVerb = 'is', excludes = [], heading = 'License & Attribution', }) {
|
|
29
31
|
return [
|
package/dist/link-check.d.ts
CHANGED
|
@@ -15,22 +15,6 @@ export interface ProbeOptions {
|
|
|
15
15
|
/** Second-attempt budget for a host that is merely slow. */
|
|
16
16
|
slowTimeoutMs?: number;
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Codes that flap. A sweep found 16 status-0 rows and 15 were one host, every
|
|
20
|
-
* one answering 200 when probed alone. Carry the code through so a caller can
|
|
21
|
-
* treat these as retryable rather than banking a death.
|
|
22
|
-
*/
|
|
23
|
-
export declare const TRANSIENT_CODES: Set<string>;
|
|
24
|
-
/** A failure as a report row: the message, the code, and the TLS caveat. */
|
|
25
|
-
export declare function describeFailure(err: unknown): Pick<Probe, 'error' | 'code' | 'tls' | 'note'>;
|
|
26
|
-
/**
|
|
27
|
-
* The URL to probe INSTEAD, where the public page lies about its own health.
|
|
28
|
-
*
|
|
29
|
-
* npmjs.com serves 403 to scripted requests whether or not the package exists,
|
|
30
|
-
* which made every package citation a permanent false positive. The registry
|
|
31
|
-
* answers honestly.
|
|
32
|
-
*/
|
|
33
|
-
export declare function probeUrlFor(url: string): string;
|
|
34
18
|
/**
|
|
35
19
|
* Run `fn` with at most one request in flight against this hostname.
|
|
36
20
|
*
|
|
@@ -59,11 +43,6 @@ export declare function extractLinks(html: string): Array<{
|
|
|
59
43
|
href: string;
|
|
60
44
|
text: string;
|
|
61
45
|
}>;
|
|
62
|
-
/** Every `<iframe>` and `<img>` source in a fragment. */
|
|
63
|
-
export declare function extractEmbeds(html: string): Array<{
|
|
64
|
-
kind: string;
|
|
65
|
-
url: string;
|
|
66
|
-
}>;
|
|
67
46
|
/** What a page links to, split the way a checker probes it. */
|
|
68
47
|
export interface BlockLinks {
|
|
69
48
|
/** Absolute http(s) targets. */
|
package/dist/link-check.js
CHANGED
|
@@ -36,6 +36,8 @@ const TLS_CODES = new Set([
|
|
|
36
36
|
* Codes that flap. A sweep found 16 status-0 rows and 15 were one host, every
|
|
37
37
|
* one answering 200 when probed alone. Carry the code through so a caller can
|
|
38
38
|
* treat these as retryable rather than banking a death.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
39
41
|
*/
|
|
40
42
|
export const TRANSIENT_CODES = new Set([
|
|
41
43
|
'UND_ERR_CONNECT_TIMEOUT',
|
|
@@ -48,7 +50,7 @@ export const TRANSIENT_CODES = new Set([
|
|
|
48
50
|
'EAI_AGAIN',
|
|
49
51
|
'EPIPE',
|
|
50
52
|
]);
|
|
51
|
-
/** A failure as a report row: the message, the code, and the TLS caveat. */
|
|
53
|
+
/** A failure as a report row: the message, the code, and the TLS caveat. @internal */
|
|
52
54
|
export function describeFailure(err) {
|
|
53
55
|
const e = err;
|
|
54
56
|
if (e?.name === 'AbortError')
|
|
@@ -71,6 +73,8 @@ export function describeFailure(err) {
|
|
|
71
73
|
* npmjs.com serves 403 to scripted requests whether or not the package exists,
|
|
72
74
|
* which made every package citation a permanent false positive. The registry
|
|
73
75
|
* answers honestly.
|
|
76
|
+
*
|
|
77
|
+
* @internal
|
|
74
78
|
*/
|
|
75
79
|
export function probeUrlFor(url) {
|
|
76
80
|
const npm = url.match(/^https:\/\/(?:www\.)?npmjs\.com\/package\/(.+?)\/?$/);
|
|
@@ -223,7 +227,7 @@ export function extractLinks(html) {
|
|
|
223
227
|
}
|
|
224
228
|
return out;
|
|
225
229
|
}
|
|
226
|
-
/** Every `<iframe>` and `<img>` source in a fragment. */
|
|
230
|
+
/** Every `<iframe>` and `<img>` source in a fragment. @internal */
|
|
227
231
|
export function extractEmbeds(html) {
|
|
228
232
|
const out = [];
|
|
229
233
|
for (const m of html.matchAll(EMBED_RE)) {
|
package/dist/links.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A readable label for an anchor whose text is empty, derived from its href.
|
|
3
|
-
* Without one, crawlers and screen readers meet a link with nothing in it.
|
|
4
|
-
*/
|
|
5
|
-
export declare function fallbackAnchorText(href: string): string;
|
|
6
1
|
export interface NormaliseLinkOptions {
|
|
7
2
|
/**
|
|
8
3
|
* This wiki's own origin. An absolute link back to it is really internal, so
|
package/dist/links.js
CHANGED
|
@@ -18,6 +18,8 @@ import { getAttr, removeAttr, stripTags } from './html.js';
|
|
|
18
18
|
/**
|
|
19
19
|
* A readable label for an anchor whose text is empty, derived from its href.
|
|
20
20
|
* Without one, crawlers and screen readers meet a link with nothing in it.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
21
23
|
*/
|
|
22
24
|
export function fallbackAnchorText(href) {
|
|
23
25
|
if (href.startsWith('#'))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type SourceListItem = string | {
|
|
2
|
+
handle?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
};
|
|
6
|
+
export interface SourceEntry {
|
|
7
|
+
lastChecked?: string | null;
|
|
8
|
+
note?: string;
|
|
9
|
+
list?: SourceListItem[];
|
|
10
|
+
}
|
|
11
|
+
export type BacklogEntry = string | {
|
|
12
|
+
date?: string;
|
|
13
|
+
item: string;
|
|
14
|
+
status?: string;
|
|
15
|
+
};
|
|
16
|
+
export type FeedbackEntry = string | {
|
|
17
|
+
date?: string;
|
|
18
|
+
note: string;
|
|
19
|
+
};
|
|
20
|
+
export interface RunEntry {
|
|
21
|
+
date?: string;
|
|
22
|
+
category?: string;
|
|
23
|
+
summary?: string;
|
|
24
|
+
tweet?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface LogState {
|
|
27
|
+
mission?: string;
|
|
28
|
+
positioning?: string;
|
|
29
|
+
totalRuns?: number;
|
|
30
|
+
lastRun?: string | null;
|
|
31
|
+
rotation?: {
|
|
32
|
+
order?: string[];
|
|
33
|
+
next?: string;
|
|
34
|
+
};
|
|
35
|
+
sources?: Record<string, SourceEntry>;
|
|
36
|
+
backlog?: BacklogEntry[];
|
|
37
|
+
feedback?: FeedbackEntry[];
|
|
38
|
+
runHistory?: RunEntry[];
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
/** Drops what is closed or a diary entry, and caps what is append-only. Newest first. */
|
|
42
|
+
export declare function compact(state: LogState): LogState;
|
|
43
|
+
/** New backlog items, deduplicated against what is there, dated today unless they carry a date. */
|
|
44
|
+
export declare function addBacklog(state: LogState, items: readonly BacklogEntry[], today: string): BacklogEntry[];
|
|
45
|
+
/** The page's content blocks, re-rendered from state on every write. `routine` names who keeps it. */
|
|
46
|
+
export declare function render(state: LogState, routine: string, newId?: () => string): {
|
|
47
|
+
id: string;
|
|
48
|
+
type: "content";
|
|
49
|
+
text: string;
|
|
50
|
+
}[];
|
|
51
|
+
/** A table or column name from the command line, quoted, or a thrown usage error. */
|
|
52
|
+
export declare function ident(name: string | undefined, what: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* `--scope column` limits a bin to rows whose column is NULL, `--scope
|
|
55
|
+
* column=value` to rows where it equals the value: caper's platform pages
|
|
56
|
+
* have no caper, and its company pages share their tag paths.
|
|
57
|
+
*/
|
|
58
|
+
export declare function scopeSql(scope: string | undefined, param: number): {
|
|
59
|
+
sql: string;
|
|
60
|
+
values: unknown[];
|
|
61
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// maintenance.ts — what the `maintenance-log` and `mark-verified` bins share,
|
|
2
|
+
// kept pure so it is tested without a database.
|
|
3
|
+
//
|
|
4
|
+
// A wiki-sweep routine keeps its memory between runs (the rotation cursor,
|
|
5
|
+
// source watermarks, backlog, feedback and run history) in one wiki page's
|
|
6
|
+
// `metadata.state`, and that page shows a capped rendering of it. radix-wiki
|
|
7
|
+
// and caper each had the script, with the same caps and rules and a different
|
|
8
|
+
// table. The rules are here; the table is the bin's arguments.
|
|
9
|
+
//
|
|
10
|
+
// Every write compacts first. `backlog` holds only open work items, since a
|
|
11
|
+
// run diary entry belongs in `runHistory`, which it duplicates, and the
|
|
12
|
+
// append-only lists are capped. Uncapped, radix-wiki's log page reached 384 KB
|
|
13
|
+
// of content and 710 KB of state, and a revision per run made it over half of
|
|
14
|
+
// its revisions table. Two kinds of entry are pinned against the caps because
|
|
15
|
+
// a run cannot recreate them: a backlog item flagged for a human, and a
|
|
16
|
+
// watermark that banks a verdict on a whole class of site (a bot wall, a 403),
|
|
17
|
+
// which is what lets an audit spend no probes on its flag list.
|
|
18
|
+
//
|
|
19
|
+
// No subpath exports this: the bins and the tests import it from dist.
|
|
20
|
+
/** Held in state. */
|
|
21
|
+
const MAX = { backlog: 60, runs: 30, feedback: 40, sources: 150 };
|
|
22
|
+
/** Shown on the page. What a reader pays is words, and the entries run long. */
|
|
23
|
+
const SHOWN = { backlog: 15, runs: 5, feedback: 8, sources: 15 };
|
|
24
|
+
/** "run 214 (ecosystem): link audit CLEAN…" is a diary entry, not a work item. */
|
|
25
|
+
const JOURNAL = /^run\s+\d+\b/i;
|
|
26
|
+
const PINNED = /FLAG FOR A HUMAN/i;
|
|
27
|
+
const BANKED = /\bbanked\b|bot[- ]?(wall|block)|\b(403|429|999)\b|cloudflare/i;
|
|
28
|
+
const itemOf = (b) => (typeof b === 'string' ? b : b.item);
|
|
29
|
+
const recency = (a, b) => String(b[1]?.lastChecked ?? '').localeCompare(String(a[1]?.lastChecked ?? ''));
|
|
30
|
+
/** Drops what is closed or a diary entry, and caps what is append-only. Newest first. */
|
|
31
|
+
export function compact(state) {
|
|
32
|
+
// Undated items fall back to append order: a later index is newer.
|
|
33
|
+
const open = (state.backlog ?? [])
|
|
34
|
+
.map((b, i) => ({ status: 'open', ...(typeof b === 'string' ? { date: '', item: b } : b), i }))
|
|
35
|
+
.filter(b => b.status === 'open' && b.item && (PINNED.test(b.item) || !JOURNAL.test(b.item)))
|
|
36
|
+
.sort((a, b) => String(b.date ?? '').localeCompare(String(a.date ?? '')) || b.i - a.i);
|
|
37
|
+
const backlog = [...open.filter(b => PINNED.test(b.item)), ...open.filter(b => !PINNED.test(b.item))]
|
|
38
|
+
.slice(0, MAX.backlog)
|
|
39
|
+
.map(({ i: _i, ...b }) => b);
|
|
40
|
+
// Most source keys are one-off verdicts on a single site. Sorting by
|
|
41
|
+
// lastChecked lets the cap age out only probes no run has touched since.
|
|
42
|
+
const entries = Object.entries(state.sources ?? {});
|
|
43
|
+
const banked = ([, v]) => BANKED.test(String(v?.note ?? ''));
|
|
44
|
+
const sources = Object.fromEntries([...entries.filter(banked), ...entries.filter(e => !banked(e)).sort(recency).slice(0, MAX.sources)].sort(recency));
|
|
45
|
+
return {
|
|
46
|
+
...state,
|
|
47
|
+
backlog,
|
|
48
|
+
sources,
|
|
49
|
+
feedback: (state.feedback ?? []).slice(-MAX.feedback),
|
|
50
|
+
runHistory: (state.runHistory ?? []).slice(-MAX.runs),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** New backlog items, deduplicated against what is there, dated today unless they carry a date. */
|
|
54
|
+
export function addBacklog(state, items, today) {
|
|
55
|
+
const seen = new Set((state.backlog ?? []).map(itemOf));
|
|
56
|
+
const added = [];
|
|
57
|
+
for (const raw of items) {
|
|
58
|
+
const item = itemOf(raw);
|
|
59
|
+
if (!item || seen.has(item))
|
|
60
|
+
continue;
|
|
61
|
+
seen.add(item);
|
|
62
|
+
added.push({ date: (typeof raw === 'object' && raw.date) || today, item, status: 'open' });
|
|
63
|
+
}
|
|
64
|
+
return added;
|
|
65
|
+
}
|
|
66
|
+
const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
67
|
+
const more = (shown, total, what) => total > shown ? `<p><em>${total - shown} older ${what} held in state.</em></p>` : '';
|
|
68
|
+
const table = (head, rows) => `<table><tbody><tr>${head.map(h => `<td><strong>${h}</strong></td>`).join('')}</tr>` +
|
|
69
|
+
(rows.join('') || `<tr>${head.map(() => '<td>–</td>').join('')}</tr>`) +
|
|
70
|
+
'</tbody></table>';
|
|
71
|
+
/** The page's content blocks, re-rendered from state on every write. `routine` names who keeps it. */
|
|
72
|
+
export function render(state, routine, newId = () => crypto.randomUUID()) {
|
|
73
|
+
const rot = state.rotation ?? {};
|
|
74
|
+
const backlog = state.backlog ?? [];
|
|
75
|
+
const feedback = state.feedback ?? [];
|
|
76
|
+
const sources = Object.entries(state.sources ?? {}).sort(recency);
|
|
77
|
+
const block = (text) => ({ id: newId(), type: 'content', text });
|
|
78
|
+
const label = (x) => (typeof x === 'string' ? x : (x.handle ?? x.name ?? x.url ?? ''));
|
|
79
|
+
const sourceRows = sources.slice(0, SHOWN.sources).map(([key, v]) => {
|
|
80
|
+
const watch = (v.list ?? []).map(label).filter(Boolean);
|
|
81
|
+
const watching = watch.length ? ` — watching: ${esc(watch.slice(0, 8).join(', '))}${watch.length > 8 ? '…' : ''}` : '';
|
|
82
|
+
return `<tr><td>${esc(key)}</td><td>${esc(v.lastChecked || '–')}</td><td>${esc(v.note)}${watching}</td></tr>`;
|
|
83
|
+
});
|
|
84
|
+
const runRows = (state.runHistory ?? []).slice(-SHOWN.runs).reverse().map(r => {
|
|
85
|
+
const tweet = r.tweet && /^https?:\/\//.test(r.tweet) ? `<a href="${esc(r.tweet)}">link</a>` : esc(r.tweet);
|
|
86
|
+
return `<tr><td>${esc(r.date)}</td><td>${esc(r.category)}</td><td>${esc(r.summary)}</td><td>${tweet}</td></tr>`;
|
|
87
|
+
});
|
|
88
|
+
return [
|
|
89
|
+
block(`<p><em>Auto-maintained by the <code>${esc(routine)}</code> routine. Each run records what was swept, ` +
|
|
90
|
+
'advances the category rotation, logs ecosystem signals, and tracks community reception. ' +
|
|
91
|
+
'Do not hand-edit the state block – the routine overwrites it.</em></p>' +
|
|
92
|
+
(state.mission ? `<p><strong>Mission:</strong> ${esc(state.mission)}</p>` : '') +
|
|
93
|
+
(state.positioning ? `<p><strong>Positioning:</strong> ${esc(state.positioning)}</p>` : '')),
|
|
94
|
+
block('<h2>Current status</h2><table><tbody>' +
|
|
95
|
+
`<tr><td><strong>Next category in rotation</strong></td><td>${esc(rot.next || '–')}</td></tr>` +
|
|
96
|
+
`<tr><td><strong>Rotation order</strong></td><td>${esc((rot.order ?? []).join(' → '))}</td></tr>` +
|
|
97
|
+
`<tr><td><strong>Last run</strong></td><td>${esc(state.lastRun || '–')}</td></tr>` +
|
|
98
|
+
`<tr><td><strong>Total runs</strong></td><td>${esc(state.totalRuns ?? 0)}</td></tr>` +
|
|
99
|
+
'</tbody></table>'),
|
|
100
|
+
block(`<h2>Signal watermarks</h2>${table(['Source', 'Last checked', 'Note'], sourceRows)}` +
|
|
101
|
+
more(SHOWN.sources, sources.length, 'watermarks')),
|
|
102
|
+
block('<h2>Open backlog</h2>' +
|
|
103
|
+
(backlog.length
|
|
104
|
+
? `<ul>${backlog.slice(0, SHOWN.backlog).map(b => `<li>${esc(itemOf(b))}</li>`).join('')}</ul>` +
|
|
105
|
+
more(SHOWN.backlog, backlog.length, 'open items')
|
|
106
|
+
: '<p><em>Empty.</em></p>')),
|
|
107
|
+
block('<h2>Community feedback & reception</h2>' +
|
|
108
|
+
(feedback.length
|
|
109
|
+
? `<ul>${feedback
|
|
110
|
+
.slice(-SHOWN.feedback)
|
|
111
|
+
.reverse()
|
|
112
|
+
.map(f => `<li>${esc(typeof f === 'string' ? f : `${f.date ?? ''} – ${f.note}`)}</li>`)
|
|
113
|
+
.join('')}</ul>` + more(SHOWN.feedback, feedback.length, 'entries')
|
|
114
|
+
: '<p><em>Nothing logged yet.</em></p>')),
|
|
115
|
+
block(`<h2>Recent runs</h2>${table(['Date', 'Category', 'Summary', 'Tweet'], runRows)}`),
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
const IDENT = /^[a-z_][a-z0-9_]*$/;
|
|
119
|
+
/** A table or column name from the command line, quoted, or a thrown usage error. */
|
|
120
|
+
export function ident(name, what) {
|
|
121
|
+
if (!name || !IDENT.test(name))
|
|
122
|
+
throw new Error(`${what} must be a lower-case SQL identifier, got ${JSON.stringify(name)}`);
|
|
123
|
+
return `"${name}"`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `--scope column` limits a bin to rows whose column is NULL, `--scope
|
|
127
|
+
* column=value` to rows where it equals the value: caper's platform pages
|
|
128
|
+
* have no caper, and its company pages share their tag paths.
|
|
129
|
+
*/
|
|
130
|
+
export function scopeSql(scope, param) {
|
|
131
|
+
if (!scope)
|
|
132
|
+
return { sql: 'TRUE', values: [] };
|
|
133
|
+
const [column, value] = scope.split('=');
|
|
134
|
+
return value === undefined
|
|
135
|
+
? { sql: `${ident(column, '--scope')} IS NULL`, values: [] }
|
|
136
|
+
: { sql: `${ident(column, '--scope')} = $${param}`, values: [value] };
|
|
137
|
+
}
|
package/dist/maps.d.ts
CHANGED
|
@@ -3,12 +3,6 @@ export interface MapCoords {
|
|
|
3
3
|
lon: number;
|
|
4
4
|
zoom?: number | undefined;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
* Dig a coordinate pair out of a maps URL. Four shapes in descending
|
|
8
|
-
* specificity: the `@lat,lon,zoom` path segment, a `/search/lat,lon`, the
|
|
9
|
-
* `!3d…!4d…` data blob, and finally an `ll`/`sll` query parameter.
|
|
10
|
-
*/
|
|
11
|
-
export declare function extractCoordsFromUrl(url: string): MapCoords | null;
|
|
12
6
|
/**
|
|
13
7
|
* An embeddable URL for `url`, or null when it is not a map link this
|
|
14
8
|
* understands. Already-embeddable URLs pass through untouched.
|
package/dist/maps.js
CHANGED
|
@@ -12,6 +12,8 @@ function mapsEmbedUrl(lat, lon, zoom = 15) {
|
|
|
12
12
|
* Dig a coordinate pair out of a maps URL. Four shapes in descending
|
|
13
13
|
* specificity: the `@lat,lon,zoom` path segment, a `/search/lat,lon`, the
|
|
14
14
|
* `!3d…!4d…` data blob, and finally an `ll`/`sll` query parameter.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
15
17
|
*/
|
|
16
18
|
export function extractCoordsFromUrl(url) {
|
|
17
19
|
const coords = url.match(/@(-?\d+\.?\d*),(-?\d+\.?\d*),?(\d+\.?\d*)?z?/);
|
package/dist/markdown.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export declare function decodeEntities(s: string): string;
|
|
2
2
|
/** Inline-level HTML → markdown. Applied inside cells, list items, headings. */
|
|
3
3
|
export declare function inlineToMarkdown(html: string): string;
|
|
4
|
-
/** One `<table>` → a GFM table. Falls back to nothing when there are no rows. */
|
|
5
|
-
export declare function tableToMarkdown(html: string): string;
|
|
6
4
|
/** Block-level HTML → markdown. */
|
|
7
5
|
export declare function htmlToMarkdown(html: string): string;
|
|
8
6
|
export interface FrontmatterFields {
|
package/dist/markdown.js
CHANGED
|
@@ -62,7 +62,7 @@ export function inlineToMarkdown(html) {
|
|
|
62
62
|
.replace(/\s+/g, ' ')
|
|
63
63
|
.trim();
|
|
64
64
|
}
|
|
65
|
-
/** One `<table>` → a GFM table. Falls back to nothing when there are no rows. */
|
|
65
|
+
/** One `<table>` → a GFM table. Falls back to nothing when there are no rows. @internal */
|
|
66
66
|
export function tableToMarkdown(html) {
|
|
67
67
|
const rows = [...html.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)].map(m => [...m[1].matchAll(/<(t[hd])\b[^>]*>([\s\S]*?)<\/\1>/gi)].map(c => inlineToMarkdown(c[2]).replace(/\|/g, '\\|')));
|
|
68
68
|
if (!rows.length)
|
package/dist/mcp.d.ts
CHANGED
|
@@ -265,20 +265,6 @@ export declare const toolText: (id: RpcRequest["id"], text: string, isError?: bo
|
|
|
265
265
|
}[];
|
|
266
266
|
};
|
|
267
267
|
};
|
|
268
|
-
/** The methods one era has and the other does not. Everything else is shared. */
|
|
269
|
-
declare const ERA_METHODS: {
|
|
270
|
-
readonly legacy: readonly ["initialize", "ping"];
|
|
271
|
-
readonly modern: readonly ["server/discover"];
|
|
272
|
-
};
|
|
273
|
-
type Era = keyof typeof ERA_METHODS;
|
|
274
|
-
/** Everything the dispatcher needs that is not in the JSON-RPC entry itself. */
|
|
275
|
-
interface Dispatch {
|
|
276
|
-
protocolVersion: McpProtocolVersion;
|
|
277
|
-
era: Era;
|
|
278
|
-
request?: Request;
|
|
279
|
-
}
|
|
280
|
-
/** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
|
|
281
|
-
export declare function handleMcp(body: RpcRequest | RpcRequest[], config: McpServerConfig, dispatch?: Dispatch): Promise<object | object[] | null>;
|
|
282
268
|
export declare const MCP_CORS: Record<string, string>;
|
|
283
269
|
export declare function withMcpCors<T extends Response>(res: T): T;
|
|
284
270
|
export declare function mcpOptions(): Response;
|
package/dist/mcp.js
CHANGED
|
@@ -433,7 +433,7 @@ async function handleRpc(req, config, dispatch) {
|
|
|
433
433
|
return rpcError(id, -32603, 'Internal error');
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
|
-
/** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. */
|
|
436
|
+
/** Dispatch a parsed body. `null` means notification-only — answer 202, not 200. @internal */
|
|
437
437
|
export async function handleMcp(body, config, dispatch = { protocolVersion: ASSUMED_VERSION, era: 'legacy' }) {
|
|
438
438
|
const maxBatch = config.maxBatch ?? DEFAULT_MAX_BATCH;
|
|
439
439
|
const isBatch = Array.isArray(body);
|
package/dist/metadata.d.ts
CHANGED
|
@@ -152,17 +152,6 @@ export declare function faqPageLd(pairs: readonly QaPair[], url: string): LdNode
|
|
|
152
152
|
* which is the number that matters to whoever writes the title.
|
|
153
153
|
*/
|
|
154
154
|
export declare const TITLE_BUDGET = 60;
|
|
155
|
-
/**
|
|
156
|
-
* Past this, a title is not merely trimmed but cut into.
|
|
157
|
-
*
|
|
158
|
-
* Between the budget and this limit a title loses its tail, which is often the
|
|
159
|
-
* site name and no loss at all. Past it, the words carrying what the page is
|
|
160
|
-
* about are going too: the radix.wiki ideas board spent its first sixteen
|
|
161
|
-
* characters on a working group and reached 93, and the symptom pages on
|
|
162
|
-
* acuiq.com reached 88 before naming the symptom. That is the line worth
|
|
163
|
-
* failing a build over; the band below it is worth reporting and no more.
|
|
164
|
-
*/
|
|
165
|
-
export declare const TITLE_LIMIT = 70;
|
|
166
155
|
/**
|
|
167
156
|
* The document `<title>` for a page: its short form where the page carries one,
|
|
168
157
|
* else its own title.
|
|
@@ -196,6 +185,20 @@ export declare function fittingTitle(variants: readonly string[], opts?: {
|
|
|
196
185
|
suffix?: string;
|
|
197
186
|
limit?: number;
|
|
198
187
|
}): string;
|
|
188
|
+
/** Characters a search result shows of a meta description before it cuts. */
|
|
189
|
+
export declare const DESCRIPTION_LIMIT = 160;
|
|
190
|
+
/**
|
|
191
|
+
* A meta description from any prose: HTML, markdown or plain. Tags, entities,
|
|
192
|
+
* link targets and bare URLs go; what is left is fitted to `limit` as whole
|
|
193
|
+
* sentences where they fill at least half of it, else cut at a word and marked
|
|
194
|
+
* with an ellipsis. Never longer than `limit`.
|
|
195
|
+
*
|
|
196
|
+
* Each of the four sites had its own: one clamped at 150 mid-word, one sliced
|
|
197
|
+
* at 160 with no mark, one fitted sentences, and one kept whatever sentences
|
|
198
|
+
* fitted however few. Pass a page's own excerpt where it has one and its
|
|
199
|
+
* opening (`openingText`) where it does not: `describe(excerpt || openingText(blocks))`.
|
|
200
|
+
*/
|
|
201
|
+
export declare function describe(text: string | null | undefined, limit?: number): string;
|
|
199
202
|
/**
|
|
200
203
|
* `pageMetadata` bound to a site's name, locale and handle, the three fields
|
|
201
204
|
* Next drops from every page that sets its own card. Two wikis wrapped it to
|
package/dist/metadata.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// with no image while their og:image named a card, one listed citations, one
|
|
20
20
|
// collection page had no ItemList at all.
|
|
21
21
|
import { decodeEntities } from './markdown.js';
|
|
22
|
+
import { plainText } from './text.js';
|
|
22
23
|
export function pageMetadata(o) {
|
|
23
24
|
const { title, description, url, type = 'website', image, imageSize = { width: 1200, height: 630 }, siteName, locale, handle, markdownTwin, article } = o;
|
|
24
25
|
const images = image ? [{ url: image, ...imageSize, alt: o.imageAlt ?? title }] : undefined;
|
|
@@ -175,6 +176,8 @@ export const TITLE_BUDGET = 60;
|
|
|
175
176
|
* characters on a working group and reached 93, and the symptom pages on
|
|
176
177
|
* acuiq.com reached 88 before naming the symptom. That is the line worth
|
|
177
178
|
* failing a build over; the band below it is worth reporting and no more.
|
|
179
|
+
*
|
|
180
|
+
* @internal
|
|
178
181
|
*/
|
|
179
182
|
export const TITLE_LIMIT = 70;
|
|
180
183
|
/**
|
|
@@ -216,6 +219,36 @@ export function fittingTitle(variants, opts = {}) {
|
|
|
216
219
|
return '';
|
|
217
220
|
return usable.find(v => v.length + suffix.length <= limit) ?? usable[usable.length - 1];
|
|
218
221
|
}
|
|
222
|
+
/** Characters a search result shows of a meta description before it cuts. */
|
|
223
|
+
export const DESCRIPTION_LIMIT = 160;
|
|
224
|
+
/**
|
|
225
|
+
* A meta description from any prose: HTML, markdown or plain. Tags, entities,
|
|
226
|
+
* link targets and bare URLs go; what is left is fitted to `limit` as whole
|
|
227
|
+
* sentences where they fill at least half of it, else cut at a word and marked
|
|
228
|
+
* with an ellipsis. Never longer than `limit`.
|
|
229
|
+
*
|
|
230
|
+
* Each of the four sites had its own: one clamped at 150 mid-word, one sliced
|
|
231
|
+
* at 160 with no mark, one fitted sentences, and one kept whatever sentences
|
|
232
|
+
* fitted however few. Pass a page's own excerpt where it has one and its
|
|
233
|
+
* opening (`openingText`) where it does not: `describe(excerpt || openingText(blocks))`.
|
|
234
|
+
*/
|
|
235
|
+
export function describe(text, limit = DESCRIPTION_LIMIT) {
|
|
236
|
+
const t = plainText(text ?? '')
|
|
237
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
238
|
+
.replace(/\((?:https?:\/\/|mailto:|\/|#)[^)\s]*\)/g, '')
|
|
239
|
+
.replace(/https?:\/\/\S+/g, '')
|
|
240
|
+
.replace(/\(\s*\)/g, '')
|
|
241
|
+
.replace(/\s+([,.;:!?])/g, '$1')
|
|
242
|
+
.replace(/\s{2,}/g, ' ')
|
|
243
|
+
.trim();
|
|
244
|
+
if (t.length <= limit)
|
|
245
|
+
return t;
|
|
246
|
+
const sentences = t.slice(0, limit + 1).match(/^.*[.!?](?=\s)/)?.[0];
|
|
247
|
+
if (sentences && sentences.length >= limit / 2)
|
|
248
|
+
return sentences;
|
|
249
|
+
const cut = t.lastIndexOf(' ', limit - 1);
|
|
250
|
+
return `${t.slice(0, cut > 0 ? cut : limit - 1).replace(/[\s,;:–—-]+$/, '')}…`;
|
|
251
|
+
}
|
|
219
252
|
// ---- the site's own identity ----------------------------------------------------
|
|
220
253
|
/**
|
|
221
254
|
* `pageMetadata` bound to a site's name, locale and handle, the three fields
|
package/dist/pagination.d.ts
CHANGED
|
@@ -42,11 +42,6 @@ export declare function listEnvelope<T>(pages: T[], total: number, page: number,
|
|
|
42
42
|
note?: string;
|
|
43
43
|
pages: T[];
|
|
44
44
|
};
|
|
45
|
-
/** The `skip`/`take` an ORM wants, from the same clamped pair. */
|
|
46
|
-
export declare function toOffset({ page, pageSize }: Pagination): {
|
|
47
|
-
skip: number;
|
|
48
|
-
take: number;
|
|
49
|
-
};
|
|
50
45
|
/**
|
|
51
46
|
* The entries either side of the current one in an already-ordered list.
|
|
52
47
|
*
|
package/dist/pagination.js
CHANGED
|
@@ -52,10 +52,6 @@ export function listEnvelope(pages, total, page, pageSize, emptyNote) {
|
|
|
52
52
|
pages,
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
-
/** The `skip`/`take` an ORM wants, from the same clamped pair. */
|
|
56
|
-
export function toOffset({ page, pageSize }) {
|
|
57
|
-
return { skip: (page - 1) * pageSize, take: pageSize };
|
|
58
|
-
}
|
|
59
55
|
/**
|
|
60
56
|
* The entries either side of the current one in an already-ordered list.
|
|
61
57
|
*
|
package/dist/passkey.js
CHANGED
|
@@ -10,21 +10,8 @@
|
|
|
10
10
|
// not open the deployed site, and one saved on the site does not open localhost.
|
|
11
11
|
//
|
|
12
12
|
// Like analytics, the package owns the SQL and the app owns the connection. The
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// model Passkey {
|
|
16
|
-
// id String @id // the credential id, base64url
|
|
17
|
-
// publicKey String // base64url
|
|
18
|
-
// counter Int
|
|
19
|
-
// transports String[]
|
|
20
|
-
// createdAt DateTime @default(now()) @db.Timestamptz(3)
|
|
21
|
-
// }
|
|
22
|
-
//
|
|
23
|
-
// model PasskeyToken {
|
|
24
|
-
// hash String @id // sha-256 of the token, base64url
|
|
25
|
-
// kind String // session | invite | challenge
|
|
26
|
-
// expiresAt DateTime @db.Timestamptz(3)
|
|
27
|
-
// }
|
|
13
|
+
// two tables are Prisma models shipped in `prisma/passkey.prisma`, which the
|
|
14
|
+
// `prisma-models` bin copies into the app's schema folder.
|
|
28
15
|
//
|
|
29
16
|
// Every token is stored as its hash, so a read of the table opens nothing. A
|
|
30
17
|
// challenge is spent the moment a response quotes it, and an invite the moment
|