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
package/dist/span.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B1 — THE SPAN RESOLVER. Bytes in, a located span or a refusal out.
|
|
3
|
+
*
|
|
4
|
+
* Built to `designs/2026-08-31-span-resolver-plan.md`, which carries the reasoning and four rounds
|
|
5
|
+
* of gate findings. The one rule everything here serves:
|
|
6
|
+
*
|
|
7
|
+
* ⚠⚠ NO SILENT MISLOCATION. AMBIGUITY REFUSES; IT NEVER PICKS.
|
|
8
|
+
*
|
|
9
|
+
* ⚠ FENCE-FREE, AND THAT IS STRUCTURAL RATHER THAN POLITE. This module takes `(bytes, request)` and
|
|
10
|
+
* no path. It never opens a file, never imports the fence, and has no way to reach the filesystem —
|
|
11
|
+
* reading is the fence's job, and a resolver that opens files has become a second write path.
|
|
12
|
+
*
|
|
13
|
+
* ⚠ THE PROPERTY THIS MODULE PROTECTS: no substituted span resolution in a refusal, and no source
|
|
14
|
+
* bytes in EITHER variant. It is NOT the fence's no-absolute-path rule — that belongs to
|
|
15
|
+
* `wyrd-fence` (`packages/wyrd-fence/src/fsgate.ts`) and is asserted there. A reviewer briefed on
|
|
16
|
+
* the fence's property is on the wrong axis here.
|
|
17
|
+
*
|
|
18
|
+
* ⚠⚠ AND THE PROPERTY IS NOT ENFORCED BY THESE TYPES. `SpanRefusal` and `ResolvedSpan` are
|
|
19
|
+
* TypeScript-only shapes: the compiler forbids no extra property at runtime, and an unchecked
|
|
20
|
+
* assignment can put a value anywhere. The enforcement is the own-key envelope in
|
|
21
|
+
* `test/span.test.js`, applied to EVERY arm — refusal and success alike. This comment is not a
|
|
22
|
+
* mechanism.
|
|
23
|
+
*
|
|
24
|
+
* ⚠ `Object.freeze` on every return is the runtime half OF THE EXTRA-KEY RULE ONLY. It says
|
|
25
|
+
* nothing about what an ACCESSOR returns: freeze constrains the property, never the getter, so a
|
|
26
|
+
* frozen own getter can serve a correct value to the first readers and the source buffer to the
|
|
27
|
+
* next. Measured at code-gate round 2 — it passed all 29 arms. What closes that is the
|
|
28
|
+
* data-property assertion in `envelope()`, not this line.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* ⚠ AN UNKNOWN KEY REFUSES RATHER THAN BEING IGNORED, and the reason is a fail-open, not tidiness.
|
|
32
|
+
* `{ offset, length, qoute }` read leniently is a valid OFFSETS-ONLY request: it succeeds, records
|
|
33
|
+
* a quote the caller never asked to have verified, and silently drops the verification the caller
|
|
34
|
+
* was asking for. That is mislocation arriving through a typo.
|
|
35
|
+
*/
|
|
36
|
+
const REQUEST_KEYS = new Set(['offset', 'length', 'quote']);
|
|
37
|
+
function refuse(reason, where) {
|
|
38
|
+
return Object.freeze({ ok: false, reason, where });
|
|
39
|
+
}
|
|
40
|
+
function resolved(offset, length, quote) {
|
|
41
|
+
return Object.freeze({ ok: true, offset, length, quote });
|
|
42
|
+
}
|
|
43
|
+
/** A UTF-8 continuation byte — `10xxxxxx`. A span may not START on one, nor END just before one. */
|
|
44
|
+
function isContinuation(byte) {
|
|
45
|
+
return byte !== undefined && (byte & 0xc0) === 0x80;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* ⚠ BOTH ENDPOINTS, AND THE ARMS TEST THEM SEPARATELY. `é` is `c3 a9`: `{1,1}` cuts the START,
|
|
49
|
+
* `{0,1}` cuts the END, and a check that validates one side passes half the rule while reading as
|
|
50
|
+
* complete.
|
|
51
|
+
*
|
|
52
|
+
* Why a non-aligned span refuses at all: D4 stores the quoted TEXT over a source model the spec
|
|
53
|
+
* declares UTF-8, and a lone `a9` has no lossless UTF-8 text representation. Decoding substitutes
|
|
54
|
+
* `U+FFFD`, so the stored quote no longer re-encodes to the bytes it came from — the identity D4
|
|
55
|
+
* exists to establish is destroyed at the moment of recording.
|
|
56
|
+
*/
|
|
57
|
+
function alignmentFault(bytes, offset, length) {
|
|
58
|
+
if (isContinuation(bytes[offset]))
|
|
59
|
+
return 'offset';
|
|
60
|
+
const end = offset + length;
|
|
61
|
+
if (end < bytes.length && isContinuation(bytes[end]))
|
|
62
|
+
return 'length';
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* ⚠ THE QUOTE IS VALIDATED FOR LOSSLESS UTF-8 ROUND-TRIP BEFORE ANY MATCHING. `"\uD800"` is a
|
|
67
|
+
* well-formed JavaScript string and NOT well-formed Unicode text; encoding it substitutes
|
|
68
|
+
* `U+FFFD`, so an unvalidated lone-surrogate quote would match GENUINE replacement-character bytes
|
|
69
|
+
* in the source and return a quote that does not identify them.
|
|
70
|
+
*
|
|
71
|
+
* The empty quote refuses here too: against an empty source it would otherwise "uniquely match"
|
|
72
|
+
* and mint a zero-length span, which the positive-length rule forbids on the offsets side.
|
|
73
|
+
*/
|
|
74
|
+
function quoteFault(quote) {
|
|
75
|
+
if (typeof quote !== 'string' || quote.length === 0)
|
|
76
|
+
return refuse('SPAN_INVALID_QUOTE', 'quote');
|
|
77
|
+
if (Buffer.from(quote, 'utf8').toString('utf8') !== quote)
|
|
78
|
+
return refuse('SPAN_INVALID_QUOTE', 'quote');
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/** Narrows `spanShapeFault`'s result. A refusal carries `ok: false`; members never do. */
|
|
82
|
+
function isMembers(value) {
|
|
83
|
+
return value.ok !== false;
|
|
84
|
+
}
|
|
85
|
+
export function spanShapeFault(request) {
|
|
86
|
+
const raw = request;
|
|
87
|
+
if (typeof raw !== 'object' || raw === null)
|
|
88
|
+
return refuse('SPAN_INVALID_RANGE', 'request');
|
|
89
|
+
// ⚠ OWN KEYS, NOT `in`. `in` walks the prototype chain, so an inherited `offset` would be read
|
|
90
|
+
// as a supplied one. Symbol keys are unknown keys and refuse with the rest.
|
|
91
|
+
for (const key of Reflect.ownKeys(raw)) {
|
|
92
|
+
if (typeof key !== 'string' || !REQUEST_KEYS.has(key))
|
|
93
|
+
return refuse('SPAN_INVALID_RANGE', 'request');
|
|
94
|
+
}
|
|
95
|
+
const own = (key) => Object.prototype.hasOwnProperty.call(raw, key);
|
|
96
|
+
const hasOffset = own('offset');
|
|
97
|
+
const hasLength = own('length');
|
|
98
|
+
const hasQuote = own('quote');
|
|
99
|
+
// ⚠ THE SAME READ-ONCE, READ-ONLY-WHAT-WAS-SUPPLIED DISCIPLINE AS THE RESOLVER BELOW, and it
|
|
100
|
+
// binds here for an additional reason: this function runs BEFORE any source is read, so an
|
|
101
|
+
// inherited accessor fired here would run earlier than the one the resolver's rule is about.
|
|
102
|
+
const view = raw;
|
|
103
|
+
const offsetValue = hasOffset ? view.offset : undefined;
|
|
104
|
+
const lengthValue = hasLength ? view.length : undefined;
|
|
105
|
+
const quoteValue = hasQuote ? view.quote : undefined;
|
|
106
|
+
if (!hasOffset && !hasLength && !hasQuote)
|
|
107
|
+
return refuse('SPAN_INVALID_RANGE', 'request');
|
|
108
|
+
// ⚠ `offset` and `length` are REQUIRED TOGETHER; `where` names the member that is missing.
|
|
109
|
+
if (hasOffset !== hasLength)
|
|
110
|
+
return refuse('SPAN_INVALID_RANGE', hasOffset ? 'length' : 'offset');
|
|
111
|
+
if (hasOffset) {
|
|
112
|
+
/**
|
|
113
|
+
* ⚠⚠ RANGE VALIDATION RUNS FIRST AND COMPLETELY. Common byte-slice APIs coerce or clamp a
|
|
114
|
+
* fractional, negative or oversized bound silently, producing a quote for a DIFFERENT
|
|
115
|
+
* effective range while the record stores the offsets as supplied. That is silent
|
|
116
|
+
* mislocation arriving through type coercion. An invalid range must never reach the
|
|
117
|
+
* matcher, or the refusal describes the wrong failure.
|
|
118
|
+
*
|
|
119
|
+
* ⚠ THE `> bytes.length` HALF IS NOT HERE, and its absence is the boundary of this
|
|
120
|
+
* function. Whether a well-formed range FITS is a question about the source.
|
|
121
|
+
*/
|
|
122
|
+
if (!Number.isSafeInteger(offsetValue) || offsetValue < 0) {
|
|
123
|
+
return refuse('SPAN_INVALID_RANGE', 'offset');
|
|
124
|
+
}
|
|
125
|
+
if (!Number.isSafeInteger(lengthValue) || lengthValue <= 0) {
|
|
126
|
+
return refuse('SPAN_INVALID_RANGE', 'length');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ⚠ THE QUOTE'S OWN WELL-FORMEDNESS IS SOURCE-INDEPENDENT ON BOTH PATHS — a lone surrogate or
|
|
130
|
+
// an empty string refuses against any source at all — so it is decided here for both, in the
|
|
131
|
+
// order the resolver reaches it: after the range members on the offsets path, immediately on
|
|
132
|
+
// the quote-only path.
|
|
133
|
+
if (hasQuote) {
|
|
134
|
+
const badQuote = quoteFault(quoteValue);
|
|
135
|
+
if (badQuote)
|
|
136
|
+
return badQuote;
|
|
137
|
+
}
|
|
138
|
+
return Object.freeze({ hasOffset, hasQuote, offsetValue, lengthValue, quoteValue });
|
|
139
|
+
}
|
|
140
|
+
export function resolveSpan(bytes, request) {
|
|
141
|
+
if (!Buffer.isBuffer(bytes)) {
|
|
142
|
+
throw new TypeError('resolveSpan(bytes, request): bytes must be a Buffer of source bytes');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* ⚠⚠ THE SOURCE-INDEPENDENT PREFIX, RUN AS ONE STEP, AND IT CARRIES THE READ-ONCE RULE WITH IT.
|
|
146
|
+
* Every refusal it can reach is one this function reached itself before the extraction, in the
|
|
147
|
+
* identical order — which is what makes `stamp.ts`'s pre-check a prediction of this resolver
|
|
148
|
+
* rather than a second opinion about the same request. It also performs the ONLY read of the
|
|
149
|
+
* request's members on this path and hands the values back: reading them again here would fire
|
|
150
|
+
* a caller-supplied accessor twice, which is `M1`'s defect arriving through a refactor.
|
|
151
|
+
*/
|
|
152
|
+
const shape = spanShapeFault(request);
|
|
153
|
+
if (!isMembers(shape))
|
|
154
|
+
return shape;
|
|
155
|
+
const { hasOffset, hasQuote, offsetValue, lengthValue, quoteValue } = shape;
|
|
156
|
+
if (hasOffset) {
|
|
157
|
+
const offset = offsetValue;
|
|
158
|
+
const length = lengthValue;
|
|
159
|
+
// ⚠ OVERFLOW-SAFE BY SUBTRACTION, NEVER `offset + length > bytes.length`. Both operands are
|
|
160
|
+
// safe integers, so `bytes.length - offset` is exact and small; the sum is never compared.
|
|
161
|
+
if (offset > bytes.length)
|
|
162
|
+
return refuse('SPAN_INVALID_RANGE', 'offset');
|
|
163
|
+
if (length > bytes.length - offset)
|
|
164
|
+
return refuse('SPAN_INVALID_RANGE', 'length');
|
|
165
|
+
const fault = alignmentFault(bytes, offset, length);
|
|
166
|
+
if (fault)
|
|
167
|
+
return refuse('SPAN_NOT_ALIGNED', fault);
|
|
168
|
+
const slice = bytes.subarray(offset, offset + length);
|
|
169
|
+
const text = slice.toString('utf8');
|
|
170
|
+
/**
|
|
171
|
+
* ⚠ ALIGNMENT IS NECESSARY AND NOT SUFFICIENT. `41 c3` is aligned at both endpoints and
|
|
172
|
+
* still decodes lossily, because the sequence INSIDE the span is truncated. The round-trip
|
|
173
|
+
* is the property alignment is a proxy for, so it is asserted directly rather than assumed
|
|
174
|
+
* from the endpoints. `where` is `'request'`: with a well-formed source neither endpoint is
|
|
175
|
+
* at fault, and naming one would be a guess.
|
|
176
|
+
*/
|
|
177
|
+
if (!Buffer.from(text, 'utf8').equals(slice))
|
|
178
|
+
return refuse('SPAN_NOT_ALIGNED', 'request');
|
|
179
|
+
if (!hasQuote)
|
|
180
|
+
return resolved(offset, length, text);
|
|
181
|
+
/**
|
|
182
|
+
* ⚠⚠ MISMATCH PREFERS NEITHER SIDE, AND `where` IS `'request'`. Preferring the offsets
|
|
183
|
+
* silently rewrites the caller's quote; preferring the quote silently relocates the
|
|
184
|
+
* caller's offsets. Naming `'quote'` at fault IS preferring the offsets — the first of the
|
|
185
|
+
* two failures the rule forbids. A whole-request inconsistency is `'request'`.
|
|
186
|
+
*
|
|
187
|
+
* String comparison is exact and safe here only because both sides are known to round-trip
|
|
188
|
+
* losslessly: the source slice by the check above, the quote by `quoteFault`.
|
|
189
|
+
*/
|
|
190
|
+
if (quoteValue !== text)
|
|
191
|
+
return refuse('SPAN_MISMATCH', 'request');
|
|
192
|
+
return resolved(offset, length, text);
|
|
193
|
+
}
|
|
194
|
+
// ⚠ THE QUOTE IS ALREADY KNOWN WELL-FORMED — `spanShapeFault` decided it, on both paths, before
|
|
195
|
+
// any byte was looked at. The two `quoteFault` calls that used to sit here and in the offsets
|
|
196
|
+
// branch are GONE rather than kept as belt-and-braces: a second call would re-run a decided
|
|
197
|
+
// check against a value that can no longer change, and a guard nothing can trip is a guard
|
|
198
|
+
// nobody is measuring.
|
|
199
|
+
const needle = Buffer.from(quoteValue, 'utf8');
|
|
200
|
+
/**
|
|
201
|
+
* ⚠⚠ THE SCAN ADVANCES BY ONE BYTE, NEVER BY THE NEEDLE LENGTH. Bytes `"aaa"` with quote `"aa"`
|
|
202
|
+
* match at offset 0 AND offset 1; a scan advancing by the needle length finds one match and
|
|
203
|
+
* returns it, passing a naive occurs-twice arm while resolving an ambiguous span. Two matches
|
|
204
|
+
* is all this needs to know, so it stops there.
|
|
205
|
+
*/
|
|
206
|
+
let first = -1;
|
|
207
|
+
let ambiguous = false;
|
|
208
|
+
for (let from = 0; from <= bytes.length - needle.length;) {
|
|
209
|
+
const at = bytes.indexOf(needle, from);
|
|
210
|
+
if (at === -1)
|
|
211
|
+
break;
|
|
212
|
+
if (first === -1) {
|
|
213
|
+
first = at;
|
|
214
|
+
from = at + 1;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
ambiguous = true;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (first === -1)
|
|
221
|
+
return refuse('SPAN_NOT_FOUND', 'quote');
|
|
222
|
+
if (ambiguous)
|
|
223
|
+
return refuse('SPAN_AMBIGUOUS', 'quote');
|
|
224
|
+
/**
|
|
225
|
+
* ⚠ ALIGNMENT IS A PROPERTY OF THE SPAN, SO IT IS CHECKED ON EVERY SHAPE THAT PRODUCES ONE.
|
|
226
|
+
* In well-formed UTF-8 a located match is aligned by construction, since a valid encoded quote
|
|
227
|
+
* never begins with a continuation byte. In a malformed source it need not be — `41 a9` with
|
|
228
|
+
* quote `"A"` ends immediately before a continuation byte — and applying the rule only to the
|
|
229
|
+
* shape that supplies offsets is the one-sided check this plan kept catching.
|
|
230
|
+
*/
|
|
231
|
+
// ⚠ `where` is 'request', NOT the endpoint `alignmentFault` names. A quote-only request has no
|
|
232
|
+
// `offset` and no `length` member, so naming either blames something the caller never sent —
|
|
233
|
+
// the diagnostic would point at a value it derived itself. The offsets path above reaches the
|
|
234
|
+
// same conclusion for a malformed source and says so in its own words: naming one endpoint
|
|
235
|
+
// "would be a guess." Extending alignment to quote-only matches was right; inheriting the
|
|
236
|
+
// offsets path's `where` with it was not. Both round-1 code-gate lenses, both judging the CODE
|
|
237
|
+
// wrong and the SPEC right.
|
|
238
|
+
if (alignmentFault(bytes, first, needle.length))
|
|
239
|
+
return refuse('SPAN_NOT_ALIGNED', 'request');
|
|
240
|
+
return resolved(first, needle.length, quoteValue);
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=span.js.map
|
package/dist/span.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"span.js","sourceRoot":"","sources":["../src/span.ts"],"names":[],"mappings":"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"}
|
package/dist/stamp.d.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `writePage` — THE STAMP PATH, TIER A (create-only). The Scribe-side half of the spec's step list.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ THE ORDER IS THE SECURITY PROPERTY, NOT AN IMPLEMENTATION DETAIL. Read the numbered steps
|
|
5
|
+
* below as a sequence of things that must not have happened yet:
|
|
6
|
+
*
|
|
7
|
+
* · EVERY REFUSAL KNOWABLE FROM THE REQUEST'S OWN SHAPE IS DECIDED BEFORE ANY SOURCE IS READ —
|
|
8
|
+
* the `Arc/` screen, the count caps, and every span fault. If any of them ran after, a caller
|
|
9
|
+
* could send a request that was ALWAYS going to refuse — an `Arc/` target, 65 sources, a
|
|
10
|
+
* malformed span — with a `derived_from` naming any path on the machine, and read those
|
|
11
|
+
* sources' existence and hashes out of the refusal's timing and reason. That turns a check into
|
|
12
|
+
* an EXISTENCE ORACLE, which is D8's hole arriving through the back door after D8 closed the
|
|
13
|
+
* front one. ⚠ The rule was written for `Arc/` and enforced only there until 2026-09-02; the
|
|
14
|
+
* counts and the span shapes were being decided after the whole source loop had run.
|
|
15
|
+
*
|
|
16
|
+
* ⚠⚠ THIS SAID "EVERY GUARANTEED REFUSAL" UNTIL 2026-09-08 AND THAT IS NO LONGER TRUE, so the
|
|
17
|
+
* claim is narrowed to what the order actually buys. The fence's `PARENT_ALIAS` is a guaranteed
|
|
18
|
+
* refusal — the page's parent is an alias, and no source read can change that — but it is
|
|
19
|
+
* knowable only from the vault's TOPOLOGY, not from the request's shape, and it fires at page
|
|
20
|
+
* creation in step 6, AFTER the source loop. ⚠ The oracle argument survives the narrowing: what
|
|
21
|
+
* a caller learns from a `PARENT_ALIAS` is a fact about a directory INSIDE the grant, which
|
|
22
|
+
* every read operation already discloses. The `.wyrd/` half is not affected at all — step 4's
|
|
23
|
+
* `loadConfig` refuses an aliased `.wyrd` before step 5 reads anything.
|
|
24
|
+
* · NO PAGE IS WRITTEN until every source has resolved and every limit KNOWABLE BEFORE THE PAGE
|
|
25
|
+
* EXISTS has passed. When a page IS created and the ledger step then REFUSES — the final size
|
|
26
|
+
* check at step 9, or the append after it — this invocation leaves that page with no ledger
|
|
27
|
+
* line: an ORPHAN, which the no-rollback ruling below leaves on disk and which the
|
|
28
|
+
* create-before-append clause two bullets down calls "detectable and repairable". That is what
|
|
29
|
+
* the draft size check at step 6 stands in front of.
|
|
30
|
+
*
|
|
31
|
+
* ⚠ THE FIRST SENTENCE READ UNCONDITIONALLY UNTIL 2026-09-08 — "a page created before the
|
|
32
|
+
* ledger line is known to be serialisable is a page this invocation leaves with no ledger line"
|
|
33
|
+
* — WHICH IS WIDER THAN THE CODE. The ordinary path creates the page before the FINAL size
|
|
34
|
+
* check and then appends successfully; the orphan is what a REFUSAL at either ledger step
|
|
35
|
+
* leaves, not what the ordering leaves. The condition is now in the sentence.
|
|
36
|
+
*
|
|
37
|
+
* ⚠ AND IT SAID "a page whose provenance can NEVER be recorded" BEFORE THAT, WHICH THE CODE
|
|
38
|
+
* ALSO DID NOT ESTABLISH. What this invocation leaves behind is a page with no ledger line.
|
|
39
|
+
* **`writePage` provides no pass that records one later, and re-invoking it on the same path
|
|
40
|
+
* refuses `EXISTS`** — so nothing in this package records one, which is narrower than the
|
|
41
|
+
* retired "a later pass can still record one" and is what the code actually says. Overclaiming
|
|
42
|
+
* in either direction is not a harmless flourish: the first makes the residual hole at step 9
|
|
43
|
+
* (`ST41`) sound impossible instead of merely narrow, the second promises a repair path this
|
|
44
|
+
* package does not contain.
|
|
45
|
+
*
|
|
46
|
+
* ⚠⚠ "EVERY LIMIT" IS NOT WHAT THIS ORDERING BUYS AND THE CLAUSE SAID SO UNTIL 2026-09-08. One
|
|
47
|
+
* limit is decided AFTER the page exists: the final ledger-line size check at step 9, over the
|
|
48
|
+
* CANONICAL path the fence returns from the create. It is reachable — a fold-equal parent alias
|
|
49
|
+
* of different byte width makes `created.rel` wider than the request the draft was sized
|
|
50
|
+
* against (`ST41`) — and when it fires the page is already on disk with no ledger line. That
|
|
51
|
+
* ORPHAN is the outcome, asserted PRESENT on purpose by `ST38` and `ST41` so a green suite is
|
|
52
|
+
* never read as meaning it was closed. What the ordering buys is the narrower claim, which is
|
|
53
|
+
* still the one worth having: no page exists until every check that CAN be decided without one
|
|
54
|
+
* has passed.
|
|
55
|
+
*
|
|
56
|
+
* ⚠⚠ THIS SAID "NOTHING IS WRITTEN" UNTIL 2026-09-08 AND THAT IS NO LONGER TRUE EITHER, so the
|
|
57
|
+
* subject is narrowed to the thing the ordering actually protects. Option C's part 3 hoists
|
|
58
|
+
* `loadConfig`'s exclusive create ahead of its read, so STEP 4 CAN CREATE `.wyrd/scribe.json`
|
|
59
|
+
* BEFORE ANY SOURCE IS READ. The distinction is worth the words: the config is this server's
|
|
60
|
+
* own bookkeeping, minted at a fixed path inside a directory the user created for it, and a
|
|
61
|
+
* vault that gains one has gained none of the caller's PAGE CONTENT (the `vault_id` it carries
|
|
62
|
+
* is whatever `newUuid` returned, and a caller supplying that option chose it). The PAGE is
|
|
63
|
+
* the first write of caller-supplied content, and it is still the write every check above
|
|
64
|
+
* stands in front of.
|
|
65
|
+
* Sources are read BETWEEN the two.
|
|
66
|
+
* · the page is created BEFORE the ledger line is appended, and that ORDER IS ALSO DELIBERATE.
|
|
67
|
+
* The reverse records provenance for a page that may never exist, which is a FALSE PROVENANCE
|
|
68
|
+
* CLAIM — D4 names that as the failure that matters, worse than breakage, because it looks
|
|
69
|
+
* healthy. A page with a missing ledger line is DETECTABLE — the page is on disk and the ledger
|
|
70
|
+
* has no line naming it, which a reader can see — and repairable BY SOMETHING OUTSIDE THIS
|
|
71
|
+
* PACKAGE; a ledger line with no page is a lie in the permanent record, and nothing detects it.
|
|
72
|
+
* ⚠ "repairable" ALONE READ AS A PROMISE THIS PACKAGE KEEPS, WHICH IT DOES NOT: `writePage` has
|
|
73
|
+
* no repair pass and refuses `EXISTS` on the orphan's own path. What the ordering buys is that
|
|
74
|
+
* the failure is the visible kind rather than the invisible kind.
|
|
75
|
+
*
|
|
76
|
+
* ⚠⚠ AND THERE IS NO ROLLBACK, WHICH IS A RULING RATHER THAN AN OMISSION. If the append fails after
|
|
77
|
+
* the page was created, the page STAYS and the refusal says so (`PAGE_WRITTEN_LEDGER_FAILED`,
|
|
78
|
+
* carrying the fence's `Created`). Two reasons, and the second is the load-bearing one: the fence
|
|
79
|
+
* has no delete primitive at all in tier A, and deleting BY PATH would reopen the TOCTOU window the
|
|
80
|
+
* fence's `wx` create exists to close — between the create and the delete, the name can become
|
|
81
|
+
* something else. A caller told exactly what happened can decide; a caller handed a silent cleanup
|
|
82
|
+
* that deleted the wrong object cannot.
|
|
83
|
+
*/
|
|
84
|
+
import type { Created, FenceRefusal, FsGate } from 'wyrd-fence';
|
|
85
|
+
import type { LineageRecord } from './lineage.js';
|
|
86
|
+
import type { LedgerAppender } from './ledger.js';
|
|
87
|
+
import type { ScribeRefusal } from './refusal.js';
|
|
88
|
+
import { SourceCache } from './source.js';
|
|
89
|
+
import type { SpanRefusal, SpanRequest } from './span.js';
|
|
90
|
+
/** A `derived_from` entry: one source path and the spans cited from it. */
|
|
91
|
+
export interface DerivedFrom {
|
|
92
|
+
readonly source: string;
|
|
93
|
+
readonly spans: readonly SpanRequest[];
|
|
94
|
+
}
|
|
95
|
+
export interface WritePageRequest {
|
|
96
|
+
readonly path: string;
|
|
97
|
+
readonly content: string;
|
|
98
|
+
readonly derivedFrom: readonly DerivedFrom[];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* ⚠⚠ THE OUTER SIDE-EFFECT SUMMARY, AND IT IS A DIFFERENT QUESTION FROM `retained`.
|
|
102
|
+
*
|
|
103
|
+
* `retained` is the FENCE's field and it is honest exactly as scoped: **it describes the ONE fence
|
|
104
|
+
* call that produced the refusal it sits on** — whether that call may have left bytes at the target
|
|
105
|
+
* IT was given. It says nothing about the invocation around it, and reading it as an
|
|
106
|
+
* invocation-wide "nothing happened" is the mistake this field exists to ANSWER — the answer is
|
|
107
|
+
* available beside it now, which is a different thing from the misreading being unavailable; a
|
|
108
|
+
* caller can still read `retained` alone and reach the old wrong conclusion. Since
|
|
109
|
+
* part 3 hoisted `loadConfig`'s exclusive create ahead of its read, a `writePage` that refuses at
|
|
110
|
+
* step 4 or later may ALREADY have minted the vault's config — a real, persistent change to the
|
|
111
|
+
* user's vault — while every `retained` in sight is `null` because the target that fence call was
|
|
112
|
+
* given was never opened.
|
|
113
|
+
*
|
|
114
|
+
* ⚠ WHICH TARGET IT DESCRIBES IS GIVEN BY WHERE THE REFUSAL AROSE, AND THERE IS NO SHORTER RULE
|
|
115
|
+
* THAN THAT. `retained`, where present, describes the fence call that produced the refusal it sits
|
|
116
|
+
* on — which target that call was given is answered by reading where in `writePage` the refusal
|
|
117
|
+
* came from, not by the refusal's shape. A pass-through from step 4 describes the CONFIG target
|
|
118
|
+
* (`ST39`, `stamp.test.js` ~2280); one from step 8 describes the page; a source-read refusal may
|
|
119
|
+
* carry no `retained` at all. On `LedgerFailed` the field lives at `cause.retained` and describes
|
|
120
|
+
* whatever the failing call addressed — and where the cause is Scribe-originated, as `ST41`'s
|
|
121
|
+
* `LINEAGE_LINE_TOO_LARGE` is, there is no `cause.retained` to read. The page's own outcome is
|
|
122
|
+
* never in that field either way: it is the `created` beside it, which says the page exists.
|
|
123
|
+
*
|
|
124
|
+
* **`retained` describes the fence call that produced the refusal it sits on; `config_created` says
|
|
125
|
+
* whether this invocation minted the vault config.** A caller repairing after a refusal needs both.
|
|
126
|
+
*
|
|
127
|
+
* ⚠ This field was added before the package's first public contract, so no earlier consumer shape
|
|
128
|
+
* required compatibility.
|
|
129
|
+
* It is on the ok shape AND on every POST-CONFIG Scribe-originated refusal shape for the same
|
|
130
|
+
* reason: a field present only on success answers the question only when the answer is least
|
|
131
|
+
* interesting. (A refusal decided at steps 1-3 carries no field; there is no answer yet.)
|
|
132
|
+
*
|
|
133
|
+
* ⚠⚠ AND THE RULE IS EXACTLY ONE SENTENCE WIDE, so read it before adding a return site. **Every
|
|
134
|
+
* result `writePage` returns after `loadConfig` has run carries `config_created`, set from what
|
|
135
|
+
* THIS invocation's `loadConfig` did — except a fence refusal passed through unchanged under D8,
|
|
136
|
+
* which carries no field.** A refusal decided at steps 1-3, before `loadConfig` runs, carries no
|
|
137
|
+
* field either, for the plainer reason that there is no answer yet.
|
|
138
|
+
*
|
|
139
|
+
* ⚠ ON A FENCE PASS-THROUGH THE CALLER IS SIMPLY NOT TOLD, and the residual is stated rather than
|
|
140
|
+
* argued away: a fence refusal from step 6's create, or from the source loop, CAN follow a config
|
|
141
|
+
* mint at step 4 and will not say so. **The absence of the field is not evidence the config was
|
|
142
|
+
* untouched.** On a fence pass-through the caller is not told whether this invocation minted the
|
|
143
|
+
* config, and **this package offers no observation that attributes it** — a later stat says the
|
|
144
|
+
* file is there, not who put it there, and nothing here records the answer anywhere else. ⚠ THAT
|
|
145
|
+
* CLAUSE READ "no later observation attributes it" UNTIL 2026-09-08, which speaks for observations
|
|
146
|
+
* outside this package that it knows nothing about. Closing the gap properly means a discriminated
|
|
147
|
+
* envelope around the pass-throughs, which is a wider change than this round is scoped to make.
|
|
148
|
+
*/
|
|
149
|
+
export interface ConfigCreated {
|
|
150
|
+
/** True when THIS invocation minted `.wyrd/scribe.json`. */
|
|
151
|
+
readonly config_created: boolean;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* ⚠ THE FAILED-LEDGER OUTCOME CARRIES THE `Created`, AND IT IS A REQUIRED FIELD. The caller's next
|
|
155
|
+
* action depends entirely on knowing the page IS on disk and where — telling them only that
|
|
156
|
+
* something failed leaves them unable to distinguish "nothing happened" from "a page exists with no
|
|
157
|
+
* provenance", which are opposite situations demanding opposite repairs.
|
|
158
|
+
*/
|
|
159
|
+
export interface LedgerFailed extends ScribeRefusal, ConfigCreated {
|
|
160
|
+
readonly reason: 'PAGE_WRITTEN_LEDGER_FAILED';
|
|
161
|
+
readonly created: Created;
|
|
162
|
+
/**
|
|
163
|
+
* The fence or ledger refusal that actually fired, passed through rather than summarised.
|
|
164
|
+
*
|
|
165
|
+
* ⚠⚠ `ScribeRefusal` IS IN THIS UNION BECAUSE ITS ABSENCE WAS THE DEFECT. Every cause reaching
|
|
166
|
+
* here used to be typed `FenceRefusal`, whose `reason` is the fence's closed `FenceReason`
|
|
167
|
+
* union — so the one site that fires on a Scribe-side fault, the post-create line-size
|
|
168
|
+
* re-check, had NOTHING HONEST IT COULD TYPECHECK and returned a fabricated `IO_ERROR`. The
|
|
169
|
+
* caller was told a filesystem error lost their provenance when the truth was a request over a
|
|
170
|
+
* documented bound. **A too-narrow cause type does not prevent the wrong cause; it compels
|
|
171
|
+
* one.** Widening the union is what lets the site name `LINEAGE_LINE_TOO_LARGE`.
|
|
172
|
+
*/
|
|
173
|
+
readonly cause: FenceRefusal | ScribeRefusal;
|
|
174
|
+
}
|
|
175
|
+
export interface Stamped extends ConfigCreated {
|
|
176
|
+
readonly ok: true;
|
|
177
|
+
readonly created: Created;
|
|
178
|
+
readonly record: LineageRecord;
|
|
179
|
+
readonly appended: number;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* A Scribe-originated refusal from the stamp path, carrying the outer summary.
|
|
183
|
+
*
|
|
184
|
+
* ⚠⚠ EVERY SCRIBE-ORIGINATED SHAPE RETURNED AFTER STEP 4 CARRIES THE FIELD, AND THE ONE EXCLUSION
|
|
185
|
+
* IS THE FENCE PASS-THROUGH. The fence's refusals are returned unwrapped under D8 — a second
|
|
186
|
+
* vocabulary for one containment implementation is what D8 exists to prevent — so they carry no
|
|
187
|
+
* `config_created` and a caller reading one is NOT told whether this invocation minted the config.
|
|
188
|
+
* That is the whole of the exclusion, and it is stated on the type at `WritePageResult` below.
|
|
189
|
+
*
|
|
190
|
+
* ⚠⚠ THE SPAN REFUSALS WERE IN THAT EXCLUSION UNTIL 2026-09-08 AND ARE NOT ANY MORE. The retired
|
|
191
|
+
* argument was that `span.ts`'s envelope holds exactly `ok`, `reason` and `where`, that `ST20`
|
|
192
|
+
* asserts that own-key set exactly, and that widening it here would be this module rewriting
|
|
193
|
+
* another module's contract on its way past. What that reasoning missed is WHICH module's contract
|
|
194
|
+
* the returned value belongs to: a `SPAN_*` refusal reaching a `writePage` caller is `writePage`'s
|
|
195
|
+
* result, and the own-key envelope exists to stop UNDOCUMENTED keys reaching that caller rather
|
|
196
|
+
* than to fix a cardinality. `config_created` is documented, on the type, in the same words on
|
|
197
|
+
* every shape that carries it — so the envelope grows by exactly one documented key and `ST20`'s
|
|
198
|
+
* assertion is widened to say so. `span.ts`'s own arms are untouched: the module still returns
|
|
199
|
+
* three keys, and step 5 is what adds the fourth on its way out.
|
|
200
|
+
*
|
|
201
|
+
* ⚠ THE SHAPE-ONLY SPAN FAULTS AT STEP 3 STILL CARRY NOTHING, and that is the ordering rather than
|
|
202
|
+
* an exception: they are decided BEFORE `loadConfig` runs, so there is no answer to report.
|
|
203
|
+
*/
|
|
204
|
+
export interface StampRefusal extends ScribeRefusal, ConfigCreated {
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* A `SPAN_*` refusal from step 5, carrying the outer summary. See `StampRefusal` for why the span
|
|
208
|
+
* envelope grew by one key on 2026-09-08, and `span.ts` for why the module's own returns did not.
|
|
209
|
+
*/
|
|
210
|
+
export interface StampSpanRefusal extends SpanRefusal, ConfigCreated {
|
|
211
|
+
}
|
|
212
|
+
export interface WritePageOptions {
|
|
213
|
+
readonly gate: FsGate;
|
|
214
|
+
readonly appender: LedgerAppender;
|
|
215
|
+
readonly version: string;
|
|
216
|
+
/** Injected so an arm can pin `recorded_at` and the minted vault id. Production uses the defaults. */
|
|
217
|
+
readonly now?: () => Date;
|
|
218
|
+
readonly newUuid?: () => string;
|
|
219
|
+
/**
|
|
220
|
+
* ⚠ A SEPARATE SEAM FROM `newUuid`, WHICH MINTS THE VAULT ID. They are both v4 UUIDs and both
|
|
221
|
+
* default to `randomUUID`, and sharing one injection point would be convenient and wrong: an
|
|
222
|
+
* arm pinning the vault id to a fixed value would pin every `event_id` to the SAME value, which
|
|
223
|
+
* is precisely the collision `event_id` exists to make impossible. A suite that cannot pin one
|
|
224
|
+
* without flattening the other cannot measure either.
|
|
225
|
+
*/
|
|
226
|
+
readonly newEventId?: () => string;
|
|
227
|
+
readonly cache?: SourceCache;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* ⚠⚠ THE ONE RULE THIS UNION EXISTS TO STATE, AND IT IS A RULE ABOUT `config_created`:
|
|
231
|
+
*
|
|
232
|
+
* **Every result `writePage` returns after `loadConfig` has run carries `config_created`, set
|
|
233
|
+
* from what THIS invocation's `loadConfig` did — EXCEPT a fence refusal passed through unchanged
|
|
234
|
+
* under D8, which carries no field at all.**
|
|
235
|
+
*
|
|
236
|
+
* So the union is written to make that sentence checkable rather than merely asserted. `ScribeRefusal`
|
|
237
|
+
* and `SpanRefusal` appear as `PreConfigRefusal`, a shape reachable ONLY from steps 1-3 — before
|
|
238
|
+
* `loadConfig` runs, where there is nothing to report — and every Scribe-originated shape from step
|
|
239
|
+
* 4 down carries the field. `FenceRefusal` is the documented exclusion: it is the ONE POST-CONFIG
|
|
240
|
+
* member that does not carry the field. `PreConfigRefusal` lacks it too, which is the ordering
|
|
241
|
+
* rather than an exclusion — it is decided before there is an answer.
|
|
242
|
+
*
|
|
243
|
+
* ⚠⚠ THE BARE `ScribeRefusal` MEMBER WAS THE DEFECT, NOT THE FIX. Until 2026-09-08 this union
|
|
244
|
+
* listed `ScribeRefusal` and `SpanRefusal` outright, which made the type say "a Scribe refusal may
|
|
245
|
+
* or may not carry the summary" — so a return site that dropped the field TYPECHECKED, and three
|
|
246
|
+
* did. A type that admits both answers cannot catch the wrong one. If a later change makes
|
|
247
|
+
* TypeScript reject a return site here, that site is an instance of the class, not a reason to
|
|
248
|
+
* widen the union back.
|
|
249
|
+
*
|
|
250
|
+
* ⚠⚠ AND THE BRAND IS WHAT MAKES IT BIND, WHICH LISTING THE SHAPES SEPARATELY DID NOT. Measured
|
|
251
|
+
* twice during this round, because the first attempt looked right and was not:
|
|
252
|
+
*
|
|
253
|
+
* · With `PreConfigRefusal` declared as a bare `ScribeRefusal | SpanRefusal`, a post-config site
|
|
254
|
+
* returning the refusal unwrapped still COMPILED. The two shapes are structurally identical,
|
|
255
|
+
* so TypeScript matched the return against the pre-config member and had nothing to say. A
|
|
256
|
+
* union whose members are structurally interchangeable cannot encode WHERE a value came from.
|
|
257
|
+
* · With the brand declared OPTIONAL (`?: never`) it still compiled, for the plainer reason that
|
|
258
|
+
* an optional property is satisfied by its absence. An optional brand brands nothing.
|
|
259
|
+
*
|
|
260
|
+
* So the brand is REQUIRED. `PRE_CONFIG` is a `unique symbol` declared and never defined, so the
|
|
261
|
+
* property exists only in the type system — no runtime value carries it, nothing can read it, and
|
|
262
|
+
* it costs no bytes. What it buys is that an unbranded bare refusal returned from step 4 down
|
|
263
|
+
* matches NO member of this union and is REJECTED, which is the whole point of writing the rule as
|
|
264
|
+
* a type instead of as a sentence.
|
|
265
|
+
*/
|
|
266
|
+
declare const PRE_CONFIG: unique symbol;
|
|
267
|
+
/**
|
|
268
|
+
* A refusal decided at steps 1-3, before `loadConfig` has run. It carries no `config_created` for
|
|
269
|
+
* the plain reason that there is no answer yet, and the brand is what keeps it from standing in for
|
|
270
|
+
* the post-config shapes — see the note above for the two measurements that forced it.
|
|
271
|
+
*/
|
|
272
|
+
export type PreConfigRefusal = (ScribeRefusal | SpanRefusal) & {
|
|
273
|
+
readonly [PRE_CONFIG]: never;
|
|
274
|
+
};
|
|
275
|
+
export type WritePageResult = Stamped | StampRefusal | StampSpanRefusal | PreConfigRefusal | LedgerFailed | FenceRefusal;
|
|
276
|
+
/**
|
|
277
|
+
* ⚠⚠ THE OUTER HALF IS THE **ONLY** SCOPE IN WHICH THIS MODULE HOLDS THE CALLER'S REQUEST OBJECT,
|
|
278
|
+
* and that is the containment rather than a tidy split. (The caller's own option hooks may close
|
|
279
|
+
* over that object and run downstream; they are the caller's, and outside what the split
|
|
280
|
+
* contains.) Steps 4-9 live in `stampValidated` below, which takes a
|
|
281
|
+
* `Validated` snapshot and has NO `request` parameter — so a downstream read of the caller's object
|
|
282
|
+
* is not a discipline anyone has to remember, it is `TS2304: Cannot find name 'request'`. The same
|
|
283
|
+
* argument the `preConfigRefuse` local makes about building a pre-config refusal: a name out of
|
|
284
|
+
* scope is a compile error.
|
|
285
|
+
*
|
|
286
|
+
* ⚠⚠ THE DISCRIMINATION IS `instanceof`, NOT `ok === false`, AND THE OLD TEST WAS A LIVE DEFECT.
|
|
287
|
+
* `isRefusalLike(checked)` read `.ok` through the prototype chain, so a request whose PROTOTYPE
|
|
288
|
+
* carried `{ ok: false }` came back from `preflight` validated, was read HERE as a refusal, and was
|
|
289
|
+
* returned to the caller as one — a refusal the caller minted, wearing this module's name. Asking
|
|
290
|
+
* `checked instanceof Validated` puts the answer on a prototype this module owns and never exports,
|
|
291
|
+
* which a caller cannot supply through inheritance or any other means. `isRefusalLike` stays where
|
|
292
|
+
* it belongs, guarding the fence and Scribe results below — values returned by the `gate` and the
|
|
293
|
+
* `appender`, which are injected dependencies. Reading `.ok` off those is trust in the dependency
|
|
294
|
+
* the caller chose to inject, not a defence against the request; it was never meant as one.
|
|
295
|
+
*/
|
|
296
|
+
export declare function writePage(request: WritePageRequest, options: WritePageOptions): Promise<WritePageResult>;
|
|
297
|
+
export {};
|