@lapxo/bound 0.1.4 → 0.1.6

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 (66) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +26 -5
  3. package/TARGET.bound +84 -19
  4. package/dist/cli/accept.d.ts +5 -0
  5. package/dist/cli/accept.js +60 -14
  6. package/dist/cli/act-result.d.ts +3 -0
  7. package/dist/cli/act-result.js +48 -0
  8. package/dist/cli/owner.d.ts +2 -0
  9. package/dist/cli/owner.js +18 -2
  10. package/dist/cli/pass.js +15 -10
  11. package/dist/cli/place.js +8 -1
  12. package/dist/cli/verb.js +48 -2
  13. package/dist/fold/admission-history.d.ts +6 -0
  14. package/dist/fold/admission-history.js +22 -0
  15. package/dist/fold/claims.js +5 -1
  16. package/dist/fold/closed.js +44 -13
  17. package/dist/fold/digests.js +18 -11
  18. package/dist/fold/kept.js +9 -4
  19. package/dist/fold/keys.js +9 -6
  20. package/dist/fold/observed.js +196 -174
  21. package/dist/fold/resolved.js +6 -2
  22. package/dist/fold/signed.d.ts +5 -0
  23. package/dist/fold/signed.js +19 -15
  24. package/dist/host/act-cost.d.ts +13 -0
  25. package/dist/host/act-cost.js +25 -0
  26. package/dist/host/adapters/modules/javascript.d.ts +1 -0
  27. package/dist/host/adapters/modules/javascript.js +48 -0
  28. package/dist/host/capsule-process.js +22 -19
  29. package/dist/host/capsule.d.ts +2 -1
  30. package/dist/host/capsule.js +13 -12
  31. package/dist/host/io.d.ts +1 -1
  32. package/dist/host/io.js +1 -1
  33. package/dist/host/ports/ingress.d.ts +24 -0
  34. package/dist/host/ports/ingress.js +57 -0
  35. package/dist/host/ports/local-act.d.ts +34 -0
  36. package/dist/host/ports/local-act.js +100 -0
  37. package/dist/host/ports/publication.d.ts +10 -0
  38. package/dist/host/ports/publication.js +56 -0
  39. package/dist/host/reading-closures.d.ts +31 -0
  40. package/dist/host/reading-closures.js +146 -0
  41. package/dist/host/release.js +24 -2
  42. package/dist/host/selected-capsules.js +4 -4
  43. package/dist/host/walk.d.ts +6 -0
  44. package/dist/host/walk.js +193 -0
  45. package/dist/land/ledger.d.ts +1 -0
  46. package/dist/land/ledger.js +21 -2
  47. package/dist/land/owned.d.ts +1 -1
  48. package/dist/land/owned.js +5 -4
  49. package/dist/observe/files.js +12 -2
  50. package/dist/observe/regions.js +3 -5
  51. package/dist/render/field.d.ts +1 -7
  52. package/dist/render/field.js +6 -12
  53. package/dist/render/names.d.ts +1 -1
  54. package/dist/render/names.js +1 -1
  55. package/dist/render/view.js +16 -11
  56. package/docs/contracts/archive.md +11 -0
  57. package/docs/contracts/cli.md +19 -0
  58. package/docs/contracts/documents.md +35 -0
  59. package/docs/contracts/evidence-view.md +6 -0
  60. package/docs/contracts/publication.md +17 -0
  61. package/docs/contracts/typed-object.md +27 -0
  62. package/docs/contracts/view-walk.md +7 -0
  63. package/docs/contracts/views.md +29 -0
  64. package/docs/usage.md +6 -0
  65. package/package.json +6 -4
  66. package/receipts.bound +367 -77
@@ -1,7 +1,12 @@
1
1
  export type Batch = {
2
2
  readonly kind: 'signed';
3
3
  readonly signed: readonly string[];
4
+ readonly records?: readonly string[];
4
5
  readonly epoch: number;
6
+ readonly base: {
7
+ readonly digest: string;
8
+ readonly places: readonly string[];
9
+ };
5
10
  readonly keyId?: string;
6
11
  } | {
7
12
  readonly kind: 'once';
@@ -1,3 +1,4 @@
1
+ import { commitLocalAct, intactLocalAct, committedLocalActs } from "../host/ports/local-act.js";
1
2
  import { withoutEffects } from "../host/read-only.js";
2
3
  import { selectedWireAt } from "../host/selected-topos.js";
3
4
  import { timing } from "../host/timing.js";
@@ -6,12 +7,12 @@ import { existsSync, rmdirSync, rmSync } from 'node:fs';
6
7
  import { dirname, join, basename } from 'node:path';
7
8
  import { closeReceipts, instrumentKeep, instrumentKept } from "../fold/closed.js";
8
9
  import { bytesDigest, fullDigest, instrumentOf, signaturesOf } from "../fold/digests.js";
9
- import { canonical, fromLine, LOCK, parse } from '@lapxo/topos/wire';
10
+ import { canonical, fromLine, LOCK, parse, actIdentity, actResultProfile } from '@lapxo/topos/wire';
10
11
  import { fieldOf, isConfig, foldClaims, sayingOf, selfName } from "../fold/claims.js";
11
12
  import { isReaderLock } from "../fold/observed.js";
12
13
  import { authorityFor, publicKeyOf, rootSigner, writerFor } from "../fold/signers.js";
13
14
  import { lockLines, lockStanding } from "../fold/keys.js";
14
- import { keepVerdict, keptVerdict, land, landBlob, storeOf } from "../land/ledger.js";
15
+ import { keepVerdict, keptVerdict, land, landBlob, ledgerLines, storeOf } from "../land/ledger.js";
15
16
  import { signConsentLot, verifiesOwnerLine } from "../land/sign.js";
16
17
  import { signerFor } from "../host/signing.js";
17
18
  import { appliedIn, ownLockOf } from "../fold/signed.js";
@@ -19,7 +20,7 @@ import { wordOf } from "../fold/wire.js";
19
20
  import { hasVouchedTree, landCoordinates, locationsOf, vouchedCoordinates } from "../land/vouched.js";
20
21
  import { entriesIn, observeFile } from "../observe/files.js";
21
22
  import { coneOf, standingOf } from "./place.js";
22
- import { ownLock, ownRoot, ownStore } from "../observe/runner.js";
23
+ import { ownLock, ownRoot } from "../observe/runner.js";
23
24
  import { widenedWithoutWitness } from "../judge/widening.js";
24
25
  import { retractedTogether, withdrawal } from "../land/withdraw.js";
25
26
  import { isCeiling } from "../fold/configures.js";
@@ -36,7 +37,7 @@ import { lateFacts } from "../judge/late.js";
36
37
  import { paidOf } from "../fold/paid.js";
37
38
  import { aliasPinned } from "../land/alias.js";
38
39
  import { coordinatesWithdrawn } from "../land/leaving.js";
39
- import { ownerOf, epochOf, forksItself, forksJoinedBy, renderInto, sideOf, signedDemands, signedOwnerLines } from "./owner.js";
40
+ import { ownerOf, epochOf, localActLines, forksItself, forksJoinedBy, renderInto, sideOf, signedDemands, signedOwnerLines } from "./owner.js";
40
41
  import { claimLinesIn, entryOf, namedPlaces, placesOfFile, positionalsOf, valueOf } from "./args.js";
41
42
  import { FLAGS } from "./names.js";
42
43
  import { judgeDemand, landTake } from "./takes.js";
@@ -53,7 +54,15 @@ function admissionEvidence(root, signed, standing) {
53
54
  if (coordinates.refused.length)
54
55
  throw Error(`REFUSE·withdraw a file is still read: ${coordinates.refused.map(one => `${one.path} by ${one.by}`).join(' · ')}`);
55
56
  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 judged = signed.filter(one => isConfig(one) && fieldOf(one, 'role') === 'demands' && fieldOf(one, 'value') !== 'withdraw' && !fieldOf(one, 'scope').startsWith(view)).map(one => {
58
+ const scope = fieldOf(one, 'scope');
59
+ const alreadyDemanded = standing.some(held => isConfig(held) && fieldOf(held, 'role') === 'demands'
60
+ && fieldOf(held, 'value') !== 'withdraw' && fieldOf(held, 'scope') === scope
61
+ && fieldOf(held, 'form') === fieldOf(one, 'form') && fieldOf(held, 'measure') === fieldOf(one, 'measure'));
62
+ // Declaring an obligation is not a claim to have discharged it.
63
+ return alreadyDemanded ? judgeDemand(root, one)
64
+ : { kind: 'owed', scope, why: 'new demand declared; no conformance reading is claimed' };
65
+ });
57
66
  const refused = judged.filter(one => one.kind === 'refused' || (one.kind === 'judged' && one.verdict.kind === 'refuse'));
58
67
  if (refused.length)
59
68
  throw Error(`REFUSE·takes ${refused.map(one => `${one.scope}: ${one.kind === 'judged' ? one.verdict.why : one.kind === 'refused' ? one.why : ''}`).join(' · ')}`);
@@ -126,7 +135,9 @@ export async function signedBatch(root, args, files) {
126
135
  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
136
  return { kind: 'exit', code: 2 };
128
137
  }
129
- const epoch = epochOf([...lockLines(store), ...destinations.flatMap((place) => appliedIn(root, place))]) + 1;
138
+ const local = localActLines(root, destinations);
139
+ const base = { digest: fullDigest(store, local), places: destinations };
140
+ const epoch = epochOf(local) + 1;
130
141
  const stale = proposed.filter((line) => fieldOf(line, 'sig')).flatMap((line) => ((by, key) => (key === undefined || !verifiesOwnerLine(line, key, signaturesOf(store).admitted)
131
142
  ? [`${fieldOf(line, 'scope')} · the signature does not hold for ${by || '∅'}`]
132
143
  : []))(fieldOf(line, 'by'), publicKeyOf(root, fieldOf(line, 'by'))));
@@ -139,9 +150,13 @@ export async function signedBatch(root, args, files) {
139
150
  const [named, places] = [namedPlaces(args), placesIn(root)];
140
151
  const inLock = new Set(lockLines(store).map(sayingOf));
141
152
  const inPlace = new Map(places.map((place) => [place, new Set(appliedIn(root, place).map(sayingOf))]));
153
+ const deliveryAuthority = authorityFor(lockLines(store), rootSigner(root), signaturesOf(store).admitted);
154
+ const delivered = (line) => Boolean(fieldOf(line, 'sig'))
155
+ && deliveryAuthority.of(line).kind === 'admitted'
156
+ && ledgerLines(store, fieldOf(line, 'by')).includes(line);
142
157
  const held = (line, at = named.length ? named : byFile.flatMap(([heads, lines]) => (lines.includes(line) ? heads : []))) => at.length
143
158
  ? at.every((place) => inPlace.get(place)?.has(sayingOf(line)))
144
- : inLock.has(sayingOf(line)) || places.some((place) => inPlace.get(place)?.has(sayingOf(line)));
159
+ : delivered(line) || inLock.has(sayingOf(line)) || places.some((place) => inPlace.get(place)?.has(sayingOf(line)));
145
160
  if (proposed.every((line) => held(line))) {
146
161
  if (proposed.some(line => !isConfig(line))) {
147
162
  try {
@@ -237,7 +252,7 @@ export async function signedBatch(root, args, files) {
237
252
  process.stderr.write(`${selfName()}: ${error instanceof Error ? error.message : String(error)} · nothing landed\n`);
238
253
  return { kind: 'exit', code: 1 };
239
254
  }
240
- return { kind: 'signed', signed, epoch, ...(drafted.length ? { keyId } : {}) };
255
+ return { kind: 'signed', signed, epoch, base, ...(drafted.length ? { keyId } : {}) };
241
256
  }
242
257
  /**
243
258
  * accept is one act under the descent of its lines. Every place, then the root where they join, only when those lines
@@ -273,7 +288,7 @@ export async function accept(root, args) {
273
288
  const where = every ? 'every place · then the root, where the places join' : regions.length ? regions.map((one) => `${one}/`).join(' · ') : 'the root alone, where the places join';
274
289
  process.stdout.write(`ACTS ${where} · ${owned.ledger.length} lines to the ledger${[...owned.own].map(([place, lines]) => ` · ${lines.length} to ${place}'s own lock`).join('')} · before the lock\n`);
275
290
  const asked = Date.now();
276
- return underTheRegions(store, every ? [...regions, ''] : regions, 'land', () => acceptNow(root, { ...batch, signed: owned.ledger }, started, owned, asked, regions));
291
+ return underTheRegions(store, every ? [...regions, ''] : regions, 'land', () => acceptNow(root, { ...batch, signed: owned.ledger, records: batch.signed }, started, owned, asked, regions));
277
292
  }
278
293
  async function acceptNow(root, batch, started, owned, asked = started, descent = []) {
279
294
  const waited = Date.now() - asked;
@@ -283,6 +298,11 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
283
298
  const took = timing(spent);
284
299
  const { signed, epoch } = batch;
285
300
  const store = storeOf(root);
301
+ const currentBase = fullDigest(store, localActLines(root, batch.base.places));
302
+ if (currentBase !== batch.base.digest) {
303
+ process.stderr.write(`${selfName()}: REFUSE·base moved ${batch.base.digest} → ${currentBase} · nothing landed\n`);
304
+ return 1;
305
+ }
286
306
  if (signed.some(line => !isConfig(line))) {
287
307
  try {
288
308
  await admittedObjects(root, [...signedOwnerLines(store), ...signed]);
@@ -335,10 +355,32 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
335
355
  for (const name of openedBy(descent, told.standing, signed))
336
356
  process.stderr.write(`OPEN ${name}\n`);
337
357
  if (owned.own.size)
338
- layReleases(store, ownRoot(), ownLock(), ownStore());
339
- const landed = took('land', () => [...new Set(signed.map((line) => fieldOf(line, 'by')))].map((by) => land(store, by, signed.filter((line) => fieldOf(line, 'by') === by))));
358
+ layReleases(store, root, told.standing, store);
359
+ const resultProfile = actResultProfile(snapshot.standing);
360
+ let landed = [];
361
+ if (resultProfile) {
362
+ const records = batch.records ?? signed;
363
+ const placements = [...(signed.length ? [{ place: '.', records: signed }] : []), ...[...owned.own].map(([place, records]) => ({ place: `${place}/${LOCK}`, records }))];
364
+ const digest = (bytes) => bytesDigest(store, new TextEncoder().encode(bytes));
365
+ const identity = actIdentity(records, { ...resultProfile, digest, placements, localEpoch: epoch });
366
+ const folder = writerFor(snapshot.standing, 'fold');
367
+ if (!folder)
368
+ throw Error('REFUSE·act native result requires a declared fold identity');
369
+ const receipt = canonical({ scope: resultProfile.scope, at: resultProfile.context, role: 'writes', form: 'alphabet', measure: 'digest', value: identity, by: folder, epoch: String(epoch) });
370
+ const bundle = { records: records.join('\n') + '\n', receipt: receipt + '\n', placements: placements.map(p => ({ place: p.place, records: p.records.join('\n') + '\n' })) };
371
+ await commitLocalAct(store, identity, descent, async () => bundle, intactLocalAct);
372
+ // Commitment precedes projections and conformance work. If either fails,
373
+ // this public identity still names the admitted act; it is never rollback.
374
+ process.stdout.write(`ACT ${identity} · epoch ${epoch} · admitted · conformance not asserted\n`);
375
+ for (const line of records)
376
+ process.stdout.write(line + '\n');
377
+ process.stdout.write(receipt + '\n');
378
+ landed = [{ at: 'committed act ' + identity, appended: records.length }];
379
+ }
380
+ else
381
+ landed = took('land', () => [...new Set(signed.map(line => fieldOf(line, 'by')))].map(by => land(store, by, signed.filter(line => fieldOf(line, 'by') === by))));
340
382
  for (const [place, lines] of owned.own)
341
- landOwned(root, place, lines);
383
+ landOwned(root, place, lines, resultProfile !== undefined);
342
384
  landSaid(store, writerFor(told.standing, 'fold'), []);
343
385
  for (const line of signed) {
344
386
  const value = fieldOf(line, 'value');
@@ -392,7 +434,7 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
392
434
  if (folder !== undefined)
393
435
  land(store, folder, [beat(perLine, 'line'), beat(1, fold.kept ? 'kept' : 'folded'), beat(Date.now() - started, 'total')]);
394
436
  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`
437
+ + `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
438
  + `SPENT ${spent.join(' ms · ')} ms · ${signed.length} lines\n`);
397
439
  if (withdrawn) {
398
440
  process.stdout.write(`WITHDRAW ${withdrawn.taken.length} taken · ${withdrawn.exact.length} exact on distributive scales · ${withdrawn.open.length} on open scales, not computed\n`);
@@ -407,6 +449,10 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
407
449
  */
408
450
  function repaid(root, proposed, started) {
409
451
  const store = storeOf(root);
452
+ const prior = committedLocalActs(store).find(act => { const lines = act.bundle.records.trimEnd().split('\n'); return lines.length === proposed.length && lines.every((line, i) => line === proposed[i]); });
453
+ if (prior) {
454
+ process.stdout.write(`ACT ${prior.identity} · epoch ${fieldOf(prior.bundle.receipt.trim(), 'epoch')} · admitted · replay\n` + prior.bundle.records + prior.bundle.receipt);
455
+ }
410
456
  const asked = new Set(proposed.filter((line) => fieldOf(line, 'role') === 'demands').map((line) => fieldOf(line, 'scope')));
411
457
  const view = `${wordOf(foldClaims(signedOwnerLines(store)).standing, 'families', 'view')}/`;
412
458
  const standingAsked = signedDemands(store).filter((line) => asked.has(fieldOf(line, 'scope')) && !fieldOf(line, 'scope').startsWith(view));
@@ -444,7 +490,7 @@ function repaid(root, proposed, started) {
444
490
  }
445
491
  const rendered = renderInto(root, store);
446
492
  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`);
493
+ 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
494
  return 0;
449
495
  }
450
496
  function packTake(one) {
@@ -0,0 +1,3 @@
1
+ /** Public query of a committed identity, using the same admission authority.
2
+ * Native fold evidence has store provenance; it never grants coverage. */
3
+ export declare function printActResult(root: string, identity: string | undefined): void;
@@ -0,0 +1,48 @@
1
+ import { actResultOf, actResultProfile } from '@lapxo/topos/wire';
2
+ import { actLines, intactLocalAct, localActResult } from "../host/ports/local-act.js";
3
+ import { storeOf } from "../land/ledger.js";
4
+ import { lockStanding } from "../fold/keys.js";
5
+ import { authorityFor, writerFor } from "../fold/signers.js";
6
+ import { eraOf } from "../fold/digests.js";
7
+ import { fieldOf, isConfig, foldClaims } from "../fold/claims.js";
8
+ import { ownRecordAdmitted } from "../fold/signed.js";
9
+ import { admissionAnchor, admissionHistoryAt } from "../fold/admission-history.js";
10
+ import { fullDigest } from "../host/digest.js";
11
+ import { wordOf, wireLine } from "../fold/wire.js";
12
+ import { viewsOf } from "../fold/views.js";
13
+ /** Public query of a committed identity, using the same admission authority.
14
+ * Native fold evidence has store provenance; it never grants coverage. */
15
+ export function printActResult(root, identity) {
16
+ const store = storeOf(root), standing = lockStanding(store);
17
+ const view = viewsOf(standing).get('act');
18
+ if (!view || view.shape || !view.regions.some(region => region.name === 'act'))
19
+ throw Error('REFUSE·view act requires an admitted result profile and act view');
20
+ if (!identity)
21
+ throw Error('REFUSE·act name one committed identity');
22
+ const bundle = localActResult(store, identity, intactLocalAct);
23
+ if (!bundle)
24
+ throw Error('REFUSE·act no committed result ' + identity);
25
+ const records = actLines(bundle.records), epoch = Number(fieldOf(bundle.receipt.trim(), 'epoch'));
26
+ const history = admissionHistoryAt(root, epoch), before = history.filter(line => !isConfig(line) || Number(fieldOf(line, 'epoch')) < epoch);
27
+ const algorithms = eraOf(foldClaims(before).standing).admitted;
28
+ const authority = authorityFor(history, admissionAnchor(root), algorithms);
29
+ const admittedBefore = foldClaims(before.filter(line => authority.of(line).kind === 'admitted')).standing;
30
+ const profile = actResultProfile(admittedBefore);
31
+ if (!profile)
32
+ throw Error('REFUSE·act no result profile at admission');
33
+ const family = authority.admitted.some(signer => !signer.coverage.includes('*')) ? wordOf(admittedBefore, 'families', 'region') : '';
34
+ const algorithm = identity.slice(0, identity.indexOf(':'));
35
+ const named = fieldOf(wireLine(admittedBefore, 'digest-algorithms') ?? '', 'value').split('|').map(value => value.split(':')[0]);
36
+ if (!named.includes(algorithm))
37
+ throw Error('REFUSE·act digest algorithm was not admitted at commitment');
38
+ const result = actResultOf(records, bundle.receipt.trim(), { ...profile, localEpoch: epoch,
39
+ digest: bytes => fullDigest(bytes, algorithm),
40
+ placements: bundle.placements.map(p => ({ place: p.place, records: actLines(p.records) })),
41
+ admitsRecord: line => {
42
+ if (isConfig(line) && Number(fieldOf(line, 'epoch')) !== epoch)
43
+ return false;
44
+ return bundle.placements.some(p => actLines(p.records).includes(line) && (p.place === '.' ? authority.of(line).kind === 'admitted' : ownRecordAdmitted(line, p.place.slice(0, p.place.lastIndexOf('/')), family, authority.admitted, algorithms)));
45
+ },
46
+ verifiesReceipt: line => fieldOf(line, 'by') === writerFor(admittedBefore, 'fold') && intactLocalAct(identity, bundle), });
47
+ process.stdout.write(`ACT ${result.identity} · epoch ${result.epoch} · admitted · conformance not asserted\n` + bundle.records + bundle.receipt);
48
+ }
@@ -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,11 @@
1
+ import { committedLocalActs } from "../host/ports/local-act.js";
2
+ import { fileURLToPath } from 'node:url';
3
+ import { ingressReceipts } from "../host/ports/ingress.js";
1
4
  import { readOnly } from "../host/read-only.js";
2
5
  import { keyFor, lockLines, releaseOf, signedLockLines } from "../fold/keys.js";
3
6
  import { existsSync, renameSync, writeFileSync } from 'node:fs';
4
7
  import { join, resolve } from 'node:path';
5
- import { bytesDigest, fullDigest, memoized, signaturesOf } from "../fold/digests.js";
8
+ import { bytesDigest, fullDigest, memoized, signaturesOf, instrumentDigest } from "../fold/digests.js";
6
9
  import { claimKey, isConfig, fieldOf, foldClaims, LOCK, selfName } from "../fold/claims.js";
7
10
  import { asksThePlace } from "../fold/configures.js";
8
11
  import { receiptsIn } from "../fold/receipts.js";
@@ -10,6 +13,7 @@ import { authorityFor, releaseSigner, rootSigner } from "../fold/signers.js";
10
13
  import { storeRoot } from "../host/ports/store.js";
11
14
  import { blobAt, landBlob, ledgerLines, storeOf } from "../land/ledger.js";
12
15
  import { observeText } from "../observe/files.js";
16
+ import { appliedHistoryIn } from "../fold/signed.js";
13
17
  import { everLanded, inside, locationsOf } from "../land/vouched.js";
14
18
  import { renderTarget } from "../render/target.js";
15
19
  export const ownerOf = (store, told = []) => keyFor(store, 'authorize', told);
@@ -47,6 +51,18 @@ export function forkedScopes(store) {
47
51
  const lines = signedOwnerLines(store);
48
52
  return new Set(foldClaims(lines, forkReceipts(store, lines)).forks.flatMap((f) => f.lines.map((line) => fieldOf(line, 'scope'))));
49
53
  }
54
+ /** Import receipts are local acts; foreign evidence and sender epochs never enter this input. */
55
+ export function localActLines(root, placed = []) {
56
+ const store = storeOf(root), local = lockLines(store), receipts = ingressReceipts(store);
57
+ if (receipts.length) {
58
+ const authority = authorityFor(local, rootSigner(root), signaturesOf(store).admitted);
59
+ for (const receipt of receipts) {
60
+ if (fieldOf(receipt, 'scope') !== 'receipts' || fieldOf(receipt, 'role') !== 'writes' || fieldOf(receipt, 'form') !== 'alphabet' || fieldOf(receipt, 'measure') !== 'digest' || authority.of(receipt).kind !== 'admitted')
61
+ throw Error('REFUSE·ingress local import receipt is not admitted');
62
+ }
63
+ }
64
+ return [...local, ...receipts, ...committedLocalActs(store).map(act => act.bundle.receipt.trim()), ...placed.flatMap(place => appliedHistoryIn(root, place))];
65
+ }
50
66
  export function epochOf(lines) {
51
67
  return lines.reduce((top, line) => Math.max(top, Number(fieldOf(line, 'epoch')) || 0), 0);
52
68
  }
@@ -92,7 +108,7 @@ export function renderedLock(root, store, told = []) {
92
108
  }
93
109
  const anchor = [join(storeRoot(root), `${selfName()}.keys`), join(storeOf(root), `${selfName()}.keys`)].map((at) => observeText(at) ?? '').join('\n');
94
110
  const judged = ledgerLines(store, 'judge');
95
- const key = `lock ${fullDigest(store, [new Date().toISOString().slice(0, 10), anchor, `${judged.length} ${judged.at(-1) ?? ''}`, ...owned])}`;
111
+ 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
112
  const held = memoized(store, key, () => {
97
113
  const bytes = new TextEncoder().encode(JSON.stringify(render()));
98
114
  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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { printActResult } from "./act-result.js";
2
3
  import { withContentStore } from "../host/content-store.js";
3
4
  import { withoutEffects } from "../host/read-only.js";
4
5
  import { resolveSources, resolveRequested } from "../host/source-content.js";
@@ -10,6 +11,7 @@ import { objectFold } from "../fold/object.js";
10
11
  import { OBJECT_VIEW } from '@lapxo/topos/wire';
11
12
  import { toposStanding } from '@lapxo/topos/standing';
12
13
  import { wireAt, parse as parseWire } from '@lapxo/topos/wire';
14
+ import { lockStanding } from "../fold/keys.js";
13
15
  import { ownLock } from "../observe/runner.js";
14
16
  import { signedOwnerLines } from "./owner.js";
15
17
  import { existsSync, lstatSync, mkdirSync, writeFileSync } from 'node:fs';
@@ -42,6 +44,9 @@ import { ownerOf, renderInto, signedDemands, targetFile } from "./owner.js";
42
44
  import { claimLinesIn, entryOf, namedPlaces, positionalsOf, valueOf } from "./args.js";
43
45
  import { EXIT, FLAGS, NAMED_VIEWS, helpLines } from "./names.js";
44
46
  import { accept, signedBatch } from "./accept.js";
47
+ import { renderWalk, landWalk, foreignWalkLines } from "../host/walk.js";
48
+ import { isWalkHeader } from '@lapxo/topos/walk';
49
+ import { ingressBundles, ingressReceipts } from "../host/ports/ingress.js";
45
50
  /** The first land of a lock: TARGET.bound into an empty owner ledger, then the sources that lock names. */
46
51
  async function firstLand(root, args) {
47
52
  const started = Date.now();
@@ -184,6 +189,23 @@ async function view(root, under, asked, told = [], coord, key, includePolicy = t
184
189
  process.stderr.write(`${selfName()}: the owner ledger is empty; land it before rendering a view\n`);
185
190
  return EXIT.refuse;
186
191
  }
192
+ if (name === 'act') {
193
+ if (under || told.length)
194
+ throw Error('REFUSE·act committed identity query only');
195
+ printActResult(root, coord);
196
+ return EXIT.closed;
197
+ }
198
+ if (name === 'evidence' && !under && !told.length && ingressBundles(storeOf(root)).length) {
199
+ const declared = viewsOf(lockStanding(storeOf(root))).get(name);
200
+ if (!declared || declared.shape || !declared.regions.some(region => region.name === 'evidence'))
201
+ throw Error('REFUSE·view foreign evidence requires a declared evidence view');
202
+ const received = await foreignWalkLines(root), authority = authorityFor(lockLines(storeOf(root)), rootSigner(root), signaturesOf(storeOf(root)).admitted);
203
+ const local = [...new Set(authority.admitted.flatMap(key => ledgerLines(storeOf(root), key.id)))].filter(line => authority.of(line).kind === 'admitted');
204
+ for (const line of [...new Set([...local, ...ingressReceipts(storeOf(root)), ...received])])
205
+ process.stdout.write(line + '\n');
206
+ process.stdout.write('FACT authenticated histories · readings · no encounters computed\n');
207
+ return EXIT.closed;
208
+ }
187
209
  const at = Date.now();
188
210
  const place = under?.replace(/\/$/, '');
189
211
  const released = releaseOf(storeOf(root));
@@ -367,6 +389,12 @@ async function landVerb(root, rest) {
367
389
  process.once('uncaughtException', (error) => die(error instanceof Error ? error.message : String(error)));
368
390
  process.once('unhandledRejection', (error) => die(error instanceof Error ? error.message : String(error)));
369
391
  try {
392
+ const proposed = batch.flatMap(claimLinesIn);
393
+ if (proposed.some(isWalkHeader)) {
394
+ for (const line of await landWalk(root, proposed, valueOf(rest, FLAGS.key), valueOf(rest, FLAGS.keyFile), valueOf(rest, FLAGS.signer)))
395
+ process.stdout.write(line + '\n');
396
+ return EXIT.closed;
397
+ }
370
398
  return await accept(root, rest);
371
399
  }
372
400
  catch (error) {
@@ -395,7 +423,7 @@ function foldFile(at) {
395
423
  process.stdout.write(`${line}\n`);
396
424
  for (const fork of folded.forks)
397
425
  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`);
426
+ process.stdout.write(`FACT ${folded.standing.length} standing · ${folded.forks.length} forks · ${basename(at)} · readings · no encounters computed\n`);
399
427
  return EXIT.closed;
400
428
  }
401
429
  function foldVerb(root, rest) {
@@ -419,7 +447,7 @@ async function foldAt(root, under, argv) {
419
447
  async function foldRequested(root, under, argv) {
420
448
  const as = valueOf(argv, FLAGS.as);
421
449
  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))
450
+ if (as?.split('@')[0] === 'act' || as === NAMED_VIEWS.help || prospective || argv.includes(FLAGS.check))
423
451
  return foldResolved(root, under, argv);
424
452
  return resolveRequested(digest => resolveSources(standingOf(root, under).standing, [digest]), () => foldResolved(root, under, argv));
425
453
  }
@@ -429,6 +457,19 @@ async function foldResolved(root, under, argv) {
429
457
  // Public content is authenticated by its digest, independently of owner history.
430
458
  // A named view does not request mounting every release of the instrument.
431
459
  const receiptCheck = as === undefined && argv.includes(FLAGS.check);
460
+ if (as?.split('@')[0] === 'walk') {
461
+ if (argv.includes(FLAGS.check))
462
+ throw Error('REFUSE·walk check does not initiate or sign an exchange');
463
+ if (under || inputs.length > 1)
464
+ throw Error('REFUSE·walk one place and one authenticated peer inventory per fold');
465
+ const depth = as.split('@')[1];
466
+ if (depth !== undefined && (!/^(0|[1-9][0-9]*)$/.test(depth) || !Number.isSafeInteger(Number(depth))))
467
+ throw Error('REFUSE·walk invalid resolution');
468
+ const peer = inputs.length ? claimLinesIn(inputs[0]) : [];
469
+ 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)))
470
+ process.stdout.write(line + '\n');
471
+ return EXIT.closed;
472
+ }
432
473
  if (as === OBJECT_VIEW) {
433
474
  const inputs = positionalsOf(argv).filter(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
434
475
  if (inputs.length > 1)
@@ -463,6 +504,11 @@ async function foldResolved(root, under, argv) {
463
504
  }
464
505
  const named = inputs;
465
506
  const coord = positionalsOf(argv).find((one) => one !== under?.replace(/\/$/, '') && !one.startsWith('-') && !(existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory()));
507
+ if (as === 'act') {
508
+ if (positionalsOf(argv).length !== 1)
509
+ throw Error('REFUSE·act name one committed identity');
510
+ return view(root, under, as, [], coord);
511
+ }
466
512
  if ((as === 'why' || as === NAMED_VIEWS.because || as.split('@')[0] === 'why') && coord !== undefined)
467
513
  return view(root, under, as === NAMED_VIEWS.because ? 'why' : as, [], coord, key);
468
514
  if (!named.length)
@@ -0,0 +1,6 @@
1
+ import type { Signer } from '@lapxo/topos/wire';
2
+ /** Local admission history only. Foreign ingress is evidence, never coverage.
3
+ * The clock cutoff is meaningful for configuration; object clocks stay intact. */
4
+ export declare function admissionHistoryAt(root: string, epoch: number): readonly string[];
5
+ /** The existing bootstrap's public root of trust, not today's key selection. */
6
+ export declare function admissionAnchor(root: string): Signer;
@@ -0,0 +1,22 @@
1
+ import { join } from 'node:path';
2
+ import { readFileSync } from 'node:fs';
3
+ import { ledgerLines, ledgerWriters, storeOf } from "../land/ledger.js";
4
+ import { fieldOf, isConfig, selfName } from "./claims.js";
5
+ /** Local admission history only. Foreign ingress is evidence, never coverage.
6
+ * The clock cutoff is meaningful for configuration; object clocks stay intact. */
7
+ export function admissionHistoryAt(root, epoch) {
8
+ const store = storeOf(root);
9
+ return [...new Set(ledgerWriters(store).flatMap(writer => ledgerLines(store, writer)))]
10
+ .filter(line => fieldOf(line, 'sig') !== '' && (!isConfig(line) || Number(fieldOf(line, 'epoch')) <= epoch));
11
+ }
12
+ /** The existing bootstrap's public root of trust, not today's key selection. */
13
+ export function admissionAnchor(root) {
14
+ const at = join(storeOf(root), `${selfName()}.keys`);
15
+ const keys = JSON.parse(readFileSync(at, 'utf8'));
16
+ const anchors = Object.values(keys);
17
+ if (anchors.length !== 1 || !anchors[0]?.publicKey || !anchors[0].coverage.length)
18
+ throw Error('REFUSE·act historical admission requires one bootstrap anchor');
19
+ const anchor = anchors[0];
20
+ return { id: anchor.id, keyClass: 'authorize', publicKey: anchor.publicKey, coverage: anchor.coverage,
21
+ depth: { lo: 1, hi: 16 }, admittedBy: [] };
22
+ }
@@ -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
  }