@lapxo/bound 0.1.4 → 0.1.5

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 (44) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +26 -5
  3. package/TARGET.bound +83 -19
  4. package/dist/cli/accept.js +21 -9
  5. package/dist/cli/owner.d.ts +2 -0
  6. package/dist/cli/owner.js +17 -2
  7. package/dist/cli/pass.js +15 -10
  8. package/dist/cli/place.js +8 -1
  9. package/dist/cli/verb.js +35 -1
  10. package/dist/fold/claims.js +5 -1
  11. package/dist/fold/closed.js +44 -13
  12. package/dist/fold/digests.js +5 -2
  13. package/dist/fold/observed.js +195 -174
  14. package/dist/fold/resolved.js +6 -2
  15. package/dist/host/act-cost.d.ts +13 -0
  16. package/dist/host/act-cost.js +25 -0
  17. package/dist/host/adapters/modules/javascript.d.ts +1 -0
  18. package/dist/host/adapters/modules/javascript.js +48 -0
  19. package/dist/host/capsule-process.js +22 -19
  20. package/dist/host/capsule.js +5 -6
  21. package/dist/host/ports/ingress.d.ts +24 -0
  22. package/dist/host/ports/ingress.js +81 -0
  23. package/dist/host/reading-closures.d.ts +28 -0
  24. package/dist/host/reading-closures.js +146 -0
  25. package/dist/host/release.js +24 -2
  26. package/dist/host/walk.d.ts +6 -0
  27. package/dist/host/walk.js +193 -0
  28. package/dist/observe/regions.js +3 -5
  29. package/dist/render/field.d.ts +1 -7
  30. package/dist/render/field.js +6 -12
  31. package/dist/render/names.d.ts +1 -1
  32. package/dist/render/names.js +1 -1
  33. package/dist/render/view.js +16 -11
  34. package/docs/contracts/archive.md +11 -0
  35. package/docs/contracts/cli.md +11 -0
  36. package/docs/contracts/documents.md +35 -0
  37. package/docs/contracts/evidence-view.md +6 -0
  38. package/docs/contracts/publication.md +17 -0
  39. package/docs/contracts/typed-object.md +27 -0
  40. package/docs/contracts/view-walk.md +7 -0
  41. package/docs/contracts/views.md +29 -0
  42. package/docs/usage.md +6 -0
  43. package/package.json +6 -4
  44. package/receipts.bound +314 -92
@@ -11,7 +11,7 @@ import { fieldOf, isConfig, foldClaims, sayingOf, selfName } from "../fold/claim
11
11
  import { isReaderLock } from "../fold/observed.js";
12
12
  import { authorityFor, publicKeyOf, rootSigner, writerFor } from "../fold/signers.js";
13
13
  import { lockLines, lockStanding } from "../fold/keys.js";
14
- import { keepVerdict, keptVerdict, land, landBlob, storeOf } from "../land/ledger.js";
14
+ import { keepVerdict, keptVerdict, land, landBlob, ledgerLines, storeOf } from "../land/ledger.js";
15
15
  import { signConsentLot, verifiesOwnerLine } from "../land/sign.js";
16
16
  import { signerFor } from "../host/signing.js";
17
17
  import { appliedIn, ownLockOf } from "../fold/signed.js";
@@ -19,7 +19,7 @@ import { wordOf } from "../fold/wire.js";
19
19
  import { hasVouchedTree, landCoordinates, locationsOf, vouchedCoordinates } from "../land/vouched.js";
20
20
  import { entriesIn, observeFile } from "../observe/files.js";
21
21
  import { coneOf, standingOf } from "./place.js";
22
- import { ownLock, ownRoot, ownStore } from "../observe/runner.js";
22
+ import { ownLock, ownRoot } from "../observe/runner.js";
23
23
  import { widenedWithoutWitness } from "../judge/widening.js";
24
24
  import { retractedTogether, withdrawal } from "../land/withdraw.js";
25
25
  import { isCeiling } from "../fold/configures.js";
@@ -36,7 +36,7 @@ import { lateFacts } from "../judge/late.js";
36
36
  import { paidOf } from "../fold/paid.js";
37
37
  import { aliasPinned } from "../land/alias.js";
38
38
  import { coordinatesWithdrawn } from "../land/leaving.js";
39
- import { ownerOf, epochOf, forksItself, forksJoinedBy, renderInto, sideOf, signedDemands, signedOwnerLines } from "./owner.js";
39
+ import { ownerOf, epochOf, localActLines, forksItself, forksJoinedBy, renderInto, sideOf, signedDemands, signedOwnerLines } from "./owner.js";
40
40
  import { claimLinesIn, entryOf, namedPlaces, placesOfFile, positionalsOf, valueOf } from "./args.js";
41
41
  import { FLAGS } from "./names.js";
42
42
  import { judgeDemand, landTake } from "./takes.js";
@@ -53,7 +53,15 @@ function admissionEvidence(root, signed, standing) {
53
53
  if (coordinates.refused.length)
54
54
  throw Error(`REFUSE·withdraw a file is still read: ${coordinates.refused.map(one => `${one.path} by ${one.by}`).join(' · ')}`);
55
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));
56
+ const judged = signed.filter(one => isConfig(one) && fieldOf(one, 'role') === 'demands' && fieldOf(one, 'value') !== 'withdraw' && !fieldOf(one, 'scope').startsWith(view)).map(one => {
57
+ const scope = fieldOf(one, 'scope');
58
+ const alreadyDemanded = standing.some(held => isConfig(held) && fieldOf(held, 'role') === 'demands'
59
+ && fieldOf(held, 'value') !== 'withdraw' && fieldOf(held, 'scope') === scope
60
+ && fieldOf(held, 'form') === fieldOf(one, 'form') && fieldOf(held, 'measure') === fieldOf(one, 'measure'));
61
+ // Declaring an obligation is not a claim to have discharged it.
62
+ return alreadyDemanded ? judgeDemand(root, one)
63
+ : { kind: 'owed', scope, why: 'new demand declared; no conformance reading is claimed' };
64
+ });
57
65
  const refused = judged.filter(one => one.kind === 'refused' || (one.kind === 'judged' && one.verdict.kind === 'refuse'));
58
66
  if (refused.length)
59
67
  throw Error(`REFUSE·takes ${refused.map(one => `${one.scope}: ${one.kind === 'judged' ? one.verdict.why : one.kind === 'refused' ? one.why : ''}`).join(' · ')}`);
@@ -126,7 +134,7 @@ export async function signedBatch(root, args, files) {
126
134
  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
135
  return { kind: 'exit', code: 2 };
128
136
  }
129
- const epoch = epochOf([...lockLines(store), ...destinations.flatMap((place) => appliedIn(root, place))]) + 1;
137
+ const epoch = epochOf(localActLines(root, destinations)) + 1;
130
138
  const stale = proposed.filter((line) => fieldOf(line, 'sig')).flatMap((line) => ((by, key) => (key === undefined || !verifiesOwnerLine(line, key, signaturesOf(store).admitted)
131
139
  ? [`${fieldOf(line, 'scope')} · the signature does not hold for ${by || '∅'}`]
132
140
  : []))(fieldOf(line, 'by'), publicKeyOf(root, fieldOf(line, 'by'))));
@@ -139,9 +147,13 @@ export async function signedBatch(root, args, files) {
139
147
  const [named, places] = [namedPlaces(args), placesIn(root)];
140
148
  const inLock = new Set(lockLines(store).map(sayingOf));
141
149
  const inPlace = new Map(places.map((place) => [place, new Set(appliedIn(root, place).map(sayingOf))]));
150
+ const deliveryAuthority = authorityFor(lockLines(store), rootSigner(root), signaturesOf(store).admitted);
151
+ const delivered = (line) => Boolean(fieldOf(line, 'sig'))
152
+ && deliveryAuthority.of(line).kind === 'admitted'
153
+ && ledgerLines(store, fieldOf(line, 'by')).includes(line);
142
154
  const held = (line, at = named.length ? named : byFile.flatMap(([heads, lines]) => (lines.includes(line) ? heads : []))) => at.length
143
155
  ? at.every((place) => inPlace.get(place)?.has(sayingOf(line)))
144
- : inLock.has(sayingOf(line)) || places.some((place) => inPlace.get(place)?.has(sayingOf(line)));
156
+ : delivered(line) || inLock.has(sayingOf(line)) || places.some((place) => inPlace.get(place)?.has(sayingOf(line)));
145
157
  if (proposed.every((line) => held(line))) {
146
158
  if (proposed.some(line => !isConfig(line))) {
147
159
  try {
@@ -335,7 +347,7 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
335
347
  for (const name of openedBy(descent, told.standing, signed))
336
348
  process.stderr.write(`OPEN ${name}\n`);
337
349
  if (owned.own.size)
338
- layReleases(store, ownRoot(), ownLock(), ownStore());
350
+ layReleases(store, root, told.standing, store);
339
351
  const landed = took('land', () => [...new Set(signed.map((line) => fieldOf(line, 'by')))].map((by) => land(store, by, signed.filter((line) => fieldOf(line, 'by') === by))));
340
352
  for (const [place, lines] of owned.own)
341
353
  landOwned(root, place, lines);
@@ -392,7 +404,7 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
392
404
  if (folder !== undefined)
393
405
  land(store, folder, [beat(perLine, 'line'), beat(1, fold.kept ? 'kept' : 'folded'), beat(Date.now() - started, 'total')]);
394
406
  process.stdout.write(`FACT signed ${signed.length + own} · landed ${landed.length ? landed.map((one) => `${one.appended} in ${one.at}`).join(' · ') : '0'}${[...owned.own].map(([place, lines]) => ` · ${lines.length} in ${place}/${LOCK}`).join('')} · epoch ${epoch}\n`
395
- + `TARGET ${rendered.standing} claims · ${rendered.forks.length} forks · ${rendered.vacuous.length} vacuous · ${rendered.grey.length} grey · ${rendered.refused.length} refused · ${Date.now() - started} ms\n`
407
+ + `TARGET ${rendered.standing} claims · ${rendered.forks.length} forks · ${rendered.vacuous.length} vacuous · ${rendered.grey.length} grey · ${rendered.refused.length} refused · readings · no encounters computed · ${Date.now() - started} ms\n`
396
408
  + `SPENT ${spent.join(' ms · ')} ms · ${signed.length} lines\n`);
397
409
  if (withdrawn) {
398
410
  process.stdout.write(`WITHDRAW ${withdrawn.taken.length} taken · ${withdrawn.exact.length} exact on distributive scales · ${withdrawn.open.length} on open scales, not computed\n`);
@@ -444,7 +456,7 @@ function repaid(root, proposed, started) {
444
456
  }
445
457
  const rendered = renderInto(root, store);
446
458
  process.stdout.write(`RENDER 0\n`);
447
- process.stdout.write(`TARGET ${rendered.standing} claims · ${rendered.forks.length} forks · ${rendered.vacuous.length} vacuous · ${rendered.grey.length} grey · ${rendered.refused.length} refused · ${Date.now() - started} ms\n`);
459
+ process.stdout.write(`TARGET ${rendered.standing} claims · ${rendered.forks.length} forks · ${rendered.vacuous.length} vacuous · ${rendered.grey.length} grey · ${rendered.refused.length} refused · readings · no encounters computed · ${Date.now() - started} ms\n`);
448
460
  return 0;
449
461
  }
450
462
  function packTake(one) {
@@ -14,6 +14,8 @@ export declare function historyOf(root: string, store: string, demands: readonly
14
14
  };
15
15
  export declare function signedDemands(store: string, root?: string): string[];
16
16
  export declare function forkedScopes(store: string): Set<string>;
17
+ /** Import receipts are local acts; foreign evidence and sender epochs never enter this input. */
18
+ export declare function localActLines(root: string, placed?: readonly string[]): readonly string[];
17
19
  export declare function epochOf(lines: readonly string[]): number;
18
20
  export declare function targetFile(root: string): string;
19
21
  export declare function renderInto(root: string, store: string): {
package/dist/cli/owner.js CHANGED
@@ -1,8 +1,10 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { ingressReceipts } from "../host/ports/ingress.js";
1
3
  import { readOnly } from "../host/read-only.js";
2
4
  import { keyFor, lockLines, releaseOf, signedLockLines } from "../fold/keys.js";
3
5
  import { existsSync, renameSync, writeFileSync } from 'node:fs';
4
6
  import { join, resolve } from 'node:path';
5
- import { bytesDigest, fullDigest, memoized, signaturesOf } from "../fold/digests.js";
7
+ import { bytesDigest, fullDigest, memoized, signaturesOf, instrumentDigest } from "../fold/digests.js";
6
8
  import { claimKey, isConfig, fieldOf, foldClaims, LOCK, selfName } from "../fold/claims.js";
7
9
  import { asksThePlace } from "../fold/configures.js";
8
10
  import { receiptsIn } from "../fold/receipts.js";
@@ -10,6 +12,7 @@ import { authorityFor, releaseSigner, rootSigner } from "../fold/signers.js";
10
12
  import { storeRoot } from "../host/ports/store.js";
11
13
  import { blobAt, landBlob, ledgerLines, storeOf } from "../land/ledger.js";
12
14
  import { observeText } from "../observe/files.js";
15
+ import { appliedIn } from "../fold/signed.js";
13
16
  import { everLanded, inside, locationsOf } from "../land/vouched.js";
14
17
  import { renderTarget } from "../render/target.js";
15
18
  export const ownerOf = (store, told = []) => keyFor(store, 'authorize', told);
@@ -47,6 +50,18 @@ export function forkedScopes(store) {
47
50
  const lines = signedOwnerLines(store);
48
51
  return new Set(foldClaims(lines, forkReceipts(store, lines)).forks.flatMap((f) => f.lines.map((line) => fieldOf(line, 'scope'))));
49
52
  }
53
+ /** Import receipts are local acts; foreign evidence and sender epochs never enter this input. */
54
+ export function localActLines(root, placed = []) {
55
+ const store = storeOf(root), local = lockLines(store), receipts = ingressReceipts(store);
56
+ if (receipts.length) {
57
+ const authority = authorityFor(local, rootSigner(root), signaturesOf(store).admitted);
58
+ for (const receipt of receipts) {
59
+ if (fieldOf(receipt, 'scope') !== 'receipts' || fieldOf(receipt, 'role') !== 'writes' || fieldOf(receipt, 'form') !== 'alphabet' || fieldOf(receipt, 'measure') !== 'digest' || authority.of(receipt).kind !== 'admitted')
60
+ throw Error('REFUSE·ingress local import receipt is not admitted');
61
+ }
62
+ }
63
+ return [...local, ...receipts, ...placed.flatMap(place => appliedIn(root, place))];
64
+ }
50
65
  export function epochOf(lines) {
51
66
  return lines.reduce((top, line) => Math.max(top, Number(fieldOf(line, 'epoch')) || 0), 0);
52
67
  }
@@ -92,7 +107,7 @@ export function renderedLock(root, store, told = []) {
92
107
  }
93
108
  const anchor = [join(storeRoot(root), `${selfName()}.keys`), join(storeOf(root), `${selfName()}.keys`)].map((at) => observeText(at) ?? '').join('\n');
94
109
  const judged = ledgerLines(store, 'judge');
95
- const key = `lock ${fullDigest(store, [new Date().toISOString().slice(0, 10), anchor, `${judged.length} ${judged.at(-1) ?? ''}`, ...owned])}`;
110
+ const key = `lock ${fullDigest(store, [instrumentDigest(store, fileURLToPath(import.meta.url), [], root), new Date().toISOString().slice(0, 10), anchor, `${judged.length} ${judged.at(-1) ?? ''}`, ...owned])}`;
96
111
  const held = memoized(store, key, () => {
97
112
  const bytes = new TextEncoder().encode(JSON.stringify(render()));
98
113
  const digest = bytesDigest(store, bytes);
package/dist/cli/pass.js CHANGED
@@ -6,9 +6,9 @@ import { foldPlace, standingOf } from "./place.js";
6
6
  import { fieldOf, selfName } from "../fold/claims.js";
7
7
  import { observedClaims } from "../fold/observed.js";
8
8
  import { layReleases, resolvedOf } from "../host/release.js";
9
- import { ownLock, ownRoot, ownStore, processOf } from "../observe/runner.js";
9
+ import { ownLock, ownRoot, processOf } from "../observe/runner.js";
10
10
  import { landLaid } from "../fold/laid.js";
11
- import { actSeconds, idleCount, instrumentKeep, instrumentKept, meets, noteIdle, receiptPlaces, receiptsSeen } from "../fold/closed.js";
11
+ import { actSeconds, closeReceipts, idleCount, instrumentKeep, instrumentKept, meets, noteIdle, receiptPlaces, receiptsSeen } from "../fold/closed.js";
12
12
  import { algorithmCost, instrumentOf, instrumentDigest } from "../fold/digests.js";
13
13
  import { writerFor } from "../fold/signers.js";
14
14
  import { defaulted, placesOf, viewsOf } from "../fold/views.js";
@@ -24,6 +24,7 @@ import { releasePolicyLines } from "../fold/release-policy.js";
24
24
  import { carriedFrom } from "../fold/resolved.js";
25
25
  import { wireLine } from "../fold/wire.js";
26
26
  import { isWire, RECEIPTS } from "../fold/claims.js";
27
+ import { withoutEffects } from "../host/read-only.js";
27
28
  /**
28
29
  * The pass of a place, in one process: the releases its instrument names are laid out first, its readers observe
29
30
  * once, each place a view writes in is folded once, the places before the root whose door reads them, and every coordinate a
@@ -74,7 +75,7 @@ async function checked(root, entry, under) {
74
75
  export async function pass(root, entry, under, check) {
75
76
  if (check) {
76
77
  // Verification reads existing evidence; it grants no admission authority.
77
- return emptyLedger(storeOf(root)) ? checked(root, entry, under) : underTheLock(storeOf(root), 'check', () => checked(root, entry, under));
78
+ return withoutEffects(() => emptyLedger(storeOf(root)) ? checked(root, entry, under) : underTheLock(storeOf(root), 'check', () => checked(root, entry, under)));
78
79
  }
79
80
  if (!under && emptyLedger(storeOf(root))) {
80
81
  sayStranger(root);
@@ -85,8 +86,11 @@ export async function pass(root, entry, under, check) {
85
86
  const swept = await sweep(root, entry, under, check, []);
86
87
  for (const coordinate of check ? swept.written : [])
87
88
  process.stdout.write(`DIFFERS ${coordinate}\n`);
89
+ // Byte seals describe the completed render pass, not its intermediate outputs.
90
+ if (!swept.written.length)
91
+ await closeReceipts(root, storeOf(root));
88
92
  const closed = meets(root, storeOf(root), under ?? '') && !swept.written.length;
89
- const fold = foldPlace(root, entry, under, false, { standing: swept.standingAt.get(under ?? '') ?? standingOf(root, under), observed: swept.observed.length ? swept.observed : observedClaims(root, storeOf(root), { wait: false }), free: true });
93
+ const fold = foldPlace(root, entry, under, false, { standing: swept.standingAt.get(under ?? '') ?? standingOf(root, under), observed: swept.settled ? [] : swept.observed.length ? swept.observed : observedClaims(root, storeOf(root), { wait: false }), free: !swept.settled });
90
94
  for (const line of releasePolicyLines(fold.release))
91
95
  process.stdout.write(`${line}\n`);
92
96
  if (closed) {
@@ -140,14 +144,15 @@ async function sweep(root, entry, under, check, told, journal, given, keep, reac
140
144
  process.stderr.write(`CLOSED ${under} · lock ${lock} ms · hash ${hash} ms\n`);
141
145
  const seconds = actSeconds();
142
146
  process.stderr.write(`COST act ${seconds} s\n`);
143
- return { same: 0, written: [], places: under === undefined ? read.count : 1, observed: [], standingAt: new Map(), folds: new Map(), said: [] };
147
+ return { same: 0, written: [], places: under === undefined ? read.count : 1, observed: [], standingAt: new Map(), folds: new Map(), said: [], settled: true };
144
148
  }
145
- layReleases(store, ownRoot(), ownLock(), ownStore());
149
+ const lock = standingOf(root, undefined, told);
150
+ // A place asks for its own mounts; the instrument catalog is not a consumer dependency.
151
+ layReleases(store, root, lock.standing, store);
146
152
  const descent = reach ?? (under ? [under.replace(/\/$/, '')] : undefined);
147
153
  const limited = descent !== undefined;
148
154
  const observed = given ?? observedClaims(root, store, { wait: true, ...(descent !== undefined ? { reach: descent } : {}) });
149
- const lock = standingOf(root, undefined, told);
150
- landLaid(store, ownRoot(), ownLock(), writerFor(lock.standing, 'fold'));
155
+ landLaid(store, root, lock.standing, writerFor(lock.standing, 'fold'));
151
156
  const shaped = (standing) => [...viewsOf(standing).values()].flatMap((view) => ('regions' in view && view.shape ? [view] : []));
152
157
  const views = shaped(lock.standing);
153
158
  const tree = viewsOf(lock.lock);
@@ -264,8 +269,8 @@ async function sweep(root, entry, under, check, told, journal, given, keep, reac
264
269
  }
265
270
  const said = [...answered.values()].flat();
266
271
  if (!check && journal === undefined)
267
- landSaid(store, writerFor(lock.standing, 'fold'), [...resolvedOf(store, ownRoot(), ownLock(), ownStore()), ...said]);
268
- return { same, written, places: places.length + (under ? 0 : 1), observed, standingAt, folds, said: journal === undefined ? [] : said };
272
+ landSaid(store, writerFor(lock.standing, 'fold'), [...resolvedOf(store, root, lock.standing, store), ...said]);
273
+ return { same, written, places: places.length + (under ? 0 : 1), observed, standingAt, folds, said: journal === undefined ? [] : said, settled: false };
269
274
  }
270
275
  function drawParallel(jobs) {
271
276
  const out = new Map();
package/dist/cli/place.js CHANGED
@@ -7,7 +7,7 @@ import { basename, join } from 'node:path';
7
7
  import { observeText } from "../observe/files.js";
8
8
  import { placesOf, viewsOf } from "../fold/views.js";
9
9
  import { prerender, viewOf } from "../render/view.js";
10
- import { meets } from "../fold/closed.js";
10
+ import { meets, instrumentKept } from "../fold/closed.js";
11
11
  import { foldPoints, keepFold, keptPlace } from "../fold/kept.js";
12
12
  import { openCells } from "../render/open.js";
13
13
  import { boundOf, packRegions, regionsNamed, regionsOf, unlikeReplay } from "../fold/region.js";
@@ -165,6 +165,13 @@ function inheritedOf(root, standing) {
165
165
  export function coneOf(root, entry, ceilings, descent) {
166
166
  const store = storeOf(root);
167
167
  const { standing, rendered } = standingOf(root);
168
+ // Admission consumes the verified completed fold; it does not ask providers again.
169
+ // Receipt bytes, reader history and instrument identity must all still agree.
170
+ if (meets(root, store, '') && instrumentKept(store) === instrumentOf(store, entry, standing, root, ownLock())) {
171
+ const held = keptPlace(store);
172
+ if (held !== undefined)
173
+ return { standing, observed: held.observed, own: held.own, kept: true };
174
+ }
168
175
  const named = regionsNamed(standing);
169
176
  const reach = descent !== undefined && descent.length ? [...descent] : [...new Set(ceilings.map((line) => boundOf(named, line).region))];
170
177
  const read = observedClaims(root, store, { wait: false, ...(reach.includes('') ? {} : { reach }) });
package/dist/cli/verb.js CHANGED
@@ -10,6 +10,7 @@ import { objectFold } from "../fold/object.js";
10
10
  import { OBJECT_VIEW } from '@lapxo/topos/wire';
11
11
  import { toposStanding } from '@lapxo/topos/standing';
12
12
  import { wireAt, parse as parseWire } from '@lapxo/topos/wire';
13
+ import { lockStanding } from "../fold/keys.js";
13
14
  import { ownLock } from "../observe/runner.js";
14
15
  import { signedOwnerLines } from "./owner.js";
15
16
  import { existsSync, lstatSync, mkdirSync, writeFileSync } from 'node:fs';
@@ -42,6 +43,9 @@ import { ownerOf, renderInto, signedDemands, targetFile } from "./owner.js";
42
43
  import { claimLinesIn, entryOf, namedPlaces, positionalsOf, valueOf } from "./args.js";
43
44
  import { EXIT, FLAGS, NAMED_VIEWS, helpLines } from "./names.js";
44
45
  import { accept, signedBatch } from "./accept.js";
46
+ import { renderWalk, landWalk, foreignWalkLines } from "../host/walk.js";
47
+ import { isWalkHeader } from '@lapxo/topos/walk';
48
+ import { ingressBundles, ingressReceipts } from "../host/ports/ingress.js";
45
49
  /** The first land of a lock: TARGET.bound into an empty owner ledger, then the sources that lock names. */
46
50
  async function firstLand(root, args) {
47
51
  const started = Date.now();
@@ -184,6 +188,17 @@ async function view(root, under, asked, told = [], coord, key, includePolicy = t
184
188
  process.stderr.write(`${selfName()}: the owner ledger is empty; land it before rendering a view\n`);
185
189
  return EXIT.refuse;
186
190
  }
191
+ if (name === 'evidence' && !under && !told.length && ingressBundles(storeOf(root)).length) {
192
+ const declared = viewsOf(lockStanding(storeOf(root))).get(name);
193
+ if (!declared || declared.shape || !declared.regions.some(region => region.name === 'evidence'))
194
+ throw Error('REFUSE·view foreign evidence requires a declared evidence view');
195
+ const received = await foreignWalkLines(root), authority = authorityFor(lockLines(storeOf(root)), rootSigner(root), signaturesOf(storeOf(root)).admitted);
196
+ const local = [...new Set(authority.admitted.flatMap(key => ledgerLines(storeOf(root), key.id)))].filter(line => authority.of(line).kind === 'admitted');
197
+ for (const line of [...new Set([...local, ...ingressReceipts(storeOf(root)), ...received])])
198
+ process.stdout.write(line + '\n');
199
+ process.stdout.write('FACT authenticated histories · readings · no encounters computed\n');
200
+ return EXIT.closed;
201
+ }
187
202
  const at = Date.now();
188
203
  const place = under?.replace(/\/$/, '');
189
204
  const released = releaseOf(storeOf(root));
@@ -367,6 +382,12 @@ async function landVerb(root, rest) {
367
382
  process.once('uncaughtException', (error) => die(error instanceof Error ? error.message : String(error)));
368
383
  process.once('unhandledRejection', (error) => die(error instanceof Error ? error.message : String(error)));
369
384
  try {
385
+ const proposed = batch.flatMap(claimLinesIn);
386
+ if (proposed.some(isWalkHeader)) {
387
+ for (const line of await landWalk(root, proposed, valueOf(rest, FLAGS.key), valueOf(rest, FLAGS.keyFile), valueOf(rest, FLAGS.signer)))
388
+ process.stdout.write(line + '\n');
389
+ return EXIT.closed;
390
+ }
370
391
  return await accept(root, rest);
371
392
  }
372
393
  catch (error) {
@@ -395,7 +416,7 @@ function foldFile(at) {
395
416
  process.stdout.write(`${line}\n`);
396
417
  for (const fork of folded.forks)
397
418
  process.stdout.write(`FORK ${fork.key}${fork.state ? ` · ${fork.state}` : ''}\n`);
398
- process.stdout.write(`FACT ${folded.standing.length} standing · ${folded.forks.length} forks · ${basename(at)}\n`);
419
+ process.stdout.write(`FACT ${folded.standing.length} standing · ${folded.forks.length} forks · ${basename(at)} · readings · no encounters computed\n`);
399
420
  return EXIT.closed;
400
421
  }
401
422
  function foldVerb(root, rest) {
@@ -429,6 +450,19 @@ async function foldResolved(root, under, argv) {
429
450
  // Public content is authenticated by its digest, independently of owner history.
430
451
  // A named view does not request mounting every release of the instrument.
431
452
  const receiptCheck = as === undefined && argv.includes(FLAGS.check);
453
+ if (as?.split('@')[0] === 'walk') {
454
+ if (argv.includes(FLAGS.check))
455
+ throw Error('REFUSE·walk check does not initiate or sign an exchange');
456
+ if (under || inputs.length > 1)
457
+ throw Error('REFUSE·walk one place and one authenticated peer inventory per fold');
458
+ const depth = as.split('@')[1];
459
+ if (depth !== undefined && (!/^(0|[1-9][0-9]*)$/.test(depth) || !Number.isSafeInteger(Number(depth))))
460
+ throw Error('REFUSE·walk invalid resolution');
461
+ const peer = inputs.length ? claimLinesIn(inputs[0]) : [];
462
+ for (const line of await renderWalk(root, depth === undefined ? undefined : Number(depth), peer, valueOf(argv, FLAGS.key), valueOf(argv, FLAGS.keyFile), valueOf(argv, FLAGS.signer)))
463
+ process.stdout.write(line + '\n');
464
+ return EXIT.closed;
465
+ }
432
466
  if (as === OBJECT_VIEW) {
433
467
  const inputs = positionalsOf(argv).filter(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
434
468
  if (inputs.length > 1)
@@ -38,7 +38,11 @@ function regionsOf(line) {
38
38
  }
39
39
  function contracted(line, sides, receipts) {
40
40
  const scope = fieldsOf(line)['scope'] ?? '';
41
- const mine = exclusive(regionsOf(line), sides.filter((other) => other !== line).map(regionsOf));
41
+ const named = regionsOf(line);
42
+ // File receipts can discharge only declared file regions. No region is not an empty proof.
43
+ if (named.length === 0)
44
+ return false;
45
+ const mine = exclusive(named, sides.filter((other) => other !== line).map(regionsOf));
42
46
  const touched = receipts.filter((r) => r.scope === scope).flatMap((r) => r.moved.map((coordinate) => point(coordinate)));
43
47
  return contracts(mine, touched);
44
48
  }
@@ -1,6 +1,8 @@
1
+ import { actCosts } from "../host/act-cost.js";
2
+ import { verifiedReadingReceipts } from "../host/reading-closures.js";
1
3
  import { dirname, existsSync, join, mkdirSync, relative, resolve, statSync, writeFileSync } from "../host/io.js";
2
- import { canonical, LOCK, parse, roleAt, roleClaimsOf } from '@lapxo/topos/wire';
3
- import { RECEIPTS, fieldOf, isWire } from "./claims.js";
4
+ import { byBytes, canonical, LOCK, parse, publicationOf, roleAt, roleClaimsOf } from '@lapxo/topos/wire';
5
+ import { RECEIPTS, fieldOf, foldClaims, isConfig, isWire } from "./claims.js";
4
6
  import { bytesDigest as hashBytes, fullDigest as hashFull } from "../host/digest.js";
5
7
  import { worldAt } from "./places.js";
6
8
  import { ownLockOf } from "./signed.js";
@@ -9,15 +11,14 @@ import { storeAt, storeOf } from "../land/ledger.js";
9
11
  import { coordinatesUnder, observeFile, observeText } from "../observe/files.js";
10
12
  import { carriedFrom } from "./resolved.js";
11
13
  import { takenFor } from "../fold/place-inputs.js";
12
- /** A region is closed when the digest of its bytes is the one its receipt names. Folding a closed region is idle: idleCount is how many were already closed, actSeconds how long this act has run, receiptsSeen how many receipts were read and how long that took. */
14
+ /** A region is closed when the digest of its bytes is the one its receipt names. Folding a closed region is idle: idleCount is how many were already closed, actSeconds the largest measured act or unpartitioned host work, receiptsSeen how many receipts were read and how long that took. */
13
15
  let idle = 0;
14
- const began = Date.now();
15
16
  let seen = { count: 0, ms: 0 };
16
17
  export const idleCount = () => idle;
17
18
  export const noteIdle = () => {
18
19
  idle += 1;
19
20
  };
20
- export const actSeconds = () => Math.max(0, Math.round((Date.now() - began) / 1000));
21
+ export const actSeconds = () => actCosts.seconds();
21
22
  export const receiptsSeen = () => seen;
22
23
  const algorithmIn = (text) => text.match(/(?:value=|place:)([a-z0-9]+):[0-9a-f]{64}/)?.[1] ?? '';
23
24
  const lockStamp = (root, place) => {
@@ -55,6 +56,14 @@ function filesOf(root, place, region, inputs) {
55
56
  return [...new Set([...owned, ...observed, ...own])].filter(rel => rel !== receipt && !rel.split('/').some(part => part === '.bound' || part === '.cache' || part === '.git'));
56
57
  }
57
58
  const rooted = new Map();
59
+ function publicConfiguration(lines) {
60
+ if (!lines.length || !lines.every(isConfig))
61
+ return lines;
62
+ const folded = foldClaims(lines);
63
+ if (folded.forks.length)
64
+ throw Error('REFUSE·receipt disputed configuration standing');
65
+ return publicationOf(folded.standing.filter(line => fieldOf(line, 'value') !== 'withdraw'));
66
+ }
58
67
  function rootLines(root, place) {
59
68
  const name = place.replace(/\/$/, '');
60
69
  if (!name)
@@ -63,7 +72,7 @@ function rootLines(root, place) {
63
72
  const held = rooted.get(key);
64
73
  if (held !== undefined)
65
74
  return held;
66
- const standing = (observeText(join(root, LOCK)) ?? '').split('\n').filter(isWire);
75
+ const standing = publicConfiguration((observeText(join(root, LOCK)) ?? '').split('\n').filter(isWire));
67
76
  // Authority validates admission separately; it is not a rendered file input.
68
77
  const lines = standing.filter((line) => fieldOf(line, 'value') !== 'withdraw' && !fieldOf(line, 'scope').startsWith('keys/'));
69
78
  const take = takenFor(lines, `${name}/`, worldAt(root, name) ? { own: ownLockOf(root, name), tree: viewsOf(standing) } : undefined);
@@ -73,8 +82,20 @@ function rootLines(root, place) {
73
82
  }
74
83
  /** File-fold identity uses public inputs; admission history stays in its separate ledger guard. */
75
84
  export function fileRegionDigest(root, place, region, algorithm, inputs = []) {
76
- const files = filesOf(root, place, region, inputs);
77
- return hashFull([...files.map((rel) => `${relative(join(root, place), join(root, rel))} ${hashBytes(observeFile(join(root, rel)) ?? new Uint8Array(), algorithm)}`), ...rootLines(root, place)], algorithm);
85
+ const files = [...filesOf(root, place, region, inputs)].sort(byBytes);
86
+ const own = join(root, place, LOCK);
87
+ const bytesOf = (rel) => {
88
+ const at = join(root, rel), bytes = observeFile(at) ?? new Uint8Array();
89
+ if (resolve(at) !== resolve(own))
90
+ return bytes;
91
+ const lines = new TextDecoder().decode(bytes).split('\n').filter(isWire);
92
+ // Object history keeps its own contract. Configuration's public identity is
93
+ // the existing Topos publication projection, not its admission envelopes.
94
+ if (!lines.length || !lines.every(isConfig))
95
+ return bytes;
96
+ return new TextEncoder().encode(publicConfiguration(lines).join('\n') + '\n');
97
+ };
98
+ return hashFull([...files.map((rel) => `${relative(join(root, place), join(root, rel))} ${hashBytes(bytesOf(rel), algorithm)}`), ...rootLines(root, place)], algorithm);
78
99
  }
79
100
  function namedReceipts(root, store) {
80
101
  const text = observeText(join(root, RECEIPTS));
@@ -89,13 +110,15 @@ function namedReceipts(root, store) {
89
110
  }
90
111
  return out;
91
112
  }
113
+ // Only the instrument's file seals describe file-region bytes. Reader receipts keep their own semantic identity.
114
+ const fileSeal = (line) => isWire(line) && fieldOf(line, 'measure') === 'bytes' && fieldOf(line, 'by') === 'bound';
92
115
  function ownBytes(root) {
93
- return (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(line => isWire(line) && fieldOf(line, 'measure') === 'bytes');
116
+ return (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(fileSeal);
94
117
  }
95
118
  function bytesMeet(root, place) {
96
119
  const shipped = (observeText(join(root, place, RECEIPTS)) ?? '').split('\n').filter(isWire);
97
120
  const inputs = carriedFrom(storeOf(root), shipped) ?? [];
98
- const lines = shipped.filter(line => fieldOf(line, 'measure') === 'bytes');
121
+ const lines = shipped.filter(fileSeal);
99
122
  const algorithm = algorithmIn(lines.join('\n'));
100
123
  return lines.length > 0 && algorithm !== '' && lines.map((line) => fieldOf(line, 'value')).join('\n') === lines.map((line) => fileRegionDigest(root, place, fieldOf(line, 'scope'), algorithm, inputs)).join('\n');
101
124
  }
@@ -115,8 +138,8 @@ function rewritePlace(root, place) {
115
138
  const algorithm = algorithmIn(text);
116
139
  if (algorithm === '')
117
140
  return;
118
- const kept = text.split('\n').filter((line) => line === '' || !isWire(line) || fieldOf(line, 'measure') !== 'bytes');
119
- const regions = kept.filter((line) => isWire(line) && fieldOf(line, 'measure') === 'count' && fieldOf(line, 'scope').startsWith('receipts/'));
141
+ const kept = text.split('\n').filter((line) => line === '' || !fileSeal(line));
142
+ const regions = kept.filter((line) => isWire(line) && fieldOf(line, 'measure') === 'count' && fieldOf(line, 'by') === 'bound' && fieldOf(line, 'scope').startsWith('receipts/'));
120
143
  const bytes = regions.map((line) => {
121
144
  const scope = fieldOf(line, 'scope');
122
145
  const inputs = carriedFrom(storeOf(root), kept.filter(isWire)) ?? [];
@@ -146,11 +169,19 @@ export function meets(root, store, place) {
146
169
  const own = ownBytes(root);
147
170
  const carried = own.length ? carriedFrom(store, (observeText(join(root, RECEIPTS)) ?? '').split('\n').filter(isWire)) : undefined;
148
171
  const ownClosed = own.length > 0 && carried !== undefined && carried.length > 0 && bytesMeet(root, '');
149
- const closed = (own.length > 0 ? ownClosed : named.size > 0) && [...named].every(([one, expected]) => {
172
+ let closed = (own.length > 0 ? ownClosed : named.size > 0) && [...named].every(([one, expected]) => {
150
173
  const bytes = observeFile(join(root, one, RECEIPTS));
151
174
  const algorithm = expected.split(':')[0];
152
175
  return bytes !== undefined && hashBytes(bytes, algorithm) === expected && meets(root, store, `${one}/`);
153
176
  });
177
+ if (closed && carried) {
178
+ const algorithm = algorithmIn(own.join('\n'));
179
+ const readings = verifiedReadingReceipts(store, carried, text => hashFull(text, algorithm));
180
+ if (readings === undefined)
181
+ closed = false;
182
+ else if (readings)
183
+ process.stderr.write(`RECEIPTS ${readings} read · 0 opened · reading closures\n`);
184
+ }
154
185
  seen = { count: named.size + (own.length > 0 ? 1 : 0), ms: Date.now() - at };
155
186
  return closed;
156
187
  }
@@ -7,6 +7,7 @@ import { EXTENSION, fieldOf, foldClaims, selfName } from "./claims.js";
7
7
  import { wireLine } from "./wire.js";
8
8
  import { publicLock, LOCK } from '@lapxo/topos/wire';
9
9
  import { builtinModules, createRequire } from 'node:module';
10
+ import { moduleImports } from "../host/adapters/modules/javascript.js";
10
11
  const held = new Map();
11
12
  let algorithmMs = 0;
12
13
  export const algorithmCost = () => algorithmMs;
@@ -110,6 +111,8 @@ export function coordinateDigest(store, at) {
110
111
  const stamp = stampOf(at);
111
112
  if (stamp?.kind !== 'file')
112
113
  return stamp === undefined ? undefined : bytesDigest(store, observeFile(at) ?? new Uint8Array());
114
+ if (readOnly())
115
+ return bytesDigest(store, observeFile(at) ?? new Uint8Array());
113
116
  const key = `${at} ${stamp.stamp}`;
114
117
  const memo = memoOf(store);
115
118
  const held = memo.get(key);
@@ -140,10 +143,10 @@ const imported = new Map();
140
143
  const importsOf = (store, at, digest) => {
141
144
  const key = `imports ${fullDigest(store, importsOf.toString())} ${digest}`;
142
145
  const memo = memoOf(store);
143
- const held = imported.get(digest) ?? memo.get(key)?.split('|').filter(Boolean);
146
+ const held = imported.get(digest) ?? (readOnly() ? undefined : memo.get(key)?.split('|').filter(Boolean));
144
147
  if (held !== undefined)
145
148
  return held;
146
- const found = [...new TextDecoder().decode(observeFile(at) ?? new Uint8Array()).matchAll(/(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s*)['"]([^'"]*)['"]/g)].map((m) => m[1] ?? '');
149
+ const found = moduleImports(new TextDecoder().decode(observeFile(at) ?? new Uint8Array()), at);
147
150
  imported.set(digest, found);
148
151
  memo.set(key, found.join('|'));
149
152
  if (!readOnly())