@333eco/corpus 1.2.5 → 2.0.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 +70 -0
- package/dist/corpus.json +1420 -127
- package/package.json +4 -3
- package/src/base-tools.mjs +72 -0
- package/src/program-tools.mjs +181 -0
- package/src/prompts.mjs +134 -0
- package/src/resources.mjs +215 -0
- package/src/results.mjs +40 -0
- package/src/server.mjs +94 -52
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@333eco/corpus",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "An MCP server for an open-licensed corpus, served with verifiable provenance
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "An MCP server for an open-licensed corpus, served with verifiable provenance \u2014 every document carries its sha256, DOI and OpenTimestamps proof so a consuming agent can check its own citation.",
|
|
5
5
|
"license": "CC0-1.0",
|
|
6
6
|
"author": "Thon Ly",
|
|
7
7
|
"homepage": "https://thonly.org/research",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"start": "node src/server.mjs",
|
|
42
42
|
"build": "node scripts/build-index.mjs --from ../../../TH/publications ../../../H3/publications ../../../missaquarius.org/missaquarius.org",
|
|
43
|
-
"check": "node scripts/build-index.mjs --check --from ../../../TH/publications ../../../H3/publications ../../../missaquarius.org/missaquarius.org"
|
|
43
|
+
"check": "node scripts/check-parity.mjs && node scripts/build-index.mjs --check --from ../../../TH/publications ../../../H3/publications ../../../missaquarius.org/missaquarius.org",
|
|
44
|
+
"check:parity": "node scripts/check-parity.mjs"
|
|
44
45
|
},
|
|
45
46
|
"publishConfig": {
|
|
46
47
|
"access": "public"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// The three base tools, shared by both surfaces.
|
|
2
|
+
//
|
|
3
|
+
// ⭐ Definitions are shared; DISPATCH is not. `callTool` takes different arguments
|
|
4
|
+
// on each surface (the worker is handed the corpus per request, the stdio server
|
|
5
|
+
// closes over it) and those functions are genuinely different code. What must
|
|
6
|
+
// never differ is what the two servers ADVERTISE — a client picks a tool by name
|
|
7
|
+
// and schema, so a drifted definition is a client calling something that isn't
|
|
8
|
+
// there. The envelope stays duplicated on purpose; that one is twenty lines and
|
|
9
|
+
// diffable by eye.
|
|
10
|
+
|
|
11
|
+
// ⭐⭐ EVERY TOOL HERE IS READ-ONLY, AND SAYING SO IS NOT DECORATION. A client that
|
|
12
|
+
// knows a call cannot mutate anything can stop putting a confirmation dialog in
|
|
13
|
+
// front of a corpus lookup. This server has no write path at all: it opens exactly
|
|
14
|
+
// one file, dist/corpus.json, and never writes.
|
|
15
|
+
// ⚠️ `openWorldHint: false` is the honest value — the corpus is a closed, fixed
|
|
16
|
+
// set for the life of a build, not an open-ended external system.
|
|
17
|
+
export const READ_ONLY = {
|
|
18
|
+
readOnlyHint: true,
|
|
19
|
+
destructiveHint: false,
|
|
20
|
+
idempotentHint: true,
|
|
21
|
+
openWorldHint: false
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const BASE_TOOLS = [
|
|
25
|
+
{
|
|
26
|
+
name: "search_corpus",
|
|
27
|
+
title: "Search the corpus",
|
|
28
|
+
annotations: { ...READ_ONLY, title: "Search the corpus" },
|
|
29
|
+
description:
|
|
30
|
+
"Full-text search across the open-licensed corpus. Returns matching documents with a provenance envelope " +
|
|
31
|
+
"and a short excerpt around each match — not the full text; call get_document for that. Every result can " +
|
|
32
|
+
"be independently verified via its sha256, DOI and OpenTimestamps proof.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
query: { type: "string", description: "Text to search for. Case-insensitive." },
|
|
37
|
+
genre: { type: "string", description: "Optional: restrict to a genre, e.g. essays, defensive-publications, positions, white-papers." },
|
|
38
|
+
limit: { type: "number", description: "Maximum documents to return. Default 10." }
|
|
39
|
+
},
|
|
40
|
+
required: ["query"]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "get_document",
|
|
45
|
+
title: "Read one document in full",
|
|
46
|
+
annotations: { ...READ_ONLY, title: "Read one document in full" },
|
|
47
|
+
description:
|
|
48
|
+
"Return one document in full, with its provenance envelope. The text is the canonical source — never a " +
|
|
49
|
+
"summary — so its sha256 can be checked against the envelope and against the anchored proof.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: { slug: { type: "string", description: "Document slug, as returned by search_corpus or list_documents." } },
|
|
53
|
+
required: ["slug"]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "list_documents",
|
|
58
|
+
title: "List the corpus",
|
|
59
|
+
annotations: { ...READ_ONLY, title: "List the corpus" },
|
|
60
|
+
description:
|
|
61
|
+
"List the corpus: slugs, titles, genres, licences and provenance summaries, without full text. Use to " +
|
|
62
|
+
"orient before searching, or to enumerate what is available under a given licence.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
genre: { type: "string", description: "Optional: restrict to a genre — essays, defensive-publications, positions, white-papers, letters, program." },
|
|
67
|
+
category: { type: "string", description: "Optional: restrict to a topic category — institutional (the four-body architecture and the institution itself), mechanism, alignment, essays, letters, program, capabilities. The response lists every category with its count." },
|
|
68
|
+
licence: { type: "string", description: "Optional: restrict to a licence id, e.g. CC0-1.0 or CC-BY-4.0." }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
];
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// The research-program tools, shared by both surfaces.
|
|
2
|
+
//
|
|
3
|
+
// ⭐ WHY THIS IS A MODULE AND THE ENVELOPE IS NOT. The envelope is deliberately
|
|
4
|
+
// duplicated between the stdio server and the worker "so the two can be diffed by
|
|
5
|
+
// eye". That argument works for twenty lines of object literal. It does not work
|
|
6
|
+
// for a tool surface: these three tools are ~120 lines of filtering, fallback and
|
|
7
|
+
// wording, and a hand-maintained second copy of that is how corpus.333.eco ends up
|
|
8
|
+
// advertising tools the npm package does not have, or worse, answering the same
|
|
9
|
+
// question differently. The repo already refuses to let the two surfaces disagree
|
|
10
|
+
// about what a DOCUMENT says; this is the same refusal applied to what a TOOL says.
|
|
11
|
+
//
|
|
12
|
+
// ⚠️ The envelope is INJECTED rather than imported, so each surface keeps its own
|
|
13
|
+
// — that duplication is intentional and is not undone here.
|
|
14
|
+
//
|
|
15
|
+
// ⚠️ Only bundles JavaScript. The corpus itself stays a static asset in the worker
|
|
16
|
+
// (1.61 MB gzipped, over the script cap); this module is a few kilobytes of logic.
|
|
17
|
+
|
|
18
|
+
import { READ_ONLY } from "./base-tools.mjs";
|
|
19
|
+
import { structured } from "./results.mjs";
|
|
20
|
+
|
|
21
|
+
export const PROGRAM_TOOLS = [
|
|
22
|
+
{
|
|
23
|
+
name: "list_predictions",
|
|
24
|
+
title: "List the pre-registered predictions",
|
|
25
|
+
annotations: { ...READ_ONLY, title: "List the pre-registered predictions" },
|
|
26
|
+
description:
|
|
27
|
+
"List the pre-registered predictions of the Which Way Value Moves research program, with each one's " +
|
|
28
|
+
"registered falsifier and current status. Filterable by state, chapter, level and stating paper. Use to " +
|
|
29
|
+
"find what would falsify a claim, or what has actually been run — the program registers what could show " +
|
|
30
|
+
"it wrong, so an unrun prediction is a disclosure, not a gap.",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
state: { type: "string", description: "Optional facet: unrun, running, run, contradicted, retired, other. The verbatim status is always returned beside it." },
|
|
35
|
+
chapter: { type: "string", description: "Optional, matched as a substring: e.g. \"Core-level\", \"Scale\"." },
|
|
36
|
+
level: { type: "string", description: "Optional: core (failing one ends the program) or chapter." },
|
|
37
|
+
paper: { type: "string", description: "Optional: slug of the stating paper, e.g. co-presence-gated-redemption." }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "get_prediction",
|
|
43
|
+
title: "Read one prediction, with its paper's proof",
|
|
44
|
+
annotations: { ...READ_ONLY, title: "Read one prediction, with its paper's proof" },
|
|
45
|
+
description:
|
|
46
|
+
"Return one pre-registered prediction by identifier (e.g. P-L2) with its registered wording, its " +
|
|
47
|
+
"falsifier, its status, and the PROVENANCE ENVELOPE OF THE PAPER THAT REGISTERED IT — hash, DOI and " +
|
|
48
|
+
"OpenTimestamps command — so the registration itself can be verified rather than trusted.",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: { id: { type: "string", description: "Prediction identifier, e.g. P-L2, P-K1, P-CS5." } },
|
|
52
|
+
required: ["id"]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "get_program",
|
|
57
|
+
title: "The research program in one call",
|
|
58
|
+
annotations: { ...READ_ONLY, title: "The research program in one call" },
|
|
59
|
+
description:
|
|
60
|
+
"The shape of the research program in one call: its hard core, its four chapters, its stopping rule, its " +
|
|
61
|
+
"revision rule and its own count reconciliation — all verbatim, drawn from the two documents that state " +
|
|
62
|
+
"it, with both provenance envelopes attached. Use to orient before querying predictions.",
|
|
63
|
+
inputSchema: { type: "object", properties: {} }
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export const PROGRAM_TOOL_NAMES = PROGRAM_TOOLS.map((t) => t.name);
|
|
68
|
+
|
|
69
|
+
export const PROGRAM_INSTRUCTIONS =
|
|
70
|
+
" This corpus also carries a research program with a public register of falsifiable predictions: " +
|
|
71
|
+
"list_predictions, get_prediction and get_program expose what would show the program wrong and what has " +
|
|
72
|
+
"actually been run. A prediction's authority is the paper that registered it, so those tools return that " +
|
|
73
|
+
"paper's provenance envelope rather than the register's.";
|
|
74
|
+
|
|
75
|
+
// A prediction as returned: the record, plus its stating paper resolved to a real
|
|
76
|
+
// envelope. ⚠️ `stating_paper` gains nothing where the register itself is the
|
|
77
|
+
// origin, or where the row names no paper — never silently filled in.
|
|
78
|
+
const view = (p, bySlug, envelope) => {
|
|
79
|
+
const paper = p.stating_paper.slug ? bySlug.get(p.stating_paper.slug) : null;
|
|
80
|
+
return {
|
|
81
|
+
...p,
|
|
82
|
+
stating_paper: {
|
|
83
|
+
...p.stating_paper,
|
|
84
|
+
...(paper
|
|
85
|
+
? {
|
|
86
|
+
title: paper.title,
|
|
87
|
+
// The whole point: verify the registration against the paper
|
|
88
|
+
// that made it, not against the index that lists it.
|
|
89
|
+
provenance: envelope(paper).provenance,
|
|
90
|
+
full_text: `call get_document with slug "${paper.slug}"`
|
|
91
|
+
}
|
|
92
|
+
: {})
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const callProgramTool = ({ program, bySlug, envelope }, name, args) => {
|
|
98
|
+
if (!program) throw new Error("this index carries no research program block");
|
|
99
|
+
|
|
100
|
+
if (name === "list_predictions") {
|
|
101
|
+
const eq = (a, b) => !b || String(b).toLowerCase() === String(a ?? "").toLowerCase();
|
|
102
|
+
const hits = program.predictions.filter(
|
|
103
|
+
(p) =>
|
|
104
|
+
eq(p.state, args?.state) &&
|
|
105
|
+
eq(p.level, args?.level) &&
|
|
106
|
+
(!args?.chapter || p.chapter.toLowerCase().includes(String(args.chapter).toLowerCase())) &&
|
|
107
|
+
eq(p.stating_paper.slug, args?.paper)
|
|
108
|
+
);
|
|
109
|
+
return structured({
|
|
110
|
+
question: program.question,
|
|
111
|
+
matched: hits.length,
|
|
112
|
+
of: program.prediction_count,
|
|
113
|
+
// ⚠️ RETURNED ON EVERY LIST, UNFILTERED, because a set of predictions
|
|
114
|
+
// means nothing without the honest denominator beside it: this program
|
|
115
|
+
// has 2 run, both desk censuses, and 0 field tests. A caller shown only
|
|
116
|
+
// its own filter could read a long list as a body of evidence.
|
|
117
|
+
by_state: program.predictions.reduce((a, x) => ((a[x.state] = (a[x.state] ?? 0) + 1), a), {}),
|
|
118
|
+
predictions: hits.map((p) => view(p, bySlug, envelope))
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (name === "get_prediction") {
|
|
123
|
+
const id = String(args?.id ?? "").trim();
|
|
124
|
+
const hit = program.predictions.find((x) => x.id && x.id.toLowerCase() === id.toLowerCase());
|
|
125
|
+
if (hit) return structured(view(hit, bySlug, envelope));
|
|
126
|
+
|
|
127
|
+
// ⭐ A WITHHELD PREDICTION IS ANSWERED, NOT DENIED. The register publishes
|
|
128
|
+
// the identifier and the reason precisely so the total is honest; a lookup
|
|
129
|
+
// that said "no such prediction" would undo that on the server's side.
|
|
130
|
+
const w = program.withheld.find((x) => x.id.toLowerCase() === id.toLowerCase());
|
|
131
|
+
if (w) {
|
|
132
|
+
return structured({
|
|
133
|
+
id: w.id,
|
|
134
|
+
withheld: true,
|
|
135
|
+
reason: w.reason,
|
|
136
|
+
note: "Recorded in the register so the total is honest; the wording is not published."
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const out = program.instrumented_outside_core.find((x) =>
|
|
140
|
+
x.ids.split(/,\s*/).map((s) => s.trim().toLowerCase()).includes(id.toLowerCase())
|
|
141
|
+
);
|
|
142
|
+
if (out) {
|
|
143
|
+
return structured({
|
|
144
|
+
id,
|
|
145
|
+
outside_core: true,
|
|
146
|
+
paper: out.paper,
|
|
147
|
+
subject: out.subject,
|
|
148
|
+
note: "Named by a paper but testing something other than the direction core — not evidence for or against the program."
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`no prediction "${id}". Call list_predictions to see the register, or get_program for its shape.`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (name === "get_program") {
|
|
155
|
+
return structured({
|
|
156
|
+
question: program.question,
|
|
157
|
+
documents: program.documents.map((slug) => {
|
|
158
|
+
const d = bySlug.get(slug);
|
|
159
|
+
return d ? { ...envelope(d), full_text: `call get_document with slug "${slug}"` } : { slug };
|
|
160
|
+
}),
|
|
161
|
+
hard_core: program.hard_core,
|
|
162
|
+
four_chapters: program.four_chapters,
|
|
163
|
+
chapters: program.chapters,
|
|
164
|
+
honest_limits: program.honest_limits,
|
|
165
|
+
stopping_rule: program.stopping_rule,
|
|
166
|
+
revision_rule: program.revision_rule,
|
|
167
|
+
verification: program.verification,
|
|
168
|
+
counts: program.counts,
|
|
169
|
+
// The register's arithmetic, re-derived from its own tables at build
|
|
170
|
+
// time rather than copied from its Summary.
|
|
171
|
+
reconciliation: program.reconciliation,
|
|
172
|
+
withheld: program.withheld,
|
|
173
|
+
instrumented_outside_core: program.instrumented_outside_core,
|
|
174
|
+
// ⚠️ Surfaced, never buried: an attribution the builder could not
|
|
175
|
+
// confirm against the paper it names.
|
|
176
|
+
unverified_attributions: program.unverified_attributions
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
throw new Error(`unknown program tool: ${name}`);
|
|
181
|
+
};
|
package/src/prompts.mjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Prompts — worked examples of how to use this server, shared by both surfaces.
|
|
2
|
+
//
|
|
3
|
+
// ⛔⛔ THE GUARD, AND IT IS THE WHOLE REASON THESE ARE SAFE TO SHIP: A PROMPT HERE
|
|
4
|
+
// MAY DESCRIBE THE API. IT MAY NEVER DESCRIBE THE SUBJECT MATTER. The moment one
|
|
5
|
+
// of these says something about gratitude, the gift, the four bodies or what the
|
|
6
|
+
// research program has shown, this server has begun editorialising on its own
|
|
7
|
+
// corpus — and a retrieval server with opinions about its documents is exactly the
|
|
8
|
+
// thing the provenance envelope exists to make unnecessary. Read every line below
|
|
9
|
+
// as an answer to "how do I check this?" and never to "what should I conclude?"
|
|
10
|
+
//
|
|
11
|
+
// ⚠️ A SECOND, NARROWER REFUSAL, recorded because it was the tempting version:
|
|
12
|
+
// there is no `verify_before_citing` prompt telling a model to behave well. That
|
|
13
|
+
// would be a RULE where the server already has a PROPERTY — every document, by
|
|
14
|
+
// every route, arrives with a provenance header the reader must actively strip.
|
|
15
|
+
// A rule that fires only when a human picks it from a menu is the weak form of a
|
|
16
|
+
// guarantee that already fires always. These prompts demonstrate the API; they do
|
|
17
|
+
// not ask anyone to be careful.
|
|
18
|
+
|
|
19
|
+
export const PROMPTS = [
|
|
20
|
+
{
|
|
21
|
+
name: "orient",
|
|
22
|
+
title: "What is in this corpus?",
|
|
23
|
+
description:
|
|
24
|
+
"A worked example of the discovery tools: what genres and topic categories exist, how many documents " +
|
|
25
|
+
"carry proofs, and where the research program's own statement of itself lives. Calls tools; asserts nothing.",
|
|
26
|
+
arguments: []
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "verify_a_quote",
|
|
30
|
+
title: "Check a quotation against its source",
|
|
31
|
+
description:
|
|
32
|
+
"A worked example of the verification path: fetch a document, then check the served text against the " +
|
|
33
|
+
"hash, the DOI and the OpenTimestamps proof its envelope names. Shows what to run, not what to believe.",
|
|
34
|
+
arguments: [
|
|
35
|
+
{ name: "slug", description: "Document slug, e.g. co-presence-gated-redemption. Completable.", required: true },
|
|
36
|
+
{ name: "quote", description: "Optional: the passage you intend to cite.", required: false }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "what_would_falsify",
|
|
41
|
+
title: "Find what would show a claim wrong",
|
|
42
|
+
description:
|
|
43
|
+
"A worked example of the program tools: search the prediction register for the registered falsifier of a " +
|
|
44
|
+
"claim, then resolve it to the paper that registered it and verify THAT paper. Demonstrates the one " +
|
|
45
|
+
"non-obvious move in this API — a prediction's authority is its stating paper, not the register.",
|
|
46
|
+
arguments: [{ name: "claim", description: "The claim or topic you want to test, in your own words.", required: true }]
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const PROMPT_NAMES = PROMPTS.map((p) => p.name);
|
|
51
|
+
|
|
52
|
+
const message = (text) => ({ role: "user", content: { type: "text", text } });
|
|
53
|
+
|
|
54
|
+
export const getPrompt = (name, args) => {
|
|
55
|
+
if (name === "orient") {
|
|
56
|
+
return {
|
|
57
|
+
description: "Discover the shape of the corpus using its own tools.",
|
|
58
|
+
messages: [
|
|
59
|
+
message(
|
|
60
|
+
"Show me what this corpus contains, using the server's tools rather than your prior knowledge.\n\n" +
|
|
61
|
+
"1. Call list_documents with no arguments. Report the total, the licence split, and the `categories` map — " +
|
|
62
|
+
"those are the corpus's own shelves, and `institutional` is where the institution describes itself.\n" +
|
|
63
|
+
"2. Call list_documents with category \"institutional\" and list what is there.\n" +
|
|
64
|
+
"3. Call get_program to see the research program's own statement of its hard core, its chapters and its " +
|
|
65
|
+
"count reconciliation.\n\n" +
|
|
66
|
+
"Quote the documents' own words where you summarise them, and name the slug you took each claim from. " +
|
|
67
|
+
"Do not fill gaps from memory: if the corpus does not cover something, say that it does not."
|
|
68
|
+
)
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (name === "verify_a_quote") {
|
|
74
|
+
const slug = String(args?.slug ?? "").trim();
|
|
75
|
+
if (!slug) {
|
|
76
|
+
const e = new Error("verify_a_quote requires a `slug` argument");
|
|
77
|
+
e.code = -32602;
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
const quote = String(args?.quote ?? "").trim();
|
|
81
|
+
return {
|
|
82
|
+
description: `Verify a passage of ${slug} against its anchored proof.`,
|
|
83
|
+
messages: [
|
|
84
|
+
message(
|
|
85
|
+
`Check a quotation from \`${slug}\` against its source, using this server's provenance envelope.\n\n` +
|
|
86
|
+
`1. Call get_document with slug "${slug}". The document arrives in content[0].text behind a ` +
|
|
87
|
+
"[PROVENANCE] header; the typed envelope is in structuredContent.\n" +
|
|
88
|
+
(quote
|
|
89
|
+
? `2. Find this passage in the text and report whether it appears verbatim:\n\n "${quote}"\n\n`
|
|
90
|
+
: "2. Choose the passage you intend to cite and report it verbatim.\n\n") +
|
|
91
|
+
"3. Run the envelope's own `verify.sha256` command and compare the result with `provenance.sha256`. " +
|
|
92
|
+
"Report whether they match, and note that the hash covers the complete source file — metadata block " +
|
|
93
|
+
"included — and not the body alone.\n" +
|
|
94
|
+
"4. If `provenance.doi` is present, give the DOI to cite. If the document's status is `living`, cite " +
|
|
95
|
+
"the concept DOI and verify against the version DOI, as the envelope's `citation` block says.\n" +
|
|
96
|
+
"5. If `licence.attribution_required` is true, name whom to attribute.\n\n" +
|
|
97
|
+
"Report what the commands actually returned. Do not vouch for the text on the server's behalf."
|
|
98
|
+
)
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (name === "what_would_falsify") {
|
|
104
|
+
const claim = String(args?.claim ?? "").trim();
|
|
105
|
+
if (!claim) {
|
|
106
|
+
const e = new Error("what_would_falsify requires a `claim` argument");
|
|
107
|
+
e.code = -32602;
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
description: "Find the registered falsifier for a claim, and verify where it was registered.",
|
|
112
|
+
messages: [
|
|
113
|
+
message(
|
|
114
|
+
`Find out what would show this claim wrong, according to the corpus's own prediction register:\n\n` +
|
|
115
|
+
` "${claim}"\n\n` +
|
|
116
|
+
"1. Call list_predictions and look for predictions bearing on it. `by_state` is returned unfiltered — " +
|
|
117
|
+
"report it, because a long list of predictions is not a body of evidence.\n" +
|
|
118
|
+
"2. For the closest match, call get_prediction with its id. Report the registered wording, the " +
|
|
119
|
+
"registered falsifier, and the status verbatim.\n" +
|
|
120
|
+
"3. ⭐ The result's `stating_paper` carries the envelope of the PAPER that registered the prediction, " +
|
|
121
|
+
"not of the register. That is deliberate: the register says to verify the stating paper against its " +
|
|
122
|
+
"stored proof rather than to trust the register. Follow that — verify the stating paper.\n" +
|
|
123
|
+
"4. Say plainly whether the prediction has been run. An unrun prediction is a disclosure, not a result, " +
|
|
124
|
+
"and must not be reported as evidence either way.\n\n" +
|
|
125
|
+
"If no registered prediction bears on the claim, say so rather than constructing one."
|
|
126
|
+
)
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const e = new Error(`no prompt "${name}". Call prompts/list to see what is available.`);
|
|
132
|
+
e.code = -32602;
|
|
133
|
+
throw e;
|
|
134
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// MCP resources over the corpus — shared by both surfaces.
|
|
2
|
+
//
|
|
3
|
+
// ⭐⭐ THE ONE DECISION THAT SHAPES THIS FILE: A RESOURCE CARRIES ITS PROVENANCE
|
|
4
|
+
// IN THE TEXT, NOT BESIDE IT. A tool response wraps a document in an envelope and
|
|
5
|
+
// a caller reads the envelope. A resource is different in kind — clients hand its
|
|
6
|
+
// contents straight to a model as context, and a `mimeType` field does not travel
|
|
7
|
+
// with a quotation. Serving bare text here would hand out corpus material stripped
|
|
8
|
+
// of the one property this server exists to provide.
|
|
9
|
+
//
|
|
10
|
+
// ⚠️ This is not a new judgement; it is the letters' rule applied a second time.
|
|
11
|
+
// The letters mark voice INLINE — "[VERBATIM — Thon Ly]" / "[SCAFFOLD …]" — rather
|
|
12
|
+
// than in a metadata field, because *with a field an agent must LOOK to know; with
|
|
13
|
+
// a marker it must STRIP not to*. The same asymmetry decides this: a header the
|
|
14
|
+
// model must delete is safe, a field it must consult is not.
|
|
15
|
+
//
|
|
16
|
+
// ⚠️⚠️ AND THE HEADER MUST SAY WHAT THE HASH DOES NOT COVER. `provenance.sha256`
|
|
17
|
+
// is computed over the whole source file, front matter included; `text` is the
|
|
18
|
+
// body. Prepending a header makes the served bytes hash to nothing at all — so
|
|
19
|
+
// the header states plainly that it is not part of the hashed artifact and points
|
|
20
|
+
// at the file that is. A "verifiable" resource that quietly cannot be verified
|
|
21
|
+
// would be worse than one that never claimed it.
|
|
22
|
+
|
|
23
|
+
const CORPUS_SCHEME = "corpus://";
|
|
24
|
+
const PAGE = 50;
|
|
25
|
+
|
|
26
|
+
export const RESOURCE_TEMPLATES = [
|
|
27
|
+
{
|
|
28
|
+
uriTemplate: "corpus://{slug}",
|
|
29
|
+
name: "corpus-document",
|
|
30
|
+
title: "Corpus document by slug",
|
|
31
|
+
description:
|
|
32
|
+
"Any document in the corpus, addressed by its slug — e.g. corpus://co-presence-gated-redemption. " +
|
|
33
|
+
"Returns the canonical text prefixed with a provenance header carrying its licence, sha256, DOI and " +
|
|
34
|
+
"a runnable verification command. Slugs come from resources/list or the list_documents tool.",
|
|
35
|
+
mimeType: "text/markdown"
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const mime = (d) => (d.metadata_convention === "html" ? "text/plain" : "text/markdown");
|
|
40
|
+
|
|
41
|
+
// The catalogue entry. Deliberately terse: a client renders this in a picker, and
|
|
42
|
+
// the licence is the one thing a person choosing a document needs to see.
|
|
43
|
+
export const resourceEntry = (d) => ({
|
|
44
|
+
uri: CORPUS_SCHEME + d.slug,
|
|
45
|
+
name: d.slug,
|
|
46
|
+
title: d.title,
|
|
47
|
+
description:
|
|
48
|
+
`${d.genre} · ${d.licence.id}` +
|
|
49
|
+
(d.date ? ` · ${d.date}` : "") +
|
|
50
|
+
(d.provenance.doi ? ` · doi:${d.provenance.doi}` : "") +
|
|
51
|
+
(d.subtitle ? ` — ${d.subtitle}` : ""),
|
|
52
|
+
mimeType: mime(d),
|
|
53
|
+
// ⚠️ `lastModified` carries the document's own date and NOTHING MORE PRECISE.
|
|
54
|
+
// ISO 8601 permits a date alone, and inventing "T00:00:00Z" would assert a
|
|
55
|
+
// time this corpus does not record — a small lie in a provenance server.
|
|
56
|
+
annotations: {
|
|
57
|
+
audience: ["user", "assistant"],
|
|
58
|
+
// Anchored papers rank above unanchored scaffold: a consumer choosing
|
|
59
|
+
// among 140 documents should meet the ones with proofs first.
|
|
60
|
+
priority: d.provenance.doi ? 0.9 : d.provenance.opentimestamps ? 0.6 : 0.4,
|
|
61
|
+
...(d.date ? { lastModified: d.date } : {})
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const listResources = (documents, cursor) => {
|
|
66
|
+
// Cursor-based paging, because a corpus grows and a client should not have to
|
|
67
|
+
// take 140 entries to find one. The cursor is an offset encoded as a string —
|
|
68
|
+
// opaque to the client, which is all the protocol asks of it.
|
|
69
|
+
// ⚠️ btoa/atob, NOT Buffer. The worker runs without nodejs_compat, so Buffer is
|
|
70
|
+
// undefined there and only there — a break that passes every local test and
|
|
71
|
+
// fails once, in production, on the surface nobody runs by hand.
|
|
72
|
+
let start = 0;
|
|
73
|
+
if (cursor) {
|
|
74
|
+
try {
|
|
75
|
+
start = Number(atob(String(cursor)));
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Error(`invalid cursor: ${cursor}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!Number.isInteger(start) || start < 0 || start > documents.length) {
|
|
81
|
+
throw new Error(`invalid cursor: ${cursor}`);
|
|
82
|
+
}
|
|
83
|
+
const page = documents.slice(start, start + PAGE);
|
|
84
|
+
const next = start + PAGE < documents.length ? btoa(String(start + PAGE)) : undefined;
|
|
85
|
+
return { resources: page.map(resourceEntry), ...(next ? { nextCursor: next } : {}) };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ⚠️ Attribution is stated as an instruction, not a licence id. CC-BY on seven of
|
|
89
|
+
// these documents means a real obligation, and "CC-BY-4.0" alone leaves an agent
|
|
90
|
+
// to know what that entails.
|
|
91
|
+
const licenceLine = (d) =>
|
|
92
|
+
d.licence.attribution_required
|
|
93
|
+
? `${d.licence.id} — ATTRIBUTION REQUIRED. Attribute to: ${d.authors ?? "⚠️ UNKNOWN — the index carries no author for this document; do not quote it until that is fixed"}. ${d.licence.url}`
|
|
94
|
+
: `${d.licence.id} — no attribution required, though citation is welcome. ${d.licence.url}`;
|
|
95
|
+
|
|
96
|
+
export const provenanceHeader = (d) => {
|
|
97
|
+
const p = d.provenance;
|
|
98
|
+
const L = [];
|
|
99
|
+
L.push("[PROVENANCE — corpus.333.eco. This header is NOT part of the document; the document begins below.]");
|
|
100
|
+
L.push(`title: ${d.title}`);
|
|
101
|
+
L.push(`slug: ${d.slug} (${d.genre}${d.date ? `, ${d.date}` : ""})`);
|
|
102
|
+
L.push(`licence: ${licenceLine(d)}`);
|
|
103
|
+
if (p.source_url) L.push(`source: ${p.source_url}`);
|
|
104
|
+
if (p.canonical_url) L.push(`canonical: ${p.canonical_url}`);
|
|
105
|
+
L.push(`sha256: ${p.sha256}`);
|
|
106
|
+
// The single most misreadable field, so it gets a full sentence.
|
|
107
|
+
L.push(" ⚠️ covers the COMPLETE SOURCE FILE at `source`, metadata block included —");
|
|
108
|
+
L.push(" NOT the text below, and NOT this header. Hashing what you were served");
|
|
109
|
+
L.push(" will not reproduce it. Fetch `source` to check.");
|
|
110
|
+
if (p.doi) L.push(`doi: https://doi.org/${p.doi} (this exact version)`);
|
|
111
|
+
if (p.concept_doi) L.push(`concept: https://doi.org/${p.concept_doi} (follows the document across versions)`);
|
|
112
|
+
if (p.opentimestamps) L.push(`proof: ots verify ${d.path}.ots — in the source repository, anchored in Bitcoin`);
|
|
113
|
+
if (p.source_url) L.push(`verify: curl -sL ${p.source_url} | shasum -a 256 # compare with sha256 above`);
|
|
114
|
+
|
|
115
|
+
if (p.deposited_matches_current === false) {
|
|
116
|
+
L.push("status: ⚠️ REVISED SINCE ITS DEPOSIT. The DOI above resolves to the deposited");
|
|
117
|
+
L.push(" version; the text below is newer. They differ legitimately.");
|
|
118
|
+
}
|
|
119
|
+
if (d.status === "living" && p.concept_doi) {
|
|
120
|
+
L.push("living: This document is revised on purpose. CITE the concept DOI, which always");
|
|
121
|
+
L.push(" resolves to the newest version; VERIFY against the version DOI and");
|
|
122
|
+
L.push(" sha256 above, which pin these exact bytes.");
|
|
123
|
+
}
|
|
124
|
+
// ⚠️ The letters' inline voice markers are meaningless without the sentence
|
|
125
|
+
// that explains them, and that sentence lives in `editorial` — a field a
|
|
126
|
+
// resource read would otherwise drop, reintroducing the exact bug the tool
|
|
127
|
+
// responses were once shipped with.
|
|
128
|
+
if (d.editorial?.annotation) {
|
|
129
|
+
L.push(`editorial: ${d.editorial.annotation.replace(/\s+/g, " ")}`);
|
|
130
|
+
}
|
|
131
|
+
L.push("[END PROVENANCE — everything below this line is the document, verbatim.]");
|
|
132
|
+
return L.join("\n");
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const readResource = (uri, bySlug) => {
|
|
136
|
+
const raw = String(uri ?? "");
|
|
137
|
+
if (!raw.startsWith(CORPUS_SCHEME)) {
|
|
138
|
+
throw new Error(`unsupported resource uri "${raw}". Corpus documents are addressed as corpus://<slug>.`);
|
|
139
|
+
}
|
|
140
|
+
const slug = raw.slice(CORPUS_SCHEME.length);
|
|
141
|
+
const d = bySlug.get(slug);
|
|
142
|
+
if (!d) throw new Error(`no document with slug "${slug}". Call resources/list to see what is available.`);
|
|
143
|
+
return {
|
|
144
|
+
contents: [
|
|
145
|
+
{
|
|
146
|
+
uri: raw,
|
|
147
|
+
mimeType: mime(d),
|
|
148
|
+
// Header, blank line, then the document exactly as the tools return it.
|
|
149
|
+
text: provenanceHeader(d) + "\n\n" + d.text
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/* ------------------------------------------------------------- completions ---
|
|
156
|
+
⭐ A URI TEMPLATE WITHOUT COMPLETION IS A TEMPLATE YOU MUST ALREADY KNOW THE
|
|
157
|
+
ANSWER TO USE. `corpus://{slug}` is only usable by someone who already has the
|
|
158
|
+
slug; this is what turns it into something a person can discover by typing.
|
|
159
|
+
|
|
160
|
+
⚠️ Prefix matches rank above substring matches, because a slug is a name and a
|
|
161
|
+
person typing one is almost always typing its beginning. Within each group the
|
|
162
|
+
order is the corpus's own (alphabetical by slug) — stable, so the same keystroke
|
|
163
|
+
never reorders the list under the user's cursor. */
|
|
164
|
+
|
|
165
|
+
const COMPLETION_MAX = 100; // the spec's ceiling
|
|
166
|
+
|
|
167
|
+
const slugCompletion = (value, documents) => {
|
|
168
|
+
const q = String(value ?? "").toLowerCase();
|
|
169
|
+
const slugs = documents.map((d) => d.slug);
|
|
170
|
+
const starts = slugs.filter((s) => s.startsWith(q));
|
|
171
|
+
const contains = q ? slugs.filter((s) => !s.startsWith(q) && s.includes(q)) : [];
|
|
172
|
+
const all = [...starts, ...contains];
|
|
173
|
+
return {
|
|
174
|
+
completion: {
|
|
175
|
+
values: all.slice(0, COMPLETION_MAX),
|
|
176
|
+
total: all.length,
|
|
177
|
+
hasMore: all.length > COMPLETION_MAX
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const completeArgument = (ref, argument, documents) => {
|
|
183
|
+
// ⭐ Prompts and completions compose: `verify_a_quote` takes a slug, and the
|
|
184
|
+
// same slug list that completes corpus://{slug} completes it here. A prompt
|
|
185
|
+
// argument nobody can autocomplete is a prompt you must already know how to
|
|
186
|
+
// fill in — the same defect the resource template had before completions.
|
|
187
|
+
if (ref?.type === "ref/prompt") {
|
|
188
|
+
if (ref.name === "verify_a_quote" && argument?.name === "slug") {
|
|
189
|
+
return slugCompletion(argument?.value, documents);
|
|
190
|
+
}
|
|
191
|
+
const e = new Error(
|
|
192
|
+
`no completions for prompt "${ref?.name}" argument "${argument?.name}". ` +
|
|
193
|
+
"The completable prompt argument is verify_a_quote(slug)."
|
|
194
|
+
);
|
|
195
|
+
e.code = -32602;
|
|
196
|
+
throw e;
|
|
197
|
+
}
|
|
198
|
+
if (ref?.type !== "ref/resource") {
|
|
199
|
+
const e = new Error(`unsupported completion reference type "${ref?.type}"`);
|
|
200
|
+
e.code = -32602;
|
|
201
|
+
throw e;
|
|
202
|
+
}
|
|
203
|
+
if (ref.uri !== "corpus://{slug}") {
|
|
204
|
+
const e = new Error(`no completions for "${ref.uri}". The completable template is corpus://{slug}.`);
|
|
205
|
+
e.code = -32602;
|
|
206
|
+
throw e;
|
|
207
|
+
}
|
|
208
|
+
if (argument?.name !== "slug") {
|
|
209
|
+
const e = new Error(`corpus://{slug} has one argument, "slug"; got "${argument?.name}"`);
|
|
210
|
+
e.code = -32602;
|
|
211
|
+
throw e;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return slugCompletion(argument?.value, documents);
|
|
215
|
+
};
|