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.
- package/LICENSE +201 -0
- package/README.md +114 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.js +215 -0
- package/dist/config.js.map +1 -0
- package/dist/frontmatter.d.ts +87 -0
- package/dist/frontmatter.js +302 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger.d.ts +69 -0
- package/dist/ledger.js +64 -0
- package/dist/ledger.js.map +1 -0
- package/dist/lineage.d.ts +195 -0
- package/dist/lineage.js +194 -0
- package/dist/lineage.js.map +1 -0
- package/dist/main.d.ts +27 -0
- package/dist/main.js +169 -0
- package/dist/main.js.map +1 -0
- package/dist/refusal.d.ts +36 -0
- package/dist/refusal.js +24 -0
- package/dist/refusal.js.map +1 -0
- package/dist/server.d.ts +34 -0
- package/dist/server.js +145 -0
- package/dist/server.js.map +1 -0
- package/dist/source.d.ts +76 -0
- package/dist/source.js +134 -0
- package/dist/source.js.map +1 -0
- package/dist/span.d.ts +117 -0
- package/dist/span.js +242 -0
- package/dist/span.js.map +1 -0
- package/dist/stamp.d.ts +297 -0
- package/dist/stamp.js +773 -0
- package/dist/stamp.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +60 -0
|
@@ -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;
|
package/dist/lineage.js
ADDED
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAEhD,+FAA+F;AAC/F,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AACvC,yDAAyD;AACzD,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC9B,mEAAmE;AACnE,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAC7B,wDAAwD;AACxD,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AACrC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AAwEzC,MAAM,UAAU,QAAQ,CAAC,OAAe,EAAE,IAAY;IAClD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,KAAa;IACrD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,QAAiB,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACnC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,IAAI,CAAC,MAAM,IAAI,iBAAiB,EAAE,CAAC;QACnC,OAAO,MAAM,CAAC,MAAM,CAAC;YACjB,IAAI;YACJ,mBAAmB,EAAE,IAAI,CAAC,MAAM;YAChC,iBAAiB,EAAE,IAAI,CAAC,MAAM;YAC9B,MAAM;YACN,SAAS,EAAE,KAAK;SACnB,CAAC,CAAC;IACP,CAAC;IACD,kFAAkF;IAClF,gGAAgG;IAChG,oEAAoE;IACpE,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,iBAAiB,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,iBAAiB,EAAE,MAAM,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACzC,OAAO,MAAM,CAAC,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC7B,mBAAmB,EAAE,IAAI,CAAC,MAAM;QAChC,iBAAiB,EAAE,OAAO;QAC1B,MAAM;QACN,SAAS,EAAE,IAAI;KAClB,CAAC,CAAC;AACP,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAa;IAClC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAkB;IAC1C,OAAO,MAAM,CAAC,MAAM,CAAC;QACjB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;KAChC,CAAC,CAAC;AACP,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAqB;IACjD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,OAAiC;IACzD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,MAAM,IAAI,OAAO;QAAE,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;IAC3D,OAAO,kBAAkB,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAClD,IAAI,KAAK,GAAG,mBAAmB,EAAE,CAAC;QAC9B,OAAO,YAAY,CACf,wBAAwB,EACxB,oBAAoB,KAAK,0BAA0B,mBAAmB,EAAE,CAC3E,CAAC;IACN,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,KAAa;IAC7D,IAAI,OAAO,GAAG,WAAW,EAAE,CAAC;QACxB,OAAO,YAAY,CACf,wBAAwB,EACxB,kCAAkC,WAAW,UAAU,CAC1D,CAAC;IACN,CAAC;IACD,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;QACpB,OAAO,YAAY,CACf,wBAAwB,EACxB,kCAAkC,SAAS,QAAQ,CACtD,CAAC;IACN,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACtC,IAAI,IAAI,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;QAC/B,OAAO,YAAY,CACf,wBAAwB,EACxB,kCAAkC,IAAI,CAAC,MAAM,0BAA0B,cAAc,EAAE,CAC1F,CAAC;IACN,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC"}
|
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
|
package/dist/main.js.map
ADDED
|
@@ -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;
|