@333eco/corpus 1.2.5 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -0
- package/dist/corpus.json +1420 -127
- package/package.json +3 -2
- package/src/base-tools.mjs +72 -0
- package/src/program-tools.mjs +181 -0
- package/src/prompts.mjs +134 -0
- package/src/report.mjs +104 -0
- package/src/resources.mjs +215 -0
- package/src/results.mjs +40 -0
- package/src/search.mjs +176 -0
- package/src/server.mjs +143 -65
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@333eco/corpus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "An MCP server for an open-licensed corpus, served with verifiable provenance — 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",
|
|
@@ -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
|
+
};
|
package/src/report.mjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// `--report-gap` and `--report-bug` — the only place this package makes an
|
|
2
|
+
// outbound request, and it is reached only by typing one of the flags.
|
|
3
|
+
//
|
|
4
|
+
// ⭐⭐ WHY THIS IS A COMMAND AND NOT TELEMETRY, because the distinction is the
|
|
5
|
+
// whole design. The useful signal from a corpus server is *what someone looked
|
|
6
|
+
// for and did not find*. The remote endpoint at corpus.333.eco collects that
|
|
7
|
+
// from its own callers as a property of being the server they called. This
|
|
8
|
+
// package runs on YOUR machine, so the same collection there would be an
|
|
9
|
+
// outbound report about your private reading — and the guard against that is
|
|
10
|
+
// not a consent flag or an opt-out. It is that THE SERVER PATH CANNOT REACH
|
|
11
|
+
// THIS FILE: `server.mjs` loads it with a dynamic import inside the argv branch,
|
|
12
|
+
// so during normal operation the module is never even read off disk.
|
|
13
|
+
//
|
|
14
|
+
// ⚠️ THE CHECKABLE CLAIM CHANGED SHAPE WHEN THIS FILE WAS ADDED, AND THAT IS
|
|
15
|
+
// WORTH STATING PLAINLY. Before it, "this package makes no network call" was
|
|
16
|
+
// verifiable by `grep -r fetch src/` returning nothing at all — the strongest
|
|
17
|
+
// kind of evidence, because it needs no reasoning. Now the honest claim is
|
|
18
|
+
// narrower: there is exactly ONE fetch in the package, it lives in this file,
|
|
19
|
+
// and this file is imported from exactly one place — a branch that requires an
|
|
20
|
+
// explicit flag. Still inspectable in under a minute, but it is a chain of two
|
|
21
|
+
// facts rather than one absence. ⛔ If a second import of this module ever
|
|
22
|
+
// appears, that chain is broken and the claim must be rewritten rather than
|
|
23
|
+
// repeated.
|
|
24
|
+
//
|
|
25
|
+
// ⛔ IT REPORTS TO THE WORKER, NOT TO THE NOTIFICATION BEACON. Sending to
|
|
26
|
+
// thonly.org/api/track would mean shipping every user of this package a working
|
|
27
|
+
// recipe for writing into the founder's admin notification channel, behind an
|
|
28
|
+
// Origin header a CLI can trivially assert. The worker's /gap endpoint writes to
|
|
29
|
+
// Analytics Engine and pushes nothing at anyone.
|
|
30
|
+
|
|
31
|
+
const ENDPOINT = "https://corpus.333.eco/report";
|
|
32
|
+
|
|
33
|
+
// ⚠️ A BUG REPORT AND A GAP REPORT CARRY THE SAME PAYLOAD, DELIBERATELY. It is
|
|
34
|
+
// tempting to attach a node version and a platform to a bug — genuinely useful
|
|
35
|
+
// to whoever fixes it — but that would give this command two different promises
|
|
36
|
+
// about what it sends, and the promise is the valuable part. Anything about your
|
|
37
|
+
// environment that matters, put in the text; then you have said it on purpose.
|
|
38
|
+
const KINDS = {
|
|
39
|
+
gap: {
|
|
40
|
+
prompt: "what you looked for and did not find",
|
|
41
|
+
thanks: "Thank you — recorded. It joins the gaps the hosted endpoint already sees."
|
|
42
|
+
},
|
|
43
|
+
bug: {
|
|
44
|
+
prompt: "what went wrong, and what you expected instead",
|
|
45
|
+
thanks: "Thank you — recorded. Issues are also welcome at github.com/333eco/corpus.333.eco/issues."
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const MAX = 200;
|
|
49
|
+
|
|
50
|
+
// ⭐ WHAT IS SENT IS THE WHOLE OF WHAT IS SENT. The text you typed, and the
|
|
51
|
+
// version of the corpus you have. No machine id, no username, no hostname, no
|
|
52
|
+
// path, no timestamp of your own — and the receiving end deliberately does not
|
|
53
|
+
// record the country it could resolve, because a voluntary note about a missing
|
|
54
|
+
// document has no use for where the sender was standing.
|
|
55
|
+
export const report = async (kind, text, version) => {
|
|
56
|
+
const spec = KINDS[kind];
|
|
57
|
+
if (!spec) throw new Error(`unknown report kind: ${kind}`);
|
|
58
|
+
const body = String(text ?? "").trim().slice(0, MAX);
|
|
59
|
+
if (!body) {
|
|
60
|
+
console.error(`usage: corpus-mcp --report-${kind} "${spec.prompt}"`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const payload = { kind, text: body, version: version ?? null };
|
|
65
|
+
|
|
66
|
+
// Printed BEFORE the request, not after, so the disclosure is not
|
|
67
|
+
// contingent on the request succeeding.
|
|
68
|
+
console.log("Sending this, and nothing else:\n");
|
|
69
|
+
console.log(" " + JSON.stringify(payload));
|
|
70
|
+
console.log("\n to " + ENDPOINT + "\n");
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(ENDPOINT, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "content-type": "application/json" },
|
|
76
|
+
body: JSON.stringify(payload)
|
|
77
|
+
});
|
|
78
|
+
// ⚠️⚠️ THE STATUS CODE IS NOT THE CONTRACT, AND TRUSTING IT REPORTED A
|
|
79
|
+
// FALSE SUCCESS ON THE FIRST RUN. A server predating /gap treats any
|
|
80
|
+
// POST without a JSON-RPC `id` as a NOTIFICATION and answers 202 with an
|
|
81
|
+
// empty body — so `res.ok` was true, and this printed "recorded" while
|
|
82
|
+
// nothing had been. The success signal must therefore be something only
|
|
83
|
+
// the real handler can produce: an explicit `ok` in the body. That also
|
|
84
|
+
// makes version skew safe in both directions, since an old server can
|
|
85
|
+
// never accidentally satisfy it.
|
|
86
|
+
const ack = await res.json().catch(() => null);
|
|
87
|
+
if (res.ok && ack?.ok === true) {
|
|
88
|
+
console.log(spec.thanks);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
console.error(
|
|
92
|
+
res.ok
|
|
93
|
+
? "The endpoint accepted the request but did not confirm it recorded anything —\n" +
|
|
94
|
+
"it is probably running a version without /report. Nothing was recorded."
|
|
95
|
+
: `The endpoint answered ${res.status}. Nothing was recorded.`
|
|
96
|
+
);
|
|
97
|
+
return 1;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// A failure here is worth nobody's day. Say so and exit cleanly.
|
|
100
|
+
console.error(`Could not reach ${ENDPOINT}: ${e.message}`);
|
|
101
|
+
console.error("Nothing was sent. This is entirely optional — carry on.");
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
};
|