@lapxo/bound 0.1.2 → 0.1.4

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.
Files changed (52) hide show
  1. package/README.md +5 -5
  2. package/TARGET.bound +27 -20
  3. package/dist/cli/accept.js +112 -71
  4. package/dist/cli/owner.js +3 -0
  5. package/dist/cli/verb.js +54 -12
  6. package/dist/fold/configures.js +3 -1
  7. package/dist/fold/digests.js +7 -1
  8. package/dist/fold/kept.js +5 -1
  9. package/dist/fold/observed.d.ts +1 -0
  10. package/dist/fold/observed.js +30 -11
  11. package/dist/fold/readers.d.ts +2 -0
  12. package/dist/fold/readers.js +3 -3
  13. package/dist/fold/region-declarations.d.ts +2 -0
  14. package/dist/fold/region-declarations.js +13 -0
  15. package/dist/fold/resolved.js +9 -1
  16. package/dist/fold/second.js +3 -0
  17. package/dist/host/adapters/transport/http.d.ts +5 -0
  18. package/dist/host/adapters/transport/http.js +28 -0
  19. package/dist/host/capsule-process.d.ts +2 -1
  20. package/dist/host/capsule-process.js +4 -3
  21. package/dist/host/capsule.d.ts +1 -0
  22. package/dist/host/capsule.js +11 -7
  23. package/dist/host/cell-receipts.js +1 -1
  24. package/dist/host/content-needed.d.ts +5 -0
  25. package/dist/host/content-needed.js +8 -0
  26. package/dist/host/content-store.d.ts +8 -0
  27. package/dist/host/content-store.js +31 -0
  28. package/dist/host/lifetime.d.ts +1 -0
  29. package/dist/host/lifetime.js +40 -0
  30. package/dist/host/objects.js +1 -1
  31. package/dist/host/ports/transport.d.ts +1 -0
  32. package/dist/host/ports/transport.js +48 -0
  33. package/dist/host/process-lifetime.d.ts +11 -0
  34. package/dist/host/process-lifetime.js +12 -0
  35. package/dist/host/read-only.d.ts +3 -0
  36. package/dist/host/read-only.js +9 -0
  37. package/dist/host/release.js +2 -1
  38. package/dist/host/selected-capsules.js +38 -12
  39. package/dist/host/selected-topos.d.ts +5 -5
  40. package/dist/host/selected-topos.js +9 -36
  41. package/dist/host/source-content.d.ts +5 -3
  42. package/dist/host/source-content.js +36 -30
  43. package/dist/land/leaving.js +4 -0
  44. package/dist/land/ledger.js +3 -0
  45. package/dist/observe/identity.d.ts +2 -0
  46. package/dist/observe/identity.js +3 -3
  47. package/dist/observe/regions.js +2 -2
  48. package/dist/observe/run.d.ts +2 -1
  49. package/dist/observe/run.js +10 -4
  50. package/dist/render/receipts.js +2 -1
  51. package/package.json +2 -2
  52. package/receipts.bound +73 -72
@@ -1,3 +1,4 @@
1
+ import { withoutEffects } from "../host/read-only.js";
1
2
  import { selectedWireAt } from "../host/selected-topos.js";
2
3
  import { timing } from "../host/timing.js";
3
4
  import { admittedObjects } from "../host/objects.js";
@@ -22,7 +23,7 @@ import { ownLock, ownRoot, ownStore } from "../observe/runner.js";
22
23
  import { widenedWithoutWitness } from "../judge/widening.js";
23
24
  import { retractedTogether, withdrawal } from "../land/withdraw.js";
24
25
  import { isCeiling } from "../fold/configures.js";
25
- import { inertIn, rankedIn } from "../land/inert.js";
26
+ import { inertIn } from "../land/inert.js";
26
27
  import { landSaid } from "./pass.js";
27
28
  import { underTheLock, underTheRegions } from "../land/act.js";
28
29
  import { openedBy } from "../fold/opened.js";
@@ -39,6 +40,68 @@ import { ownerOf, epochOf, forksItself, forksJoinedBy, renderInto, sideOf, signe
39
40
  import { claimLinesIn, entryOf, namedPlaces, placesOfFile, positionalsOf, valueOf } from "./args.js";
40
41
  import { FLAGS } from "./names.js";
41
42
  import { judgeDemand, landTake } from "./takes.js";
43
+ /** Admission reads an existing evidence snapshot; acquiring observations belongs to fold. */
44
+ function admissionEvidence(root, signed, standing) {
45
+ return withoutEffects(() => {
46
+ const store = storeOf(root);
47
+ const reads = !signed.some(isReaderLock) && signed.some(isCeiling);
48
+ const fold = reads ? coneOf(root, entryOf(root), signed.filter(isCeiling)) : { standing, observed: [], own: {}, kept: true };
49
+ const inert = inertIn(signed, { standing: fold.standing, ceiling: isCeiling, signsReader: !reads, observed: fold.observed, own: fold.own, epoch: epochOf([...lockLines(store), ...signed]) });
50
+ if (inert.length)
51
+ throw Error('REFUSE·inert no existing reading meets these ceilings; fold the declared inputs first');
52
+ const coordinates = coordinatesWithdrawn(root, store, signed, [...signed, ...standing]);
53
+ if (coordinates.refused.length)
54
+ throw Error(`REFUSE·withdraw a file is still read: ${coordinates.refused.map(one => `${one.path} by ${one.by}`).join(' · ')}`);
55
+ const view = `${wordOf(standing, 'families', 'view')}/`;
56
+ const judged = signed.filter(one => isConfig(one) && fieldOf(one, 'role') === 'demands' && fieldOf(one, 'value') !== 'withdraw' && !fieldOf(one, 'scope').startsWith(view)).map(one => judgeDemand(root, one));
57
+ const refused = judged.filter(one => one.kind === 'refused' || (one.kind === 'judged' && one.verdict.kind === 'refuse'));
58
+ if (refused.length)
59
+ throw Error(`REFUSE·takes ${refused.map(one => `${one.scope}: ${one.kind === 'judged' ? one.verdict.why : one.kind === 'refused' ? one.why : ''}`).join(' · ')}`);
60
+ return { coordinates, judged, fold };
61
+ });
62
+ }
63
+ /** One witness rule for prospective admission and the locked act. */
64
+ function refusesWidening(signed, standing) {
65
+ const widened = widenedWithoutWitness(signed, standing, isCeiling, wordOf(standing, 'at-classes', 'witness'));
66
+ if (widened.length) {
67
+ for (const line of widened)
68
+ process.stderr.write(`WIDER ${fieldOf(line, 'scope')} · ${fieldOf(line, 'value')}\n`);
69
+ process.stderr.write(`${selfName()}: REFUSE·widening nothing landed · a ceiling that admits more than it did gives back freedom the lock had spent: sign it under a witness that names what made the old ceiling wrong\n`);
70
+ return true;
71
+ }
72
+ return false;
73
+ }
74
+ /** Admission-only checks; observation and mutation belong to the later act. */
75
+ function refusesAdmission(root, signed, standing) {
76
+ if (refusesWidening(signed, standing))
77
+ return true;
78
+ const store = storeOf(root);
79
+ const outside = outsideTheirCapsule(root, standing, signed);
80
+ for (const line of outside)
81
+ process.stderr.write(`OUTSIDE ${fieldOf(line, 'scope')} · ${fieldOf(line, 'value')}\n`);
82
+ if (outside.length) {
83
+ process.stderr.write(`${selfName()}: REFUSE·outside nothing landed · a reader or a capsule lands in the capsule of its world, never in the wire or the instrument\n`);
84
+ return true;
85
+ }
86
+ const forks = forksJoinedBy(store, signed).filter(fork => forksItself(fork.lines) || fork.lines.filter(line => signed.includes(line)).length > 1);
87
+ if (forks.length) {
88
+ for (const fork of forks) {
89
+ process.stderr.write(`FORK ${fork.key}\n`);
90
+ for (const line of fork.lines)
91
+ process.stderr.write(` ${signed.includes(line) ? 'new ' : 'held'} by=${fieldOf(line, 'by')} ${sideOf(line, lockLines(store))}\n`);
92
+ }
93
+ process.stderr.write(`${selfName()}: REFUSE·fork nothing landed · one signer never forks itself, and one act never forks itself: withdraw the held side in this batch, or sign the other reading under its own scope\n`);
94
+ return true;
95
+ }
96
+ const held = aliasPinned(root, signed);
97
+ if (held.length) {
98
+ for (const one of held)
99
+ process.stderr.write(`ALIAS ${one.scope} · ${one.digest} · ${one.by.join(' ')}\n`);
100
+ process.stderr.write(`${selfName()}: REFUSE·alias nothing landed · an alias is withdrawn only when no pin names its digest\n`);
101
+ return true;
102
+ }
103
+ return false;
104
+ }
42
105
  /** A batch signed as accept signs it: at the next epoch, by the key it names, each line read by the wire it would land under. */
43
106
  export async function signedBatch(root, args, files) {
44
107
  const byFile = files.map((file) => [placesOfFile(file), claimLinesIn(file)]);
@@ -57,10 +120,16 @@ export async function signedBatch(root, args, files) {
57
120
  }
58
121
  const store = storeOf(root);
59
122
  const destinations = [...new Set([...namedPlaces(args), ...byFile.flatMap(([heads]) => heads)])];
123
+ const rooted = lockStanding(store);
124
+ const stray = destinations.filter(one => !placesIn(root).includes(one) && !foundableIn(root, rooted, wordOf(rooted, 'families', 'region'), one));
125
+ if (stray.length) {
126
+ process.stderr.write(`${selfName()}: REFUSE·place nothing landed · ${stray.join(', ')} is no place of the tree, and no line of the root founds it: --place names the own lock a line lands in\n`);
127
+ return { kind: 'exit', code: 2 };
128
+ }
60
129
  const epoch = epochOf([...lockLines(store), ...destinations.flatMap((place) => appliedIn(root, place))]) + 1;
61
130
  const stale = proposed.filter((line) => fieldOf(line, 'sig')).flatMap((line) => ((by, key) => (key === undefined || !verifiesOwnerLine(line, key, signaturesOf(store).admitted)
62
131
  ? [`${fieldOf(line, 'scope')} · the signature does not hold for ${by || '∅'}`]
63
- : isConfig(line) && Number(fieldOf(line, 'epoch')) !== epoch ? [`${fieldOf(line, 'scope')} · signed by ${by} at epoch ${fieldOf(line, 'epoch')}, and this act lands at ${epoch}: sign it again`] : []))(fieldOf(line, 'by'), publicKeyOf(root, fieldOf(line, 'by'))));
132
+ : []))(fieldOf(line, 'by'), publicKeyOf(root, fieldOf(line, 'by'))));
64
133
  if (stale.length) {
65
134
  for (const one of stale)
66
135
  process.stderr.write(`SIGNED ${one}\n`);
@@ -85,6 +154,13 @@ export async function signedBatch(root, args, files) {
85
154
  }
86
155
  return { kind: 'once', proposed };
87
156
  }
157
+ const outdated = proposed.filter(line => fieldOf(line, 'sig') && isConfig(line) && Number(fieldOf(line, 'epoch')) !== epoch);
158
+ if (outdated.length) {
159
+ for (const line of outdated)
160
+ process.stderr.write(`SIGNED ${fieldOf(line, 'scope')} · signed by ${fieldOf(line, 'by')} at epoch ${fieldOf(line, 'epoch')}, and this act lands at ${epoch}: sign it again\n`);
161
+ process.stderr.write(`${selfName()}: REFUSE·signed nothing landed · ${outdated.length} lines a lane signed stand at no epoch this act\n`);
162
+ return { kind: 'exit', code: 1 };
163
+ }
88
164
  const keyFile = valueOf(args, FLAGS.keyFile);
89
165
  const keyId = valueOf(args, FLAGS.key) ?? ownerOf(store);
90
166
  const publicKey = drafted.length ? publicKeyOf(root, keyId) : undefined;
@@ -121,6 +197,15 @@ export async function signedBatch(root, args, files) {
121
197
  return { kind: 'exit', code: 1 };
122
198
  }
123
199
  }
200
+ const taken = retractedTogether(signed);
201
+ if (taken.length) {
202
+ for (const pair of taken) {
203
+ process.stderr.write(`WITHDRAW ${sayingOf(pair.withdraw)}\n`);
204
+ process.stderr.write(`NEW ${sayingOf(pair.line)}\n`);
205
+ }
206
+ process.stderr.write(`${selfName()}: REFUSE·withdraw nothing landed · a withdraw matches a new line in this batch, so it takes that line too\n`);
207
+ return { kind: 'exit', code: 1 };
208
+ }
124
209
  const authority = authorityFor([...lockLines(store), ...signed], rootSigner(root), signaturesOf(store).admitted);
125
210
  const ownership = ownedOf(root, signed, line => authority.of(line).kind === 'admitted', by => authority.admitted.find(one => one.id === by)?.coverage ?? [], named, line => { const heads = byFile.flatMap(([heads, lines]) => lines.some(draft => sayingOf(draft) === sayingOf(line)) ? heads : []); return heads.length ? heads : undefined; });
126
211
  if (ownership.nowhere.length) {
@@ -128,6 +213,14 @@ export async function signedBatch(root, args, files) {
128
213
  process.stderr.write(`${selfName()}: REFUSE·signer ${keyId} uncovered ${fieldOf(line, 'scope')} · nothing signed for delivery\n`);
129
214
  return { kind: 'exit', code: 1 };
130
215
  }
216
+ if (ownership.forked.length) {
217
+ for (const one of ownership.forked)
218
+ process.stderr.write(`FORKED ${one.place}/${LOCK} · ${one.key} · the lot would leave two standing claims where the lock had one\n`);
219
+ process.stderr.write(`${selfName()}: REFUSE·forked nothing landed · ${ownership.forked.length} claims an own lot would fork: a lot for many places lands the same lines at each, and lines meant for one place go in a lot of their own\n`);
220
+ return { kind: 'exit', code: 1 };
221
+ }
222
+ if (refusesAdmission(root, ownership.ledger, lockStanding(store)))
223
+ return { kind: 'exit', code: 1 };
131
224
  if (signed.some(line => !isConfig(line))) {
132
225
  try {
133
226
  await admittedObjects(root, [...signedOwnerLines(store), ...signed], text => process.stdout.write(text + '\n'));
@@ -137,6 +230,13 @@ export async function signedBatch(root, args, files) {
137
230
  return { kind: 'exit', code: 1 };
138
231
  }
139
232
  }
233
+ try {
234
+ admissionEvidence(root, ownership.ledger, lockStanding(store));
235
+ }
236
+ catch (error) {
237
+ process.stderr.write(`${selfName()}: ${error instanceof Error ? error.message : String(error)} · nothing landed\n`);
238
+ return { kind: 'exit', code: 1 };
239
+ }
140
240
  return { kind: 'signed', signed, epoch, ...(drafted.length ? { keyId } : {}) };
141
241
  }
142
242
  /**
@@ -151,26 +251,11 @@ export async function accept(root, args) {
151
251
  const started = Date.now();
152
252
  const named = namedPlaces(args);
153
253
  const placed = positionalsOf(args).reduce((all, file) => ((places) => (places.length ? claimLinesIn(file).reduce((got, line) => got.set(sayingOf(line), [...new Set([...(got.get(sayingOf(line)) ?? []), ...places])]), all) : all))(placesOfFile(file)), new Map());
154
- const rooted = lockStanding(storeOf(root));
155
- const stray = [...new Set([...named, ...[...placed.values()].flat()])].filter((one) => !placesIn(root).includes(one) && !foundableIn(root, rooted, wordOf(rooted, 'families', 'region'), one));
156
- if (stray.length) {
157
- process.stderr.write(`${selfName()}: REFUSE·place nothing landed · ${stray.join(', ')} is no place of the tree, and no line of the root founds it: --place names the own lock a line lands in\n`);
158
- return 2;
159
- }
160
254
  const batch = await signedBatch(root, args, positionalsOf(args));
161
255
  if (batch.kind === 'exit')
162
256
  return batch.code;
163
257
  if (batch.kind === 'once')
164
258
  return underTheLock(storeOf(root), 'land', () => repaid(root, batch.proposed, started));
165
- const taken = retractedTogether(batch.signed);
166
- if (taken.length) {
167
- for (const pair of taken) {
168
- process.stderr.write(`WITHDRAW ${sayingOf(pair.withdraw)}\n`);
169
- process.stderr.write(`NEW ${sayingOf(pair.line)}\n`);
170
- }
171
- process.stderr.write(`${selfName()}: REFUSE·withdraw nothing landed · a withdraw matches a new line in this batch, so it takes that line too\n`);
172
- return 1;
173
- }
174
259
  const store = storeOf(root);
175
260
  const authority = authorityFor([...lockLines(store), ...batch.signed], rootSigner(root), signaturesOf(store).admitted);
176
261
  const owned = ownedOf(root, batch.signed, (line) => authority.of(line).kind === 'admitted', (by) => authority.admitted.find((one) => one.id === by)?.coverage ?? [], named, (line) => placed.get(sayingOf(line)));
@@ -180,12 +265,6 @@ export async function accept(root, args) {
180
265
  process.stderr.write(`${selfName()}: REFUSE·nowhere nothing landed · ${owned.nowhere.length} lines neither the ledger nor an own lock admits: a line is landed signed where it stands, never written in bare\n`);
181
266
  return 1;
182
267
  }
183
- if (owned.forked.length) {
184
- for (const one of owned.forked)
185
- process.stderr.write(`FORKED ${one.place}/${LOCK} · ${one.key} · the lot would leave two standing claims where the lock had one\n`);
186
- process.stderr.write(`${selfName()}: REFUSE·forked nothing landed · ${owned.forked.length} claims an own lot would fork: a lot for many places lands the same lines at each, and lines meant for one place go in a lot of their own\n`);
187
- return 1;
188
- }
189
268
  const places = placesIn(root);
190
269
  const pinned = [...new Set([...owned.own.keys(), ...named])];
191
270
  const touched = pinned.length === 0 && owned.ledger.length ? touchedBy(root, places, owned.ledger, lockStanding(store)) : [];
@@ -213,42 +292,18 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
213
292
  return 1;
214
293
  }
215
294
  }
216
- const reads = !signed.some(isReaderLock) && signed.some(isCeiling);
217
- const fold = reads ? took('cone', () => coneOf(root, entryOf(root), signed.filter(isCeiling), descent)) : { ...took('lines', () => standingOf(root)), observed: [], own: {}, kept: true };
218
- const inert = inertIn(signed, { standing: fold.standing, ceiling: isCeiling, signsReader: !reads, observed: fold.observed, own: fold.own, epoch });
219
- const widened = widenedWithoutWitness(signed, fold.standing, isCeiling, wordOf(fold.standing, 'at-classes', 'witness'));
220
- if (widened.length) {
221
- for (const line of widened)
222
- process.stderr.write(`WIDER ${fieldOf(line, 'scope')} · ${fieldOf(line, 'value')}\n`);
223
- process.stderr.write(`${selfName()}: REFUSE·widening nothing landed · a ceiling that admits more than it did gives back freedom the lock had spent: sign it under a witness that names what made the old ceiling wrong\n`);
224
- return 1;
225
- }
226
- if (inert.length) {
227
- for (const line of inert)
228
- process.stderr.write(`INERT ${fieldOf(line, 'scope')} measure=${fieldOf(line, 'measure')}\n`);
229
- process.stderr.write(`${selfName()}: REFUSE·inert nothing landed · no reading meets these ceilings: sign the reader that meets them in this batch, or leave them out\n`);
295
+ const snapshot = took('lines', () => standingOf(root));
296
+ if (refusesAdmission(root, signed, snapshot.standing))
230
297
  return 1;
298
+ let evidence;
299
+ try {
300
+ evidence = took('evidence', () => admissionEvidence(root, signed, snapshot.standing));
231
301
  }
232
- const outside = outsideTheirCapsule(root, fold.standing, signed);
233
- for (const line of outside)
234
- process.stderr.write(`OUTSIDE ${fieldOf(line, 'scope')} · ${fieldOf(line, 'value')}\n`);
235
- if (outside.length) {
236
- process.stderr.write(`${selfName()}: REFUSE·outside nothing landed · a reader or a capsule lands in the capsule of its world, never in the wire or the instrument\n`);
302
+ catch (error) {
303
+ process.stderr.write(`${selfName()}: ${error instanceof Error ? error.message : String(error)} · nothing landed\n`);
237
304
  return 1;
238
305
  }
239
- const forks = took('fork', () => forksJoinedBy(store, signed).filter((fork) => forksItself(fork.lines) || fork.lines.filter((line) => signed.includes(line)).length > 1));
240
- if (forks.length) {
241
- for (const fork of forks) {
242
- process.stderr.write(`FORK ${fork.key}\n`);
243
- for (const line of fork.lines)
244
- process.stderr.write(` ${signed.includes(line) ? 'new ' : 'held'} by=${fieldOf(line, 'by')} ${sideOf(line, lockLines(store))}\n`);
245
- }
246
- process.stderr.write(`${selfName()}: REFUSE·fork nothing landed · one signer never forks itself, and one act never forks itself: withdraw the held side in this batch, or sign the other reading under its own scope\n`);
247
- return 1;
248
- }
249
- if (reads)
250
- for (const line of rankedIn(signed.filter(isCeiling), fold))
251
- process.stdout.write(`${line}\n`);
306
+ const fold = evidence.fold;
252
307
  const authority = took('authority', () => authorityFor([...lockLines(store), ...signed], rootSigner(root), signaturesOf(store).admitted));
253
308
  for (const line of signed) {
254
309
  const verdict = authority.of(line);
@@ -257,14 +312,8 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
257
312
  return 1;
258
313
  }
259
314
  }
260
- const coordinates = coordinatesWithdrawn(root, store, signed, [...signed, ...fold.standing]);
261
- if (coordinates.refused.length) {
262
- for (const one of coordinates.refused)
263
- process.stderr.write(`RUN ${one.path} · still read by ${one.by}\n`);
264
- process.stderr.write(`${selfName()}: REFUSE·withdraw nothing landed · a file a block still reads cannot leave: withdraw what reads it first, or keep it\n`);
265
- return 1;
266
- }
267
315
  const withdraws = signed.filter((line) => fieldOf(line, 'value') === 'withdraw');
316
+ const coordinates = evidence.coordinates;
268
317
  const withdrawn = withdraws.length ? withdrawal(withdraws, lockLines(store)) : null;
269
318
  const view = `${wordOf(fold.standing, 'families', 'view')}/`;
270
319
  const lot = fullDigest(store, signed);
@@ -272,8 +321,7 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
272
321
  ? fullDigest(store, [...vouchedCoordinates(store)].sort(([a], [b]) => a.localeCompare(b)).map(([coordinate, digest]) => `${coordinate} ${digest}`))
273
322
  : 'key' in fold && typeof fold.key === 'string' ? fold.key : fullDigest(store, fold.standing);
274
323
  const cached = keptTakes(keptVerdict(store, tree, lot));
275
- const judged = cached ?? took('takes', () => signed.filter((one) => fieldOf(one, 'role') === 'demands' && fieldOf(one, 'value') !== 'withdraw' && !fieldOf(one, 'scope').startsWith(view))
276
- .map((line) => judgeDemand(root, line)));
324
+ const judged = cached ?? evidence.judged;
277
325
  if (!cached)
278
326
  keepVerdict(store, tree, lot, judged.map(packTake));
279
327
  const refused = judged.filter((one) => one.kind === 'refused' || (one.kind === 'judged' && one.verdict.kind === 'refuse'));
@@ -283,13 +331,6 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
283
331
  process.stderr.write(`${selfName()}: REFUSE·takes nothing landed · ${refused.length} takes refused: a batch lands whole or not at all\n`);
284
332
  return 1;
285
333
  }
286
- const held = aliasPinned(root, signed);
287
- if (held.length) {
288
- for (const one of held)
289
- process.stderr.write(`ALIAS ${one.scope} · ${one.digest} · ${one.by.join(' ')}\n`);
290
- process.stderr.write(`${selfName()}: REFUSE·alias nothing landed · an alias is withdrawn only when no pin names its digest\n`);
291
- return 1;
292
- }
293
334
  const told = took('told', () => standingOf(root, undefined, signed));
294
335
  for (const name of openedBy(descent, told.standing, signed))
295
336
  process.stderr.write(`OPEN ${name}\n`);
package/dist/cli/owner.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { readOnly } from "../host/read-only.js";
1
2
  import { keyFor, lockLines, releaseOf, signedLockLines } from "../fold/keys.js";
2
3
  import { existsSync, renameSync, writeFileSync } from 'node:fs';
3
4
  import { join, resolve } from 'node:path';
@@ -83,6 +84,8 @@ export function renderedLock(root, store, told = []) {
83
84
  return readRelease(released);
84
85
  const owned = [...lockLines(store), ...told];
85
86
  const render = () => renderTarget(owned, undefined, authorityFor(owned, rootSigner(root), signaturesOf(store, told).admitted).of, forkReceipts(store, owned));
87
+ if (readOnly())
88
+ return render();
86
89
  if (told.length) {
87
90
  const byLock = rendered.get(lockLines(store)) ?? rendered.set(lockLines(store), new Map()).get(lockLines(store));
88
91
  return byLock.get(told) ?? byLock.set(told, render()).get(told);
package/dist/cli/verb.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { resolveSources } from "../host/source-content.js";
2
+ import { withContentStore } from "../host/content-store.js";
3
+ import { withoutEffects } from "../host/read-only.js";
4
+ import { resolveSources, resolveRequested } from "../host/source-content.js";
3
5
  import { cellsWithReceipts } from "../host/cell-receipts.js";
6
+ import { renderCells } from '@lapxo/topos/cells-view';
4
7
  import { releasePolicyLines } from "../fold/release-policy.js";
5
8
  import { admittedObjects } from "../host/objects.js";
6
9
  import { objectFold } from "../fold/object.js";
@@ -15,7 +18,7 @@ import { eraOf, signaturesOf } from "../fold/digests.js";
15
18
  import { fullDigest as bootstrapDigest } from "../host/digest.js";
16
19
  import { canonical } from '@lapxo/topos/wire';
17
20
  import { EXTENSION, LOCK, fieldOf, foldClaims, isWire, selfName } from "../fold/claims.js";
18
- import { observedClaims } from "../fold/observed.js";
21
+ import { observedClaims, readerRefusals } from "../fold/observed.js";
19
22
  import { rolesOf } from "../fold/roles.js";
20
23
  import { authorityFor, releaseSigner, rootSigner, writerFor } from "../fold/signers.js";
21
24
  import { land, ledgerLines, storeOf } from "../land/ledger.js";
@@ -189,10 +192,31 @@ async function view(root, under, asked, told = [], coord, key, includePolicy = t
189
192
  const rest = told.filter((line) => !ledger.includes(line));
190
193
  if (place && rest.length)
191
194
  tellOwn(place, rest);
195
+ if (name === NAMED_VIEWS.lines && (told.length || depth === undefined)) {
196
+ const text = standingOf(root, under, ledger).standing.filter(isWire).join('\n');
197
+ process.stdout.write(text ? `${text}\n` : '');
198
+ return EXIT.closed;
199
+ }
200
+ if (told.length) {
201
+ const selected = viewsOf(standingOf(root, under, ledger).standing).get(name);
202
+ if (selected && !selected.shape && selected.regions.some(region => region.name === 'cells')) {
203
+ const history = [...signedOwnerLines(storeOf(root)), ...ledger];
204
+ const held = await admittedObjects(root, history);
205
+ process.stdout.write(`TOLD ${told.length} lines folded as if landed · nothing written\n`);
206
+ for (const line of renderCells(objectFold(held.records, held.context)))
207
+ process.stdout.write(line + '\n');
208
+ return EXIT.closed;
209
+ }
210
+ }
192
211
  const own = place ? rest.filter((line) => admittedIn(root, place, line)) : [];
193
212
  const fold = told.length
194
- ? foldPlace(root, entryOf(root), under, false, { standing: standingOf(root, under, ledger), observed: observedClaims(root, storeOf(root), { wait: false }) })
195
- : foldPlace(root, entryOf(root), under, false);
213
+ ? foldPlace(root, entryOf(root), under, false, { standing: standingOf(root, under, ledger), observed: observedClaims(root, storeOf(root), { wait: false }), free: true })
214
+ : foldPlace(root, entryOf(root), under, true);
215
+ if (!told.length && readerRefusals().length) {
216
+ for (const why of readerRefusals())
217
+ process.stderr.write(`${selfName()}: ${why}\n`);
218
+ return EXIT.refuse;
219
+ }
196
220
  if (coord)
197
221
  askWhy(fold, coord);
198
222
  if (name === NAMED_VIEWS.lines) {
@@ -200,6 +224,9 @@ async function view(root, under, asked, told = [], coord, key, includePolicy = t
200
224
  beat(root, fold, Date.now() - at);
201
225
  const text = fold.standing.filter(isWire).join('\n');
202
226
  process.stdout.write(text ? `${text}\n` : '');
227
+ if (depth === '1')
228
+ for (const line of readingsIn([...fold.standing.filter(isCeiling), ...ownCeilings(under)], fold))
229
+ process.stdout.write(`${line}\n`);
203
230
  return EXIT.closed;
204
231
  }
205
232
  const views = viewsOf(fold.standing);
@@ -371,7 +398,7 @@ function foldFile(at) {
371
398
  process.stdout.write(`FACT ${folded.standing.length} standing · ${folded.forks.length} forks · ${basename(at)}\n`);
372
399
  return EXIT.closed;
373
400
  }
374
- function foldVerb(_root, rest) {
401
+ function foldVerb(root, rest) {
375
402
  const files = positionalsOf(rest).filter((one) => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory() && one.endsWith(EXTENSION));
376
403
  const places = positionalsOf(rest).filter((one) => existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory());
377
404
  if (files.length === 1 && !places.length && valueOf(rest, FLAGS.as) === undefined) {
@@ -381,16 +408,27 @@ function foldVerb(_root, rest) {
381
408
  return foldAt(dirname(at), undefined, rest);
382
409
  }
383
410
  const under = places[0];
384
- const tree = files.length === 1 ? dirname(resolve(files[0])) : resolve('.');
411
+ // A prospective lot is data, not the place whose authority admits it.
412
+ const tree = files.length ? root : resolve('.');
385
413
  const place = under ? relative(tree, resolve(under)).split(sep).join('/') : '';
386
414
  return foldAt(tree, place ? `${place}/` : undefined, rest);
387
415
  }
388
416
  async function foldAt(root, under, argv) {
417
+ return withContentStore(storeOf(root), () => foldRequested(root, under, argv), () => standingOf(root, under).standing);
418
+ }
419
+ async function foldRequested(root, under, argv) {
420
+ const as = valueOf(argv, FLAGS.as);
421
+ const prospective = as !== undefined && positionalsOf(argv).some(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
422
+ if (as === NAMED_VIEWS.help || prospective || argv.includes(FLAGS.check))
423
+ return foldResolved(root, under, argv);
424
+ return resolveRequested(digest => resolveSources(standingOf(root, under).standing, [digest]), () => foldResolved(root, under, argv));
425
+ }
426
+ async function foldResolved(root, under, argv) {
389
427
  const as = valueOf(argv, FLAGS.as);
428
+ const inputs = positionalsOf(argv).filter(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
390
429
  // Public content is authenticated by its digest, independently of owner history.
391
430
  // A named view does not request mounting every release of the instrument.
392
- if (as !== NAMED_VIEWS.help && lockLines(storeOf(root)).length)
393
- await resolveSources(standingOf(root, under).standing, as === undefined);
431
+ const receiptCheck = as === undefined && argv.includes(FLAGS.check);
394
432
  if (as === OBJECT_VIEW) {
395
433
  const inputs = positionalsOf(argv).filter(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
396
434
  if (inputs.length > 1)
@@ -406,6 +444,8 @@ async function foldAt(root, under, argv) {
406
444
  const key = valueOf(argv, FLAGS.key);
407
445
  if (as === undefined) {
408
446
  const exit = await pass(root, entryOf(root), under, argv.includes(FLAGS.check));
447
+ if (receiptCheck)
448
+ return exit;
409
449
  if (exit !== 0)
410
450
  return exit;
411
451
  const history = signedOwnerLines(storeOf(root));
@@ -421,7 +461,7 @@ async function foldAt(root, under, argv) {
421
461
  }
422
462
  return exit;
423
463
  }
424
- const named = positionalsOf(argv).filter((one) => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
464
+ const named = inputs;
425
465
  const coord = positionalsOf(argv).find((one) => one !== under?.replace(/\/$/, '') && !one.startsWith('-') && !(existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory()));
426
466
  if ((as === 'why' || as === NAMED_VIEWS.because || as.split('@')[0] === 'why') && coord !== undefined)
427
467
  return view(root, under, as === NAMED_VIEWS.because ? 'why' : as, [], coord, key);
@@ -432,7 +472,7 @@ async function foldAt(root, under, argv) {
432
472
  return batch.code;
433
473
  if (batch.kind === 'once')
434
474
  process.stdout.write(`ONCE ${batch.proposed.length} lines · every one already landed: the view folds the tree as it stands\n`);
435
- return view(root, under, as, batch.kind === 'signed' ? batch.signed : [], undefined, key);
475
+ return withoutEffects(() => view(root, under, as, batch.kind === 'signed' ? batch.signed : batch.proposed, undefined, key));
436
476
  }
437
477
  const VERBS = {
438
478
  fold: (root, rest) => foldVerb(root, rest),
@@ -459,8 +499,10 @@ function main(argv) {
459
499
  const verb = argv[0] ?? '';
460
500
  const run = verb in VERBS ? VERBS[verb] : undefined;
461
501
  const places = positionalsOf(run === undefined ? argv : argv.slice(1)).filter((one) => existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory());
462
- if (run !== undefined)
463
- return run(resolve(places[0] ?? '.'), argv.slice(1), argv);
502
+ if (run !== undefined) {
503
+ const root = resolve(places[0] ?? '.');
504
+ return withContentStore(storeOf(root), () => run(root, argv.slice(1), argv));
505
+ }
464
506
  const under = argv[0] ?? '';
465
507
  if (under && !under.startsWith('-') && places.includes(under)) {
466
508
  const root = resolve('.');
@@ -4,6 +4,8 @@ import { isReaderLock } from "./observed.js";
4
4
  import { isRoleLock } from "./roles.js";
5
5
  import { isSignerLock } from "./signers.js";
6
6
  import { isReleaseBinding } from "../host/release.js";
7
+ import { isRegionDeclaration } from "./region-declarations.js";
8
+ import { ownLock } from "../observe/runner.js";
7
9
  function isEffectLock(line) {
8
10
  const got = parse(line);
9
11
  return got.kind === 'fact' && got.value.fields['measure'] === 'effect';
@@ -11,7 +13,7 @@ function isEffectLock(line) {
11
13
  /** A lock line the instrument itself consumes — an effect it runs, a reader it loads, a role it walks by, a signer it admits, the wire it reads by. */
12
14
  function configures(line) {
13
15
  const got = parse(line);
14
- return isReleaseBinding(line) || isEffectLock(line) || isReaderLock(line) || isRoleLock(line) || isSignerLock(line) || (got.kind === 'fact' && isWireClaim(got.value.fields));
16
+ return isReleaseBinding(line) || isEffectLock(line) || isReaderLock(line) || isRoleLock(line) || isSignerLock(line) || (got.kind === 'fact' && isWireClaim(got.value.fields)) || isRegionDeclaration(line, ownLock());
15
17
  }
16
18
  /** Every reads line that configures nothing is a ceiling: a bound some reading has to meet. */
17
19
  export function isCeiling(line) {
@@ -1,3 +1,4 @@
1
+ import { readOnly } from "../host/read-only.js";
1
2
  import { appendFileSync, dirname, join, mkdirSync, readdirSync, relative, resolve } from "../host/io.js";
2
3
  import { bytesDigest as bytesWith, fullDigest as fullWith } from "../host/digest.js";
3
4
  import { coordinatesUnder, observeFile, observeText, stampOf } from "../observe/files.js";
@@ -116,6 +117,8 @@ export function coordinateDigest(store, at) {
116
117
  return held;
117
118
  const digest = bytesDigest(store, observeFile(at) ?? new Uint8Array());
118
119
  memo.set(key, digest);
120
+ if (readOnly())
121
+ return digest;
119
122
  mkdirSync(join(store, 'cas'), { recursive: true });
120
123
  appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${digest}\n`);
121
124
  return digest;
@@ -127,6 +130,8 @@ export function memoized(store, key, compute) {
127
130
  return held;
128
131
  const value = compute();
129
132
  memo.set(key, value);
133
+ if (readOnly())
134
+ return value;
130
135
  mkdirSync(join(store, 'cas'), { recursive: true });
131
136
  appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${value}\n`);
132
137
  return value;
@@ -141,7 +146,8 @@ const importsOf = (store, at, digest) => {
141
146
  const found = [...new TextDecoder().decode(observeFile(at) ?? new Uint8Array()).matchAll(/(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s*)['"]([^'"]*)['"]/g)].map((m) => m[1] ?? '');
142
147
  imported.set(digest, found);
143
148
  memo.set(key, found.join('|'));
144
- appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${found.join('|')}\n`);
149
+ if (!readOnly())
150
+ appendFileSync(join(store, 'cas', `stamps${EXTENSION}`), `${key}\t${found.join('|')}\n`);
145
151
  return found;
146
152
  };
147
153
  /**
package/dist/fold/kept.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { readOnly } from "../host/read-only.js";
1
2
  import { createHash, dirname, existsSync, join, mkdirSync, readdirSync, renameSync, writeFileSync } from "../host/io.js";
2
3
  import { bytesDigest, coordinateDigest } from "./digests.js";
3
4
  import { viewsOf } from "./views.js";
@@ -129,8 +130,11 @@ export function keptPlace(store, under) {
129
130
  };
130
131
  }
131
132
  export const keptRender = (store, key, name) => observeText(memoAt(store, `render/${key}/${name}`));
132
- export const keepRender = (store, _signer, key, name, _epoch, text_) => { landMemo(store, `render/${key}/${name}`, text(text_)); };
133
+ export const keepRender = (store, _signer, key, name, _epoch, text_) => { if (!readOnly())
134
+ landMemo(store, `render/${key}/${name}`, text(text_)); };
133
135
  export function keepFold(store, key, fold, points, instrument = '') {
136
+ if (readOnly())
137
+ return;
134
138
  const packed = {
135
139
  ...fold,
136
140
  standing: joined(fold.standing),
@@ -15,6 +15,7 @@ export declare const readersBehind: () => readonly {
15
15
  readonly module: string;
16
16
  readonly places: number;
17
17
  }[];
18
+ export declare const readerRefusals: () => readonly string[];
18
19
  export declare function readerLines(store: string): readonly string[];
19
20
  /**
20
21
  * A reader never blocks a fold that did not ask to wait: what it read before is served at once, and a reader whose
@@ -1,3 +1,5 @@
1
+ import { readerLifetime, readerAllowsEmpty, assertReaderInputs, parse as parseLifetime } from '@lapxo/topos/wire';
2
+ import { readOnly } from "../host/read-only.js";
1
3
  import { basename, dirname, existsSync, mkdirSync, resolve, rmSync, spawn, statSync, writeFileSync } from "../host/io.js";
2
4
  import { bundleOf, processOf } from "../observe/runner.js";
3
5
  import { canonical, parse, PROTOCOL } from '@lapxo/topos/wire';
@@ -15,7 +17,7 @@ import { capsuleReadersOf, handedAt, lookOf, offersOf } from "./readers.js";
15
17
  import { runReader, runs } from "../observe/run.js";
16
18
  import { LEVEL, OBSERVED, keepObservation, keyOf, legacyOf, observation, observed as observedAt, opened, pointer, readerOf, regionIn, setAside, sha, trimmed } from "./shards.js";
17
19
  import { saidDigest } from "./kept.js";
18
- import { carriedIn } from "./resolved.js";
20
+ import { carriedIn, carriedFrom } from "./resolved.js";
19
21
  export const cased = (question) => /^(?:vector|sample)\//.test(question);
20
22
  export function isReaderLock(line) {
21
23
  const got = parse(line);
@@ -87,6 +89,8 @@ function takeReadings(observed) {
87
89
  }
88
90
  const behind = [];
89
91
  export const readersBehind = () => behind;
92
+ const refused = [];
93
+ export const readerRefusals = () => refused;
90
94
  const alive = (pid) => { try {
91
95
  return process.kill(pid, 0);
92
96
  }
@@ -170,6 +174,14 @@ export function readerReaches(reach, where) {
170
174
  return where.some((one) => reached(trimmed(one)));
171
175
  }
172
176
  export function observedClaims(root, store, options = {}) {
177
+ refused.length = 0;
178
+ if (readOnly()) {
179
+ const carried = (observeText(resolve(root, 'receipts.bound')) ?? '').split('\n').filter(isWire);
180
+ const evidence = carriedFrom(store, carried);
181
+ if (evidence === undefined)
182
+ throw Error('REFUSE·preview native receipt bytes unavailable; fold the declared inputs first');
183
+ return evidence;
184
+ }
173
185
  const wait = options.wait ?? true;
174
186
  const standing = lockStanding(store);
175
187
  const lines = readersOf(store);
@@ -253,14 +265,15 @@ export function observedClaims(root, store, options = {}) {
253
265
  }
254
266
  function observeOne(reader, started) {
255
267
  const module = resolve(root, reader.module);
256
- const asked = reader.capsule === undefined ? undefined : capsuleAt(reader.capsule.digest, reader.capsule.entry);
268
+ const asked = reader.capsule === undefined ? undefined : reader.capsule.selected ?? capsuleAt(reader.capsule.digest, reader.capsule.entry);
257
269
  const source = reader.capsule === undefined ? observeFile(module) : asked === undefined ? undefined : new Uint8Array();
258
- const bundle = source && reader.capsule === undefined ? bundleOf(module) ?? 'unbundled' : 'none';
270
+ const bundle = source && reader.capsule === undefined ? bundleOf(module) ?? bytesDigest(store, source) : 'none';
259
271
  const offered = offersOf(root, store, reader, roleOf, look, (where) => reached(trimmed(where)));
272
+ assertReaderInputs(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module, offered.length);
260
273
  const vectors = reader.capsule === undefined ? observeText(resolve(root, vectorsOf(reader.module, standing))) : undefined;
261
274
  const kept = whole && legacy === undefined && answered === undefined && source ? sha([
262
275
  speaker, reader.module, reader.kind, reader.shape.join('|'), reader.where.join('|'), bundle, vectors === undefined ? 'no vectors' : sha(vectors),
263
- reader.capsule === undefined ? '' : `${reader.capsule.digest} ${reader.capsule.region} ${reader.capsule.reads.join('|')} ${[...reader.capsule.globs].map(([place, globs]) => `${place}${globs.join('|')}`).join(' ')}`,
276
+ reader.capsule === undefined ? '' : `${reader.capsule.selected?.selection ?? reader.capsule.digest} ${reader.capsule.region} ${reader.capsule.reads.join('|')} ${[...reader.capsule.globs].map(([place, globs]) => `${place}${globs.join('|')}`).join(' ')}`,
264
277
  ...offered.map((offer) => `${offer.place} ${offer.digest?.() ?? bytesDigest(store, offer.bytes())}`),
265
278
  ].join('\n')) : undefined;
266
279
  const stored = kept === undefined ? undefined : observeText(memoAt(store, `observed/${kept}`));
@@ -275,8 +288,8 @@ export function observedClaims(root, store, options = {}) {
275
288
  const own = shardLines(store, speaker, home).filter((line) => fieldOf(line, 'scope') === `reader/${reader.module}`);
276
289
  const lineage = own.filter((line) => fieldOf(line, 'measure') === 'identity').map((line) => fieldOf(line, 'value'));
277
290
  const sum = source && reader.capsule === undefined ? bytesDigest(store, new TextEncoder().encode(`${reader.module} ${bundle}`)).replace(/^sha256:/, '').slice(0, 12) : 'none';
278
- const measured = reader.capsule !== undefined ? { by: source ? sha(`${reader.capsule.digest}\n${reader.capsule.region}`).slice(0, 12) : 'none', lines: [] }
279
- : source ? identityOf({ store, root, speaker, module: reader.module, kind: reader.kind, code: sum, lineage, kept: own.filter((line) => /(?:^|\s)measure=(?:extension|emits)\s/.test(line)) })
291
+ const measured = reader.capsule !== undefined ? { by: source ? sha(`${reader.capsule.selected?.selection ?? reader.capsule.digest}\n${reader.capsule.region}`).slice(0, 12) : 'none', lines: [] }
292
+ : source ? identityOf({ store, root, speaker, module: reader.module, kind: reader.kind, code: sum, lineage, lifetime: readerLifetime(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module), kept: own.filter((line) => /(?:^|\s)measure=(?:extension|emits)\s/.test(line)) })
280
293
  : { by: 'none', lines: [] };
281
294
  const by = measured.by;
282
295
  const level = [...measured.lines];
@@ -351,8 +364,10 @@ export function observedClaims(root, store, options = {}) {
351
364
  }
352
365
  return [];
353
366
  };
354
- if (due.length && !source)
367
+ if (due.length && !source) {
368
+ refused.push(`REFUSE·reader ${reader.module} unavailable · ${due.length} readings remain due`);
355
369
  return false;
370
+ }
356
371
  if (due.length && !wait) {
357
372
  for (const plan of plans) {
358
373
  if (plan.said !== undefined) {
@@ -394,14 +409,18 @@ export function observedClaims(root, store, options = {}) {
394
409
  throw new Error(`REFUSE·reader ${reader.module} · ${answer?.why ?? 'no capsule answer'}`);
395
410
  return (answer.claims ?? []);
396
411
  })
397
- : runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })));
412
+ : runReader(module, root, due.map((d) => ({ place: d.place, ...(reader.kind === 'js' ? { text: new TextDecoder().decode(d.bytes) } : {}), held: heldOf(d.place, d.region) })), readerLifetime(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module));
398
413
  }
399
414
  catch (error) {
400
- process.stderr.write(`REFUSE reader ${reader.module} · ${error instanceof Error ? error.message : String(error)}\n`);
415
+ const why = `REFUSE·reader ${reader.module} · ${error instanceof Error ? error.message : String(error)}`;
416
+ refused.push(why);
417
+ process.stderr.write(`${why}\n`);
401
418
  return false;
402
419
  }
403
- if (rows.every((mine) => !mine.length)) {
404
- process.stderr.write(`REFUSE reader ${reader.module} · answered none of ${due.length} places · nothing landed, they stay due\n`);
420
+ if (rows.every((mine) => !mine.length) && !readerAllowsEmpty(standing.flatMap(line => { const p = parseLifetime(line); return p.kind === 'fact' ? [p.value.fields] : []; }), reader.module)) {
421
+ const why = `REFUSE·reader ${reader.module} · answered none of ${due.length} places · nothing landed, they stay due`;
422
+ refused.push(why);
423
+ process.stderr.write(`${why}\n`);
405
424
  return false;
406
425
  }
407
426
  ran += due.length;