wyrd-scribe 0.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.
@@ -0,0 +1,195 @@
1
+ /**
2
+ * THE LINEAGE LINE — `wyrd.lineage/v1`. One JSON object per line, LF-terminated, append-only (D3).
3
+ *
4
+ * ⚠⚠ EVERY IDENTITY CARRIES AN EXPLICIT DISCRIMINANT AND ITS VAULT, and both halves are rulings
5
+ * rather than taste (2026-08-31 identity block).
6
+ *
7
+ * · `kind` exists so no consumer ever type-sniffs a string. J35 shipped a POLYMORPHIC SCALAR —
8
+ * one field holding a capture id normally and a path for rows that can never have one — and
9
+ * the measured cost was that every consumer needed a sniff branch, with ~84 edges hanging on
10
+ * one of them being written correctly. Our format is JSONL. A structured format has no reason
11
+ * to make a reader guess. A page with no stable id is PERMANENTLY `kind: 'path'`; it is never
12
+ * `id: null`, and never a value with an explanatory suffix.
13
+ * · `vault_id` exists because `.wyrd/lineage.jsonl` scopes identity IMPLICITLY, and implicit
14
+ * scoping survives only while the record stays home. Magi is mage-to-mage, so a record WILL
15
+ * travel, and the moment it does a bare path is ambiguous across two granted vaults —
16
+ * the same defect the ruling exists to prevent, one layer up. The identity block gave two
17
+ * acceptable answers and named picking neither as the failure. This is the first: the identity
18
+ * carries its vault explicitly.
19
+ *
20
+ * ⚠⚠ THE LIMITS BELOW ARE POLICY BOUNDS, NOT PROVEN ATOMICITY GUARANTEES. B4's argument is that a
21
+ * single `write` of a complete line under `O_APPEND` does not interleave for writes below a
22
+ * pipe/PIPE_BUF-class threshold. That threshold is a property of the platform and the filesystem,
23
+ * it is not 65,536 anywhere in particular, and nothing in this repo has measured it. What these
24
+ * numbers actually buy is that a line CANNOT grow without bound through a large quote or a long
25
+ * `derived_from` list — so the atomicity argument is being made about a bounded object rather than
26
+ * an unbounded one. Calling that a guarantee would be the "confident number with no regime" shape
27
+ * this lane keeps catching in its own work.
28
+ */
29
+ import type { ResolvedSpan } from './span.js';
30
+ import type { ScribeRefusal } from './refusal.js';
31
+ export declare const LINEAGE_SCHEMA = "wyrd.lineage/v1";
32
+ /** ≤ 1,024 UTF-8 bytes of quote text retained; the rest is dropped and `truncated` says so. */
33
+ export declare const QUOTE_BYTE_BUDGET = 1024;
34
+ /** At most this many `derived_from` entries per line. */
35
+ export declare const MAX_SOURCES = 64;
36
+ /** At most this many spans per line, summed across all sources. */
37
+ export declare const MAX_SPANS = 256;
38
+ /** The serialised line INCLUDING its terminating LF. */
39
+ export declare const MAX_LINE_BYTES = 65536;
40
+ /**
41
+ * At most this many UTF-8 bytes in the page path, as the ledger spells it.
42
+ *
43
+ * ⚠⚠ IT EXISTS BECAUSE THE PAGE PATH WAS THE ONLY VARIABLE-WIDTH FIELD IN A BOUNDED LINE WITH NO
44
+ * BOUND OF ITS OWN. Quotes have `QUOTE_BYTE_BUDGET`, sources have `MAX_SOURCES`, spans have
45
+ * `MAX_SPANS`, the digest is fixed at 64 hex characters and the byte count is bounded by
46
+ * `Number.MAX_SAFE_INTEGER` — so every other contributor to `MAX_LINE_BYTES` could be reasoned
47
+ * about in advance, and one could not. A single 60,000-byte path was enough to consume the whole
48
+ * budget and starve fields the caller had every right to expect would fit.
49
+ *
50
+ * ⚠ 4,096 IS THE FENCE'S `MAX_GRANT_LENGTH`, DELIBERATELY. That constant bounds the grant ROOT and
51
+ * says nothing about the grant-RELATIVE path — measured 2026-09-03, an in-grant path was created
52
+ * at 16,407 characters with no refusal — so this is not a restatement of an existing guarantee.
53
+ * Reusing the number keeps one order of magnitude in the reader's head rather than two, and 4,096
54
+ * bytes of path is far past any real vault while leaving 61,440 bytes for everything else.
55
+ *
56
+ * ⚠ WHAT THIS DOES NOT CLOSE. The bound is checked against the path the CALLER asked for, and the
57
+ * ledger records the path the filesystem RESOLVED to; an in-grant junction can lengthen it after
58
+ * the page is created. `stamp.ts` step 9 remains the last line of defence for that case and now
59
+ * reports it honestly. Closing it properly needs a pre-write canonical path from the fence, which
60
+ * is a change to the Fence's public API and remains outside this module's contract.
61
+ */
62
+ export declare const MAX_PAGE_PATH_BYTES = 4096;
63
+ export interface PageIdentity {
64
+ readonly kind: 'path';
65
+ readonly vault_id: string;
66
+ readonly path: string;
67
+ }
68
+ export interface ContentHash {
69
+ readonly algorithm: 'sha256';
70
+ readonly digest: string;
71
+ readonly bytes: number;
72
+ }
73
+ export interface StoredQuote {
74
+ readonly text: string;
75
+ readonly original_utf8_bytes: number;
76
+ readonly stored_utf8_bytes: number;
77
+ readonly sha256: string;
78
+ readonly truncated: boolean;
79
+ }
80
+ export interface LineageSpan {
81
+ readonly offset: number;
82
+ readonly length: number;
83
+ readonly quote: StoredQuote;
84
+ }
85
+ export interface LineageSource {
86
+ readonly identity: PageIdentity;
87
+ readonly content: ContentHash;
88
+ readonly spans: readonly LineageSpan[];
89
+ }
90
+ export interface LineagePage {
91
+ readonly identity: PageIdentity;
92
+ readonly content: ContentHash;
93
+ }
94
+ export interface LineageWriter {
95
+ readonly server: 'wyrd-scribe';
96
+ readonly version: string;
97
+ readonly tool: 'write_page';
98
+ }
99
+ export interface LineageRecord {
100
+ readonly schema: typeof LINEAGE_SCHEMA;
101
+ readonly event: 'page_written';
102
+ /**
103
+ * ⚠⚠ THE PER-WRITE IDENTIFIER, AND IT EXISTS BECAUSE `recorded_at` IS NOT ONE. Two distinct
104
+ * writes can produce BYTE-IDENTICAL lines: write a page, delete it outside this server, write
105
+ * the identical content again within the same millisecond, and every field of the record —
106
+ * timestamp, page path, digest, sources — is the same. A ledger holding two identical lines
107
+ * cannot say whether it recorded two events or duplicated one, and that is a question about the
108
+ * permanent record that nothing else in the line can answer.
109
+ *
110
+ * ⚠ SERVER-MINTED, LIKE `recorded_at` AND FOR THE SAME REASON. A caller-supplied id would be an
111
+ * unauthenticated assertion recorded as fact — the objection that keeps an `actor` field out of
112
+ * this record entirely.
113
+ *
114
+ * ⚠ IT IS AN EVENT ID, NOT A CONTENT HASH. Identical content written twice is TWO events and
115
+ * gets two ids; that is the distinction the field exists to draw, so deriving it from the
116
+ * record's own bytes would defeat it exactly where it is needed.
117
+ */
118
+ readonly event_id: string;
119
+ readonly recorded_at: string;
120
+ readonly writer: LineageWriter;
121
+ readonly vault: {
122
+ readonly kind: 'uuid';
123
+ readonly id: string;
124
+ };
125
+ readonly page: LineagePage;
126
+ readonly sources: readonly LineageSource[];
127
+ }
128
+ export declare function identity(vaultId: string, path: string): PageIdentity;
129
+ export declare function contentHash(digest: string, bytes: number): ContentHash;
130
+ /**
131
+ * Cut a quote to the byte budget ON A UTF-8 BOUNDARY, and hash the FULL text either way.
132
+ *
133
+ * ⚠⚠ THE HASH IS OVER THE WHOLE QUOTE EVEN WHEN THE TEXT IS CUT, and that is the entire value of
134
+ * the truncation design. D4's repairability claim is that a stale stamp can be REPAIRED rather than
135
+ * merely flagged — which needs an identity for the text that was actually cited. Hashing the stored
136
+ * prefix instead would make a truncated span unverifiable against the source it came from: the
137
+ * recorded hash would match nothing, and the record would look healthy while proving nothing.
138
+ *
139
+ * ⚠ THE CUT IS ON A CODEPOINT BOUNDARY, NOT A `slice(0, 1024)`. A byte cut through a multibyte
140
+ * sequence yields text that does not re-encode to the bytes it came from, which is precisely the
141
+ * identity-destroying move `span.ts` refuses on every other path. `Buffer.write` into a fixed
142
+ * buffer does this natively: it never writes a partial character.
143
+ */
144
+ export declare function storeQuote(text: string): StoredQuote;
145
+ /**
146
+ * ⚠ THE SINGLE HOME OF THE SCRIBE'S CONTENT DIGEST, AND IT IS EXPORTED FOR THAT REASON RATHER THAN
147
+ * BECAUSE THREE CALLERS WANTED A SHORTER LINE. `source.ts` hashes what it re-read to prove the
148
+ * source did not move under it, `stamp.ts` hashes the page it is about to create, and this module
149
+ * hashes the recorded text — and those three digests are COMPARED WITH EACH OTHER downstream. Three
150
+ * inlined spellings of one algorithm can disagree the moment any one of them is edited, and the
151
+ * failure would read as a source that changed rather than as a hash that drifted.
152
+ *
153
+ * ⚠ The fence's `hashInGrant` is NOT this and must not be folded in: it streams a file in windows
154
+ * because a source is routinely a whole transcript, while this takes bytes already in hand.
155
+ */
156
+ export declare function hashText(bytes: Buffer): string;
157
+ export declare function lineageSpan(span: ResolvedSpan): LineageSpan;
158
+ /**
159
+ * ⚠ THE KEY ORDER HERE IS THE WIRE ORDER, because `JSON.stringify` emits own keys in insertion
160
+ * order and a ledger line is a text artifact people will diff. Nothing depends on it semantically —
161
+ * a consumer parses JSON — but a format whose byte layout drifts between versions makes every diff
162
+ * of the ledger unreadable for no gain.
163
+ */
164
+ export declare function serialiseRecord(record: LineageRecord): Buffer;
165
+ /**
166
+ * The count bounds, checked BEFORE anything is serialised or written.
167
+ *
168
+ * ⚠ SPANS ARE COUNTED ACROSS ALL SOURCES, not per source. A per-source cap of 256 with 64 sources
169
+ * admits 16,384 spans, which is the bound this is supposed to be — the whole reason the cap exists
170
+ * is the serialised size of one line, and size is a property of the line rather than of a source.
171
+ */
172
+ export declare function checkCounts(sources: readonly LineageSource[]): ScribeRefusal | null;
173
+ /**
174
+ * The same two bounds, over counts taken from the REQUEST rather than from resolved records.
175
+ *
176
+ * ⚠⚠ IT EXISTS SO A GUARANTEED REFUSAL CAN BE DECIDED BEFORE ANY SOURCE IS READ. Both caps are
177
+ * knowable from the request's own shape — 65 `derived_from` entries is over the cap whether or not
178
+ * a single one of those files exists — so checking them only after the source loop meant reading
179
+ * every source to reach a conclusion that never depended on them, and leaking each one's existence
180
+ * and timing on the way. `stamp.ts`'s step 3 carries the full argument.
181
+ *
182
+ * ⚠ ONE IMPLEMENTATION, TWO ENTRY POINTS. `checkCounts` now delegates here rather than repeating
183
+ * the comparisons, so the pre-check and the post-resolution check can never disagree about a bound
184
+ * or about the words a refusal uses.
185
+ */
186
+ export declare function checkPagePath(pagePath: string): ScribeRefusal | null;
187
+ export declare function checkRequestCounts(sources: number, spans: number): ScribeRefusal | null;
188
+ /**
189
+ * The size bound, checked on the SERIALISED bytes.
190
+ *
191
+ * ⚠ NEVER SPLIT A LINE. B4's atomicity argument is about one `write` of one complete line; a line
192
+ * split across two writes is exactly the interleaving the argument rules out, arriving through the
193
+ * repair for the case it could not handle. So an over-size line REFUSES and nothing is appended.
194
+ */
195
+ export declare function checkLineSize(line: Buffer): ScribeRefusal | null;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * THE LINEAGE LINE — `wyrd.lineage/v1`. One JSON object per line, LF-terminated, append-only (D3).
3
+ *
4
+ * ⚠⚠ EVERY IDENTITY CARRIES AN EXPLICIT DISCRIMINANT AND ITS VAULT, and both halves are rulings
5
+ * rather than taste (2026-08-31 identity block).
6
+ *
7
+ * · `kind` exists so no consumer ever type-sniffs a string. J35 shipped a POLYMORPHIC SCALAR —
8
+ * one field holding a capture id normally and a path for rows that can never have one — and
9
+ * the measured cost was that every consumer needed a sniff branch, with ~84 edges hanging on
10
+ * one of them being written correctly. Our format is JSONL. A structured format has no reason
11
+ * to make a reader guess. A page with no stable id is PERMANENTLY `kind: 'path'`; it is never
12
+ * `id: null`, and never a value with an explanatory suffix.
13
+ * · `vault_id` exists because `.wyrd/lineage.jsonl` scopes identity IMPLICITLY, and implicit
14
+ * scoping survives only while the record stays home. Magi is mage-to-mage, so a record WILL
15
+ * travel, and the moment it does a bare path is ambiguous across two granted vaults —
16
+ * the same defect the ruling exists to prevent, one layer up. The identity block gave two
17
+ * acceptable answers and named picking neither as the failure. This is the first: the identity
18
+ * carries its vault explicitly.
19
+ *
20
+ * ⚠⚠ THE LIMITS BELOW ARE POLICY BOUNDS, NOT PROVEN ATOMICITY GUARANTEES. B4's argument is that a
21
+ * single `write` of a complete line under `O_APPEND` does not interleave for writes below a
22
+ * pipe/PIPE_BUF-class threshold. That threshold is a property of the platform and the filesystem,
23
+ * it is not 65,536 anywhere in particular, and nothing in this repo has measured it. What these
24
+ * numbers actually buy is that a line CANNOT grow without bound through a large quote or a long
25
+ * `derived_from` list — so the atomicity argument is being made about a bounded object rather than
26
+ * an unbounded one. Calling that a guarantee would be the "confident number with no regime" shape
27
+ * this lane keeps catching in its own work.
28
+ */
29
+ import { createHash } from 'node:crypto';
30
+ import { scribeRefuse } from './refusal.js';
31
+ export const LINEAGE_SCHEMA = 'wyrd.lineage/v1';
32
+ /** ≤ 1,024 UTF-8 bytes of quote text retained; the rest is dropped and `truncated` says so. */
33
+ export const QUOTE_BYTE_BUDGET = 1_024;
34
+ /** At most this many `derived_from` entries per line. */
35
+ export const MAX_SOURCES = 64;
36
+ /** At most this many spans per line, summed across all sources. */
37
+ export const MAX_SPANS = 256;
38
+ /** The serialised line INCLUDING its terminating LF. */
39
+ export const MAX_LINE_BYTES = 65_536;
40
+ /**
41
+ * At most this many UTF-8 bytes in the page path, as the ledger spells it.
42
+ *
43
+ * ⚠⚠ IT EXISTS BECAUSE THE PAGE PATH WAS THE ONLY VARIABLE-WIDTH FIELD IN A BOUNDED LINE WITH NO
44
+ * BOUND OF ITS OWN. Quotes have `QUOTE_BYTE_BUDGET`, sources have `MAX_SOURCES`, spans have
45
+ * `MAX_SPANS`, the digest is fixed at 64 hex characters and the byte count is bounded by
46
+ * `Number.MAX_SAFE_INTEGER` — so every other contributor to `MAX_LINE_BYTES` could be reasoned
47
+ * about in advance, and one could not. A single 60,000-byte path was enough to consume the whole
48
+ * budget and starve fields the caller had every right to expect would fit.
49
+ *
50
+ * ⚠ 4,096 IS THE FENCE'S `MAX_GRANT_LENGTH`, DELIBERATELY. That constant bounds the grant ROOT and
51
+ * says nothing about the grant-RELATIVE path — measured 2026-09-03, an in-grant path was created
52
+ * at 16,407 characters with no refusal — so this is not a restatement of an existing guarantee.
53
+ * Reusing the number keeps one order of magnitude in the reader's head rather than two, and 4,096
54
+ * bytes of path is far past any real vault while leaving 61,440 bytes for everything else.
55
+ *
56
+ * ⚠ WHAT THIS DOES NOT CLOSE. The bound is checked against the path the CALLER asked for, and the
57
+ * ledger records the path the filesystem RESOLVED to; an in-grant junction can lengthen it after
58
+ * the page is created. `stamp.ts` step 9 remains the last line of defence for that case and now
59
+ * reports it honestly. Closing it properly needs a pre-write canonical path from the fence, which
60
+ * is a change to the Fence's public API and remains outside this module's contract.
61
+ */
62
+ export const MAX_PAGE_PATH_BYTES = 4_096;
63
+ export function identity(vaultId, path) {
64
+ return Object.freeze({ kind: 'path', vault_id: vaultId, path });
65
+ }
66
+ export function contentHash(digest, bytes) {
67
+ return Object.freeze({ algorithm: 'sha256', digest, bytes });
68
+ }
69
+ /**
70
+ * Cut a quote to the byte budget ON A UTF-8 BOUNDARY, and hash the FULL text either way.
71
+ *
72
+ * ⚠⚠ THE HASH IS OVER THE WHOLE QUOTE EVEN WHEN THE TEXT IS CUT, and that is the entire value of
73
+ * the truncation design. D4's repairability claim is that a stale stamp can be REPAIRED rather than
74
+ * merely flagged — which needs an identity for the text that was actually cited. Hashing the stored
75
+ * prefix instead would make a truncated span unverifiable against the source it came from: the
76
+ * recorded hash would match nothing, and the record would look healthy while proving nothing.
77
+ *
78
+ * ⚠ THE CUT IS ON A CODEPOINT BOUNDARY, NOT A `slice(0, 1024)`. A byte cut through a multibyte
79
+ * sequence yields text that does not re-encode to the bytes it came from, which is precisely the
80
+ * identity-destroying move `span.ts` refuses on every other path. `Buffer.write` into a fixed
81
+ * buffer does this natively: it never writes a partial character.
82
+ */
83
+ export function storeQuote(text) {
84
+ const full = Buffer.from(text, 'utf8');
85
+ const sha256 = hashText(full);
86
+ if (full.length <= QUOTE_BYTE_BUDGET) {
87
+ return Object.freeze({
88
+ text,
89
+ original_utf8_bytes: full.length,
90
+ stored_utf8_bytes: full.length,
91
+ sha256,
92
+ truncated: false
93
+ });
94
+ }
95
+ // ⚠ `Buffer.write` STOPS AT THE LAST WHOLE CHARACTER THAT FITS, so `written` is a
96
+ // codepoint-aligned length by construction. Doing the arithmetic by hand here would be a second
97
+ // implementation of the boundary rule the fence already has one of.
98
+ const held = Buffer.allocUnsafe(QUOTE_BYTE_BUDGET);
99
+ const written = held.write(text, 0, QUOTE_BYTE_BUDGET, 'utf8');
100
+ const stored = held.subarray(0, written);
101
+ return Object.freeze({
102
+ text: stored.toString('utf8'),
103
+ original_utf8_bytes: full.length,
104
+ stored_utf8_bytes: written,
105
+ sha256,
106
+ truncated: true
107
+ });
108
+ }
109
+ /**
110
+ * ⚠ THE SINGLE HOME OF THE SCRIBE'S CONTENT DIGEST, AND IT IS EXPORTED FOR THAT REASON RATHER THAN
111
+ * BECAUSE THREE CALLERS WANTED A SHORTER LINE. `source.ts` hashes what it re-read to prove the
112
+ * source did not move under it, `stamp.ts` hashes the page it is about to create, and this module
113
+ * hashes the recorded text — and those three digests are COMPARED WITH EACH OTHER downstream. Three
114
+ * inlined spellings of one algorithm can disagree the moment any one of them is edited, and the
115
+ * failure would read as a source that changed rather than as a hash that drifted.
116
+ *
117
+ * ⚠ The fence's `hashInGrant` is NOT this and must not be folded in: it streams a file in windows
118
+ * because a source is routinely a whole transcript, while this takes bytes already in hand.
119
+ */
120
+ export function hashText(bytes) {
121
+ return createHash('sha256').update(bytes).digest('hex');
122
+ }
123
+ export function lineageSpan(span) {
124
+ return Object.freeze({
125
+ offset: span.offset,
126
+ length: span.length,
127
+ quote: storeQuote(span.quote)
128
+ });
129
+ }
130
+ /**
131
+ * ⚠ THE KEY ORDER HERE IS THE WIRE ORDER, because `JSON.stringify` emits own keys in insertion
132
+ * order and a ledger line is a text artifact people will diff. Nothing depends on it semantically —
133
+ * a consumer parses JSON — but a format whose byte layout drifts between versions makes every diff
134
+ * of the ledger unreadable for no gain.
135
+ */
136
+ export function serialiseRecord(record) {
137
+ return Buffer.from(`${JSON.stringify(record)}\n`, 'utf8');
138
+ }
139
+ /**
140
+ * The count bounds, checked BEFORE anything is serialised or written.
141
+ *
142
+ * ⚠ SPANS ARE COUNTED ACROSS ALL SOURCES, not per source. A per-source cap of 256 with 64 sources
143
+ * admits 16,384 spans, which is the bound this is supposed to be — the whole reason the cap exists
144
+ * is the serialised size of one line, and size is a property of the line rather than of a source.
145
+ */
146
+ export function checkCounts(sources) {
147
+ let spans = 0;
148
+ for (const source of sources)
149
+ spans += source.spans.length;
150
+ return checkRequestCounts(sources.length, spans);
151
+ }
152
+ /**
153
+ * The same two bounds, over counts taken from the REQUEST rather than from resolved records.
154
+ *
155
+ * ⚠⚠ IT EXISTS SO A GUARANTEED REFUSAL CAN BE DECIDED BEFORE ANY SOURCE IS READ. Both caps are
156
+ * knowable from the request's own shape — 65 `derived_from` entries is over the cap whether or not
157
+ * a single one of those files exists — so checking them only after the source loop meant reading
158
+ * every source to reach a conclusion that never depended on them, and leaking each one's existence
159
+ * and timing on the way. `stamp.ts`'s step 3 carries the full argument.
160
+ *
161
+ * ⚠ ONE IMPLEMENTATION, TWO ENTRY POINTS. `checkCounts` now delegates here rather than repeating
162
+ * the comparisons, so the pre-check and the post-resolution check can never disagree about a bound
163
+ * or about the words a refusal uses.
164
+ */
165
+ export function checkPagePath(pagePath) {
166
+ const bytes = Buffer.byteLength(pagePath, 'utf8');
167
+ if (bytes > MAX_PAGE_PATH_BYTES) {
168
+ return scribeRefuse('LINEAGE_LINE_TOO_LARGE', `the page path is ${bytes} bytes; the ceiling is ${MAX_PAGE_PATH_BYTES}`);
169
+ }
170
+ return null;
171
+ }
172
+ export function checkRequestCounts(sources, spans) {
173
+ if (sources > MAX_SOURCES) {
174
+ return scribeRefuse('LINEAGE_LINE_TOO_LARGE', `a lineage line carries at most ${MAX_SOURCES} sources`);
175
+ }
176
+ if (spans > MAX_SPANS) {
177
+ return scribeRefuse('LINEAGE_LINE_TOO_LARGE', `a lineage line carries at most ${MAX_SPANS} spans`);
178
+ }
179
+ return null;
180
+ }
181
+ /**
182
+ * The size bound, checked on the SERIALISED bytes.
183
+ *
184
+ * ⚠ NEVER SPLIT A LINE. B4's atomicity argument is about one `write` of one complete line; a line
185
+ * split across two writes is exactly the interleaving the argument rules out, arriving through the
186
+ * repair for the case it could not handle. So an over-size line REFUSES and nothing is appended.
187
+ */
188
+ export function checkLineSize(line) {
189
+ if (line.length > MAX_LINE_BYTES) {
190
+ return scribeRefuse('LINEAGE_LINE_TOO_LARGE', `the serialised lineage line is ${line.length} bytes; the ceiling is ${MAX_LINE_BYTES}`);
191
+ }
192
+ return null;
193
+ }
194
+ //# sourceMappingURL=lineage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineage.js","sourceRoot":"","sources":["../src/lineage.ts"],"names":[],"mappings":"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"}
package/dist/main.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { Transport } from '@modelcontextprotocol/server';
2
+ import type { FenceRefusal, FsGate } from 'wyrd-fence';
3
+ import type { WritePageRequest, WritePageResult } from './stamp.js';
4
+ interface Scribe {
5
+ writePage(request: WritePageRequest): Promise<WritePageResult>;
6
+ }
7
+ export interface MainDeps {
8
+ readonly argv: readonly string[];
9
+ readonly env: Record<string, string | undefined>;
10
+ readonly makeFsGate: (options: {
11
+ rawGrant: string;
12
+ }) => FsGate | FenceRefusal;
13
+ readonly makeScribe?: (options: {
14
+ readonly gate: FsGate;
15
+ readonly version: string;
16
+ }) => Scribe;
17
+ readonly makeTransport?: () => Transport;
18
+ readonly stderr: (line: string) => void;
19
+ readonly setExitCode: (code: number) => void;
20
+ }
21
+ export interface MainResult {
22
+ readonly started: boolean;
23
+ readonly reason: string | null;
24
+ }
25
+ /** Plan the tier first; only a successful plan may reach the grant or transport factories. */
26
+ export declare function main(deps: MainDeps): Promise<MainResult>;
27
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,169 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
2
+ import { isRefusal } from 'wyrd-fence';
3
+ import { gateAppender } from './ledger.js';
4
+ import { createServer, FENCE_DISCLOSURE, planTier, productionInstructions, SERVER_VERSION } from './server.js';
5
+ import { SourceCache } from './source.js';
6
+ import { writePage } from './stamp.js';
7
+ function createScribe(options) {
8
+ const cache = new SourceCache();
9
+ const appender = options.appender ?? gateAppender(options.gate);
10
+ return Object.freeze({
11
+ writePage: (request) => writePage(request, {
12
+ gate: options.gate,
13
+ appender,
14
+ version: options.version,
15
+ cache
16
+ })
17
+ });
18
+ }
19
+ function writePageRegistration(context, activeTier) {
20
+ const description = [
21
+ `Tier ${activeTier}: attempt to create a page outside Arc/; target and sources use the grant fence.`,
22
+ '',
23
+ 'The target is created exclusively. An occupied leaf refuses EXISTS; this tool does not',
24
+ 'overwrite, delete or rename. The target and every derived_from.source are grant-relative',
25
+ 'and go through the imported wyrd-fence implementation.',
26
+ '',
27
+ FENCE_DISCLOSURE,
28
+ '',
29
+ 'Success creates the page and appends one lineage line. The vault configuration may also',
30
+ 'project lineage into frontmatter. If the ledger fails after creation, the page stays in',
31
+ 'place and the refusal includes both the created page and the underlying cause.'
32
+ ].join('\n');
33
+ const span = {
34
+ oneOf: [
35
+ {
36
+ type: 'object',
37
+ properties: {
38
+ offset: { type: 'integer', minimum: 0 },
39
+ length: { type: 'integer', minimum: 1 }
40
+ },
41
+ required: ['offset', 'length'],
42
+ additionalProperties: false
43
+ },
44
+ {
45
+ type: 'object',
46
+ properties: { quote: { type: 'string', minLength: 1 } },
47
+ required: ['quote'],
48
+ additionalProperties: false
49
+ },
50
+ {
51
+ type: 'object',
52
+ properties: {
53
+ offset: { type: 'integer', minimum: 0 },
54
+ length: { type: 'integer', minimum: 1 },
55
+ quote: { type: 'string', minLength: 1 }
56
+ },
57
+ required: ['offset', 'length', 'quote'],
58
+ additionalProperties: false
59
+ }
60
+ ]
61
+ };
62
+ const registration = {
63
+ declaration: {
64
+ name: 'write_page',
65
+ title: 'Create a page with lineage',
66
+ description,
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ path: { type: 'string', minLength: 1 },
71
+ content: { type: 'string' },
72
+ derived_from: {
73
+ type: 'array',
74
+ items: {
75
+ type: 'object',
76
+ properties: {
77
+ source: { type: 'string', minLength: 1 },
78
+ spans: { type: 'array', minItems: 1, items: span }
79
+ },
80
+ required: ['source', 'spans'],
81
+ additionalProperties: false
82
+ }
83
+ }
84
+ },
85
+ required: ['path', 'content', 'derived_from'],
86
+ additionalProperties: false
87
+ /*
88
+ * ⚠ `span` above is `as const`, so its arrays are `readonly`, and v2's schema type
89
+ * wants mutable ones. Same cause as the Reader's `READ_INPUT_SCHEMA_FOR_TOOL`
90
+ * (`wyrd/src/server.ts`), and found the same way: by BUILDING against v2, which no
91
+ * read-only review lane could do — three lanes read this plan and approved it.
92
+ *
93
+ * Widened at the boundary rather than by dropping `as const`. The literal types
94
+ * document the three legal span shapes (offset+length, quote, or all three), and
95
+ * that precision is enforced at runtime by the Scribe's own AJV validation, which
96
+ * is the check that actually protects the vault. Structurally identical; only
97
+ * readonly modifiers differ.
98
+ */
99
+ }
100
+ },
101
+ call: async (args) => {
102
+ const { derived_from, ...rest } = args;
103
+ const result = await context.scribe.writePage({
104
+ ...rest,
105
+ derivedFrom: derived_from
106
+ });
107
+ const prefix = result.ok
108
+ ? 'wyrd-scribe completed write_page.'
109
+ : result.reason === 'PAGE_WRITTEN_LEDGER_FAILED'
110
+ ? 'wyrd-scribe created the page, but no lineage append was confirmed; inspect `cause` and, when present, `cause.retained`, because the ledger may contain no new line, a fragment, or the complete line.'
111
+ : 'wyrd-scribe refused write_page.';
112
+ const serialised = JSON.stringify(result);
113
+ return {
114
+ ...(result.ok ? {} : { isError: true }),
115
+ content: [{ type: 'text', text: `${prefix}\n${serialised}` }]
116
+ };
117
+ }
118
+ };
119
+ return Object.freeze(registration);
120
+ }
121
+ const PRODUCTION_LAYERS = Object.freeze({
122
+ A: (context, activeTier) => Object.freeze([
123
+ writePageRegistration(context, activeTier)
124
+ ]),
125
+ B: null,
126
+ C: null
127
+ });
128
+ function readGrantArg(argv) {
129
+ for (let index = 0; index < argv.length; index += 1) {
130
+ const argument = argv[index];
131
+ if (argument === '--grant')
132
+ return argv[index + 1] ?? '';
133
+ if (argument.startsWith('--grant='))
134
+ return argument.slice('--grant='.length);
135
+ }
136
+ return null;
137
+ }
138
+ /** Plan the tier first; only a successful plan may reach the grant or transport factories. */
139
+ export async function main(deps) {
140
+ const planned = planTier(deps.env['WYRD_SCRIBE_TIER'], PRODUCTION_LAYERS);
141
+ if (!planned.ok) {
142
+ deps.stderr(planned.message);
143
+ deps.setExitCode(2);
144
+ return { started: false, reason: planned.reason };
145
+ }
146
+ const fromArgv = readGrantArg(deps.argv);
147
+ const rawGrant = fromArgv !== null ? fromArgv : (deps.env['WYRD_GRANT'] ?? null);
148
+ if (rawGrant === null || rawGrant === '') {
149
+ deps.stderr('wyrd-scribe: refusing to start — no folder has been granted; use --grant or WYRD_GRANT.');
150
+ deps.setExitCode(2);
151
+ return { started: false, reason: 'NO_GRANT' };
152
+ }
153
+ const gate = deps.makeFsGate({ rawGrant });
154
+ if (isRefusal(gate)) {
155
+ deps.stderr(`wyrd-scribe: refusing to start — ${gate.detail}`);
156
+ deps.setExitCode(2);
157
+ return { started: false, reason: gate.reason };
158
+ }
159
+ const context = Object.freeze({
160
+ instructions: (tier, names) => productionInstructions(gate.disclosedRoot(), tier, names),
161
+ scribe: deps.makeScribe?.({ gate, version: SERVER_VERSION })
162
+ ?? createScribe({ gate, version: SERVER_VERSION })
163
+ });
164
+ const server = createServer(planned, context);
165
+ const transport = deps.makeTransport?.() ?? new StdioServerTransport();
166
+ await server.connect(transport);
167
+ return { started: true, reason: null };
168
+ }
169
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * THE SCRIBE'S OWN REFUSAL SHAPE, and the one rule that binds every field in it.
3
+ *
4
+ * ⚠⚠ NEVER A RESOLVED ABSOLUTE PATH IN A `detail`. This is the fence's `META-no-outside-names`
5
+ * property carried across the package boundary — the fence returns `rel` and never `actual`
6
+ * precisely so a refusal cannot become a filesystem oracle, and a Scribe refusal that echoed a
7
+ * caller-supplied path would hand that oracle back one layer up. So a Scribe `detail` names a
8
+ * CONSTANT (`.wyrd/scribe.json`) or a BOUND (`65536 bytes`), never a request string.
9
+ *
10
+ * ⚠ THE CALLER'S OWN REQUEST STRING IS NOT SAFE TO ECHO EITHER, and that is the non-obvious half.
11
+ * It looks harmless — the caller already knows what it sent — but the refusal travels to a MODEL
12
+ * driving the server over stdio, and echoing `C:\Users\...\secrets\x.md` back into a transcript
13
+ * puts an outside name in the one place the fence spent seven rounds keeping it out of. A refusal
14
+ * says which RULE fired; it does not repeat the request.
15
+ *
16
+ * ⚠ FENCE REFUSALS PASS THROUGH UNCHANGED and are NOT re-wrapped in this shape. A parallel
17
+ * vocabulary for `ESCAPES`/`MISSING`/`EXISTS` would be a second thing to get wrong, and D8's whole
18
+ * argument is that there is one containment implementation and one set of words for its outcomes.
19
+ * `SOURCE_MISSING` in the spec's prose IS the fence's `MISSING`; it is not a distinct reason.
20
+ */
21
+ /**
22
+ * The reasons the Scribe itself originates. `ARC_IMMUTABLE` is doctrine (D7) rather than a fence
23
+ * outcome, so it lives here with the rest.
24
+ */
25
+ export type ScribeReason = 'BAD_INPUT' | 'DERIVED_FROM_INVALID' | 'ARC_IMMUTABLE' | 'SCRIBE_NOT_INITIALISED' | 'SCRIBE_CONFIG_INVALID' | 'SCRIBE_CONFIG_TOO_LARGE' | 'SOURCE_CHANGED_DURING_READ' | 'FRONTMATTER_INVALID' | 'FRONTMATTER_CONFLICT' | 'LINEAGE_LINE_TOO_LARGE' | 'PAGE_WRITTEN_LEDGER_FAILED';
26
+ /**
27
+ * ⚠ NO `resolvedPath`, DELIBERATELY. The fence carries that field because a CONFIG refusal names
28
+ * the grant the user themselves configured; a Scribe refusal has no such field to fill honestly,
29
+ * and an empty-string placeholder would be a slot a later session fills with the wrong thing.
30
+ */
31
+ export interface ScribeRefusal {
32
+ readonly ok: false;
33
+ readonly reason: ScribeReason;
34
+ readonly detail: string;
35
+ }
36
+ export declare function scribeRefuse(reason: ScribeReason, detail: string): ScribeRefusal;