@lapxo/bound 0.1.5 → 0.1.7

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 (56) hide show
  1. package/README.md +8 -8
  2. package/TARGET.bound +19 -18
  3. package/dist/cli/accept.d.ts +5 -0
  4. package/dist/cli/accept.js +57 -8
  5. package/dist/cli/act-result.d.ts +3 -0
  6. package/dist/cli/act-result.js +48 -0
  7. package/dist/cli/owner.js +3 -2
  8. package/dist/cli/pass.js +6 -4
  9. package/dist/cli/place.d.ts +1 -1
  10. package/dist/cli/place.js +1 -1
  11. package/dist/cli/release-result.d.ts +5 -0
  12. package/dist/cli/release-result.js +80 -0
  13. package/dist/cli/standing-result.d.ts +5 -0
  14. package/dist/cli/standing-result.js +39 -0
  15. package/dist/cli/verb.js +33 -2
  16. package/dist/fold/admission-history.d.ts +6 -0
  17. package/dist/fold/admission-history.js +22 -0
  18. package/dist/fold/closed.js +9 -3
  19. package/dist/fold/digests.js +13 -9
  20. package/dist/fold/kept.d.ts +1 -1
  21. package/dist/fold/kept.js +21 -6
  22. package/dist/fold/keys.js +16 -9
  23. package/dist/fold/observed.js +8 -7
  24. package/dist/fold/places.d.ts +5 -1
  25. package/dist/fold/places.js +19 -3
  26. package/dist/fold/readers.js +2 -1
  27. package/dist/fold/release-policy.js +0 -2
  28. package/dist/fold/signed.d.ts +5 -0
  29. package/dist/fold/signed.js +19 -15
  30. package/dist/fold/zoom.js +5 -4
  31. package/dist/host/capsule.d.ts +2 -1
  32. package/dist/host/capsule.js +8 -6
  33. package/dist/host/cell-receipts.js +7 -3
  34. package/dist/host/io.d.ts +1 -1
  35. package/dist/host/io.js +1 -1
  36. package/dist/host/ports/ingress.js +4 -28
  37. package/dist/host/ports/local-act.d.ts +34 -0
  38. package/dist/host/ports/local-act.js +100 -0
  39. package/dist/host/ports/publication.d.ts +10 -0
  40. package/dist/host/ports/publication.js +56 -0
  41. package/dist/host/reading-closures.d.ts +3 -0
  42. package/dist/host/reading-closures.js +5 -4
  43. package/dist/host/selected-capsules.js +4 -4
  44. package/dist/host/selected-topos.d.ts +4 -1
  45. package/dist/host/selected-topos.js +16 -1
  46. package/dist/land/ledger.d.ts +1 -0
  47. package/dist/land/ledger.js +21 -2
  48. package/dist/land/owned.d.ts +1 -1
  49. package/dist/land/owned.js +5 -4
  50. package/dist/land/standing-pin.d.ts +3 -0
  51. package/dist/land/standing-pin.js +49 -0
  52. package/dist/observe/files.js +12 -2
  53. package/docs/contracts/cli.md +16 -0
  54. package/docs/contracts/evidence-view.md +4 -0
  55. package/package.json +2 -2
  56. package/receipts.bound +593 -314
@@ -1,12 +1,13 @@
1
+ import { commitLocalAct, intactLocalAct, committedLocalActs } from "../host/ports/local-act.js";
1
2
  import { withoutEffects } from "../host/read-only.js";
2
- import { selectedWireAt } from "../host/selected-topos.js";
3
+ import { admittedLine } from "../host/selected-topos.js";
3
4
  import { timing } from "../host/timing.js";
4
5
  import { admittedObjects } from "../host/objects.js";
5
6
  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, 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";
@@ -35,6 +36,7 @@ import { outsideTheirCapsule } from "../fold/own.js";
35
36
  import { lateFacts } from "../judge/late.js";
36
37
  import { paidOf } from "../fold/paid.js";
37
38
  import { aliasPinned } from "../land/alias.js";
39
+ import { admitStandingPins } from "../land/standing-pin.js";
38
40
  import { coordinatesWithdrawn } from "../land/leaving.js";
39
41
  import { ownerOf, epochOf, localActLines, forksItself, forksJoinedBy, renderInto, sideOf, signedDemands, signedOwnerLines } from "./owner.js";
40
42
  import { claimLinesIn, entryOf, namedPlaces, placesOfFile, positionalsOf, valueOf } from "./args.js";
@@ -110,6 +112,16 @@ function refusesAdmission(root, signed, standing) {
110
112
  }
111
113
  return false;
112
114
  }
115
+ function refusesStandingPins(root, lines) {
116
+ try {
117
+ admitStandingPins(root, lines);
118
+ return false;
119
+ }
120
+ catch (error) {
121
+ process.stderr.write(`${selfName()}: ${error instanceof Error ? error.message : String(error)}\n`);
122
+ return true;
123
+ }
124
+ }
113
125
  /** 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. */
114
126
  export async function signedBatch(root, args, files) {
115
127
  const byFile = files.map((file) => [placesOfFile(file), claimLinesIn(file)]);
@@ -134,7 +146,9 @@ export async function signedBatch(root, args, files) {
134
146
  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`);
135
147
  return { kind: 'exit', code: 2 };
136
148
  }
137
- const epoch = epochOf(localActLines(root, destinations)) + 1;
149
+ const local = localActLines(root, destinations);
150
+ const base = { digest: fullDigest(store, local), places: destinations };
151
+ const epoch = epochOf(local) + 1;
138
152
  const stale = proposed.filter((line) => fieldOf(line, 'sig')).flatMap((line) => ((by, key) => (key === undefined || !verifiesOwnerLine(line, key, signaturesOf(store).admitted)
139
153
  ? [`${fieldOf(line, 'scope')} · the signature does not hold for ${by || '∅'}`]
140
154
  : []))(fieldOf(line, 'by'), publicKeyOf(root, fieldOf(line, 'by'))));
@@ -203,12 +217,14 @@ export async function signedBatch(root, args, files) {
203
217
  return got.kind === 'fact' ? [got.value.fields] : [];
204
218
  });
205
219
  for (const line of signed) {
206
- const got = fromLine(line, selectedWireAt(wireLines, Number(fieldOf(line, 'epoch')), foldClaims(signedOwnerLines(store)).standing.filter(one => fieldOf(one, 'scope').startsWith('uses/') && fieldOf(one, 'value') !== 'withdraw')));
220
+ const got = admittedLine(line, wireLines, Number(fieldOf(line, 'epoch')), foldClaims(signedOwnerLines(store)).standing.filter(one => fieldOf(one, 'scope').startsWith('uses/') && fieldOf(one, 'value') !== 'withdraw'));
207
221
  if (got.kind !== 'fact') {
208
222
  process.stderr.write(`${selfName()}: REFUSE·wire ${fieldOf(line, 'scope')} · ${got.why} · nothing landed\n`);
209
223
  return { kind: 'exit', code: 1 };
210
224
  }
211
225
  }
226
+ if (refusesStandingPins(root, signed))
227
+ return { kind: 'exit', code: 1 };
212
228
  const taken = retractedTogether(signed);
213
229
  if (taken.length) {
214
230
  for (const pair of taken) {
@@ -249,7 +265,7 @@ export async function signedBatch(root, args, files) {
249
265
  process.stderr.write(`${selfName()}: ${error instanceof Error ? error.message : String(error)} · nothing landed\n`);
250
266
  return { kind: 'exit', code: 1 };
251
267
  }
252
- return { kind: 'signed', signed, epoch, ...(drafted.length ? { keyId } : {}) };
268
+ return { kind: 'signed', signed, epoch, base, ...(drafted.length ? { keyId } : {}) };
253
269
  }
254
270
  /**
255
271
  * accept is one act under the descent of its lines. Every place, then the root where they join, only when those lines
@@ -285,7 +301,7 @@ export async function accept(root, args) {
285
301
  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';
286
302
  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`);
287
303
  const asked = Date.now();
288
- return underTheRegions(store, every ? [...regions, ''] : regions, 'land', () => acceptNow(root, { ...batch, signed: owned.ledger }, started, owned, asked, regions));
304
+ return underTheRegions(store, every ? [...regions, ''] : regions, 'land', () => acceptNow(root, { ...batch, signed: owned.ledger, records: batch.signed }, started, owned, asked, regions));
289
305
  }
290
306
  async function acceptNow(root, batch, started, owned, asked = started, descent = []) {
291
307
  const waited = Date.now() - asked;
@@ -294,7 +310,14 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
294
310
  const spent = [`sign ${asked - started}`, `wait ${waited}`];
295
311
  const took = timing(spent);
296
312
  const { signed, epoch } = batch;
313
+ if (refusesStandingPins(root, batch.records ?? signed))
314
+ return 1;
297
315
  const store = storeOf(root);
316
+ const currentBase = fullDigest(store, localActLines(root, batch.base.places));
317
+ if (currentBase !== batch.base.digest) {
318
+ process.stderr.write(`${selfName()}: REFUSE·base moved ${batch.base.digest} → ${currentBase} · nothing landed\n`);
319
+ return 1;
320
+ }
298
321
  if (signed.some(line => !isConfig(line))) {
299
322
  try {
300
323
  await admittedObjects(root, [...signedOwnerLines(store), ...signed]);
@@ -348,9 +371,31 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
348
371
  process.stderr.write(`OPEN ${name}\n`);
349
372
  if (owned.own.size)
350
373
  layReleases(store, root, told.standing, store);
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))));
374
+ const resultProfile = actResultProfile(snapshot.standing);
375
+ let landed = [];
376
+ if (resultProfile) {
377
+ const records = batch.records ?? signed;
378
+ const placements = [...(signed.length ? [{ place: '.', records: signed }] : []), ...[...owned.own].map(([place, records]) => ({ place: `${place}/${LOCK}`, records }))];
379
+ const digest = (bytes) => bytesDigest(store, new TextEncoder().encode(bytes));
380
+ const identity = actIdentity(records, { ...resultProfile, digest, placements, localEpoch: epoch });
381
+ const folder = writerFor(snapshot.standing, 'fold');
382
+ if (!folder)
383
+ throw Error('REFUSE·act native result requires a declared fold identity');
384
+ const receipt = canonical({ scope: resultProfile.scope, at: resultProfile.context, role: 'writes', form: 'alphabet', measure: 'digest', value: identity, by: folder, epoch: String(epoch) });
385
+ const bundle = { records: records.join('\n') + '\n', receipt: receipt + '\n', placements: placements.map(p => ({ place: p.place, records: p.records.join('\n') + '\n' })) };
386
+ await commitLocalAct(store, identity, descent, async () => bundle, intactLocalAct);
387
+ // Commitment precedes projections and conformance work. If either fails,
388
+ // this public identity still names the admitted act; it is never rollback.
389
+ process.stdout.write(`ACT ${identity} · epoch ${epoch} · admitted · conformance not asserted\n`);
390
+ for (const line of records)
391
+ process.stdout.write(line + '\n');
392
+ process.stdout.write(receipt + '\n');
393
+ landed = [{ at: 'committed act ' + identity, appended: records.length }];
394
+ }
395
+ else
396
+ landed = took('land', () => [...new Set(signed.map(line => fieldOf(line, 'by')))].map(by => land(store, by, signed.filter(line => fieldOf(line, 'by') === by))));
352
397
  for (const [place, lines] of owned.own)
353
- landOwned(root, place, lines);
398
+ landOwned(root, place, lines, resultProfile !== undefined);
354
399
  landSaid(store, writerFor(told.standing, 'fold'), []);
355
400
  for (const line of signed) {
356
401
  const value = fieldOf(line, 'value');
@@ -419,6 +464,10 @@ async function acceptNow(root, batch, started, owned, asked = started, descent =
419
464
  */
420
465
  function repaid(root, proposed, started) {
421
466
  const store = storeOf(root);
467
+ 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]); });
468
+ if (prior) {
469
+ process.stdout.write(`ACT ${prior.identity} · epoch ${fieldOf(prior.bundle.receipt.trim(), 'epoch')} · admitted · replay\n` + prior.bundle.records + prior.bundle.receipt);
470
+ }
422
471
  const asked = new Set(proposed.filter((line) => fieldOf(line, 'role') === 'demands').map((line) => fieldOf(line, 'scope')));
423
472
  const view = `${wordOf(foldClaims(signedOwnerLines(store)).standing, 'families', 'view')}/`;
424
473
  const standingAsked = signedDemands(store).filter((line) => asked.has(fieldOf(line, 'scope')) && !fieldOf(line, 'scope').startsWith(view));
@@ -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
+ }
package/dist/cli/owner.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { committedLocalActs } from "../host/ports/local-act.js";
1
2
  import { fileURLToPath } from 'node:url';
2
3
  import { ingressReceipts } from "../host/ports/ingress.js";
3
4
  import { readOnly } from "../host/read-only.js";
@@ -12,7 +13,7 @@ import { authorityFor, releaseSigner, rootSigner } from "../fold/signers.js";
12
13
  import { storeRoot } from "../host/ports/store.js";
13
14
  import { blobAt, landBlob, ledgerLines, storeOf } from "../land/ledger.js";
14
15
  import { observeText } from "../observe/files.js";
15
- import { appliedIn } from "../fold/signed.js";
16
+ import { appliedHistoryIn } from "../fold/signed.js";
16
17
  import { everLanded, inside, locationsOf } from "../land/vouched.js";
17
18
  import { renderTarget } from "../render/target.js";
18
19
  export const ownerOf = (store, told = []) => keyFor(store, 'authorize', told);
@@ -60,7 +61,7 @@ export function localActLines(root, placed = []) {
60
61
  throw Error('REFUSE·ingress local import receipt is not admitted');
61
62
  }
62
63
  }
63
- return [...local, ...receipts, ...placed.flatMap(place => appliedIn(root, place))];
64
+ return [...local, ...receipts, ...committedLocalActs(store).map(act => act.bundle.receipt.trim()), ...placed.flatMap(place => appliedHistoryIn(root, place))];
64
65
  }
65
66
  export function epochOf(lines) {
66
67
  return lines.reduce((top, line) => Math.max(top, Number(fieldOf(line, 'epoch')) || 0), 0);
package/dist/cli/pass.js CHANGED
@@ -13,12 +13,12 @@ import { algorithmCost, instrumentOf, instrumentDigest } from "../fold/digests.j
13
13
  import { writerFor } from "../fold/signers.js";
14
14
  import { defaulted, placesOf, viewsOf } from "../fold/views.js";
15
15
  import { ownLockOf } from "../fold/signed.js";
16
- import { worldAt, worldsIn } from "../fold/places.js";
16
+ import { localDirectoriesIn, worldAt, worldsIn } from "../fold/places.js";
17
17
  import { wordsOf } from "../fold/wire.js";
18
18
  import { land, ledgerLines, replaceWhole, storeOf } from "../land/ledger.js";
19
19
  import { holdRoot, holds, underTheLock } from "../land/act.js";
20
20
  import { emptyLedger, sayStranger } from "../fold/stranger.js";
21
- import { entriesIn, observeFile, observeText } from "../observe/files.js";
21
+ import { observeFile, observeText } from "../observe/files.js";
22
22
  import { render, renderedOf, unrendered, viewOf } from "../render/view.js";
23
23
  import { releasePolicyLines } from "../fold/release-policy.js";
24
24
  import { carriedFrom } from "../fold/resolved.js";
@@ -90,7 +90,9 @@ export async function pass(root, entry, under, check) {
90
90
  if (!swept.written.length)
91
91
  await closeReceipts(root, storeOf(root));
92
92
  const closed = meets(root, storeOf(root), under ?? '') && !swept.written.length;
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 });
93
+ // Sealed files do not imply an empty reading. If a retained fold is stale,
94
+ // foldPlace must reload its evidence; a valid retained fold still costs no read.
95
+ const fold = foldPlace(root, entry, under, false, { standing: swept.standingAt.get(under ?? '') ?? standingOf(root, under), ...(swept.settled ? {} : { observed: swept.observed.length ? swept.observed : observedClaims(root, storeOf(root), { wait: false }) }), free: !swept.settled });
94
96
  for (const line of releasePolicyLines(fold.release))
95
97
  process.stdout.write(`${line}\n`);
96
98
  if (closed) {
@@ -166,7 +168,7 @@ async function sweep(root, entry, under, check, told, journal, given, keep, reac
166
168
  return picked.length ? picked : [...worldPlaces];
167
169
  };
168
170
  const placeless = views.filter((view) => !worldList.includes(view.name) && !placesOf(lock.standing, view).length);
169
- const holding = entriesIn(root).filter((entry) => entry.dir && placeless.some((view) => observeText(join(root, entry.name, view.shape)) !== undefined)).map((entry) => `${entry.name}/`);
171
+ const holding = localDirectoriesIn(root).filter((name) => placeless.some((view) => observeText(join(root, name, view.shape)) !== undefined)).map((name) => `${name}/`);
170
172
  const descended = (place) => !limited || descent.some((one) => one === '' || place === `${one}/` || place.startsWith(`${one}/`));
171
173
  const reached = limited ? descent.filter((one) => one !== '').map((one) => (one.endsWith('/') ? one : `${one}/`)) : [];
172
174
  const places = [...new Set([...views.flatMap(resolved), ...holding, ...reached])]
@@ -69,6 +69,6 @@ export declare function coneOf(root: string, entry: string, ceilings: readonly s
69
69
  export declare const ownCeilings: (under?: string) => readonly string[];
70
70
  export declare function foldPlace(root: string, entry: string, under?: string, look?: boolean, given?: {
71
71
  readonly standing: ReturnType<typeof standingOf>;
72
- readonly observed: readonly string[];
72
+ readonly observed?: readonly string[];
73
73
  readonly free?: boolean;
74
74
  }): PlaceFold;
package/dist/cli/place.js CHANGED
@@ -195,7 +195,7 @@ export function foldPlace(root, entry, under, look = true, given) {
195
195
  const closed = meets(root, store, under ?? '');
196
196
  const digestMs = Date.now() - digestAt;
197
197
  if (closed && given?.free !== true) {
198
- const prior = keptPlace(store, under);
198
+ const prior = keptPlace(store, under, instrumentOf(store, entry, standingOf(root).standing, root, ownLock()));
199
199
  if (prior !== undefined) {
200
200
  process.stderr.write(`CLOSED ${under || '.'} · digest ${digestMs} ms\n`);
201
201
  return { ...prior, kept: true };
@@ -0,0 +1,5 @@
1
+ /** Judge an external signed approval without landing it, fetching its source,
2
+ * executing a provider, or changing the release-policy verdict of the place.
3
+ * The anchor is the place's key, never a key offered by the approval. */
4
+ export declare function releaseApproval(lock: readonly string[], lines: readonly string[]): string;
5
+ export declare function printReleaseApproval(root: string, file: string): void;
@@ -0,0 +1,80 @@
1
+ import { parse } from '@lapxo/topos/wire';
2
+ import { authorityFor, writerFor, releaseSigner } from "../fold/signers.js";
3
+ import { eraOf } from "../fold/digests.js";
4
+ import { fieldOf, foldClaims } from "../fold/claims.js";
5
+ import { observeText } from "../observe/files.js";
6
+ import { claimLinesIn } from "./args.js";
7
+ import { LOCK } from '@lapxo/topos/wire';
8
+ import { join } from 'node:path';
9
+ /** Judge an external signed approval without landing it, fetching its source,
10
+ * executing a provider, or changing the release-policy verdict of the place.
11
+ * The anchor is the place's key, never a key offered by the approval. */
12
+ export function releaseApproval(lock, lines) {
13
+ const standing = foldClaims(lock).standing;
14
+ const id = writerFor(standing, 'authorize');
15
+ if (!id)
16
+ throw Error('REFUSE·release place names no authorizer');
17
+ const one = (rows, scope, measure) => {
18
+ const selected = rows.filter(line => fieldOf(line, 'scope') === scope && (!measure || fieldOf(line, 'measure') === measure) && fieldOf(line, 'value') !== 'withdraw');
19
+ if (selected.length !== 1)
20
+ throw Error(`REFUSE·release ${scope} needs one declaration`);
21
+ return fieldOf(selected[0], 'value');
22
+ };
23
+ const anchor = { ...releaseSigner(standing), id, publicKey: one(standing, `keys/${id}`, 'public-key'), coverage: one(standing, `keys/${id}`, 'coverage').split('|') };
24
+ if (!lines.length)
25
+ throw Error('REFUSE·release empty approval');
26
+ for (const line of lines) {
27
+ const got = parse(line);
28
+ if (got.kind !== 'fact')
29
+ throw Error('REFUSE·release malformed approval');
30
+ if (got.value.fields['by'] !== id || !got.value.fields['sig'])
31
+ throw Error(`REFUSE·release approval must be signed by ${id}`);
32
+ }
33
+ // A separate release place may declare an era; its configuration must itself
34
+ // authenticate against the existing anchor. No packet key or coverage is used.
35
+ const wire = lines.filter(line => ['wire/signature-algorithms', 'audit/wire/signature-algorithms', 'wire/era', 'audit/wire/era'].includes(fieldOf(line, 'scope')));
36
+ let era;
37
+ try {
38
+ era = eraOf(wire.length ? foldClaims(wire).standing : standing);
39
+ }
40
+ catch {
41
+ throw Error('REFUSE·release signature era is absent or conflicting');
42
+ }
43
+ const authority = authorityFor([], anchor, era.admitted);
44
+ for (const line of lines) {
45
+ const verdict = authority.of(line);
46
+ if (verdict.kind !== 'admitted')
47
+ throw Error(`REFUSE·release ${fieldOf(line, 'scope')}: ${verdict.why ?? verdict.kind}`);
48
+ }
49
+ const released = lines.filter(line => /^release\/[^/]+$/.test(fieldOf(line, 'scope')) && fieldOf(line, 'measure') === 'digest' && fieldOf(line, 'role') === 'writes');
50
+ if (released.length !== 1)
51
+ throw Error('REFUSE·release needs one signed release/<tag> digest');
52
+ const required = (scope, measure) => {
53
+ const selected = lines.filter(line => fieldOf(line, 'scope') === scope);
54
+ if (selected.some(line => fieldOf(line, 'role') !== 'writes' || fieldOf(line, 'form') !== 'alphabet' || fieldOf(line, 'measure') !== measure))
55
+ throw Error(`REFUSE·release ${scope} is not an approval declaration`);
56
+ return one(selected, scope, measure);
57
+ };
58
+ const digest = required(fieldOf(released[0], 'scope'), 'digest'), name = required('release/artifact', 'id'), source = required('sources/archive', 'id');
59
+ if (!/^sha256:[a-f0-9]{64}$/.test(digest))
60
+ throw Error('REFUSE·release archive needs sha256');
61
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name))
62
+ throw Error('REFUSE·release artifact needs one safe filename');
63
+ let url;
64
+ try {
65
+ url = new URL(source);
66
+ }
67
+ catch {
68
+ throw Error('REFUSE·release archive source needs an absolute URL');
69
+ }
70
+ if (url.username || url.password || /[\s\x00-\x1f\x7f]/.test(source))
71
+ throw Error('REFUSE·release archive source needs a credential-free absolute URL without whitespace');
72
+ return `READY ${digest} ${name} ${source}`;
73
+ }
74
+ export function printReleaseApproval(root, file) {
75
+ const text = observeText(file);
76
+ if (text === undefined)
77
+ throw Error('REFUSE·release approval file is absent');
78
+ const lines = text.split('\n').filter(line => line.trim().length > 0);
79
+ process.stdout.write(releaseApproval(claimLinesIn(join(root, LOCK)), lines) + '\n');
80
+ }
@@ -0,0 +1,5 @@
1
+ /** An evidence projection, never a new authority lock. Authentication uses
2
+ * the admission snapshot at each local epoch; the existing fold applies
3
+ * supersessions and withdrawals across all declared classes. Storage layout
4
+ * and delivery mode do not enter this public boundary. */
5
+ export declare function printStandingResult(root: string): void;
@@ -0,0 +1,39 @@
1
+ import { viewsOf } from "../fold/views.js";
2
+ import { lockStanding } from "../fold/keys.js";
3
+ import { admissionAnchor, admissionHistoryAt } from "../fold/admission-history.js";
4
+ import { authorityFor } from "../fold/signers.js";
5
+ import { eraOf } from "../fold/digests.js";
6
+ import { fieldOf, foldClaims } from "../fold/claims.js";
7
+ import { storeOf } from "../land/ledger.js";
8
+ import { foldedEvidence } from '@lapxo/topos/contract';
9
+ /** An evidence projection, never a new authority lock. Authentication uses
10
+ * the admission snapshot at each local epoch; the existing fold applies
11
+ * supersessions and withdrawals across all declared classes. Storage layout
12
+ * and delivery mode do not enter this public boundary. */
13
+ export function printStandingResult(root) {
14
+ const store = storeOf(root), view = viewsOf(lockStanding(store)).get('standing');
15
+ if (!view || view.shape || !view.regions.some(region => region.name === 'standing'))
16
+ throw Error('REFUSE·view standing requires a declared standing view');
17
+ const history = admissionHistoryAt(root, Number.MAX_SAFE_INTEGER), anchor = admissionAnchor(root);
18
+ const snapshots = new Map();
19
+ const admitted = history.filter(line => {
20
+ const epoch = Number(fieldOf(line, 'epoch')) || 0;
21
+ let authority = snapshots.get(epoch);
22
+ if (!authority) {
23
+ const snapshot = admissionHistoryAt(root, epoch);
24
+ // The signature vocabulary is configuration, not the as-yet
25
+ // unauthenticated evidence whose values are about to be folded.
26
+ const wire = snapshot.filter(record => fieldOf(record, 'scope').startsWith('wire/'));
27
+ authority = authorityFor(snapshot, anchor, eraOf(foldClaims(wire).standing).admitted);
28
+ snapshots.set(epoch, authority);
29
+ }
30
+ return authority.of(line).kind === 'admitted';
31
+ });
32
+ // The host authenticates against historical authority; Topos projects the
33
+ // authenticated history using the instrument's existing fold.
34
+ const { standing } = foldedEvidence(admitted, {
35
+ classification: () => 'local',
36
+ fold: lines => foldClaims(lines).standing,
37
+ });
38
+ process.stdout.write(standing.length ? standing.join('\n') + '\n' : '');
39
+ }
package/dist/cli/verb.js CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { printReleaseApproval } from "./release-result.js";
3
+ import { printActResult } from "./act-result.js";
4
+ import { printStandingResult } from "./standing-result.js";
2
5
  import { withContentStore } from "../host/content-store.js";
3
- import { withoutEffects } from "../host/read-only.js";
6
+ import { readOnly, withoutEffects } from "../host/read-only.js";
7
+ import { layReleases } from "../host/release.js";
4
8
  import { resolveSources, resolveRequested } from "../host/source-content.js";
5
9
  import { cellsWithReceipts } from "../host/cell-receipts.js";
6
10
  import { renderCells } from '@lapxo/topos/cells-view';
@@ -188,6 +192,18 @@ async function view(root, under, asked, told = [], coord, key, includePolicy = t
188
192
  process.stderr.write(`${selfName()}: the owner ledger is empty; land it before rendering a view\n`);
189
193
  return EXIT.refuse;
190
194
  }
195
+ if (name === 'act') {
196
+ if (under || told.length)
197
+ throw Error('REFUSE·act committed identity query only');
198
+ printActResult(root, coord);
199
+ return EXIT.closed;
200
+ }
201
+ if (name === 'standing') {
202
+ if (under || told.length || coord || depth !== undefined)
203
+ throw Error('REFUSE·standing local committed projection only');
204
+ printStandingResult(root);
205
+ return EXIT.closed;
206
+ }
191
207
  if (name === 'evidence' && !under && !told.length && ingressBundles(storeOf(root)).length) {
192
208
  const declared = viewsOf(lockStanding(storeOf(root))).get(name);
193
209
  if (!declared || declared.shape || !declared.regions.some(region => region.name === 'evidence'))
@@ -224,6 +240,10 @@ async function view(root, under, asked, told = [], coord, key, includePolicy = t
224
240
  }
225
241
  }
226
242
  const own = place ? rest.filter((line) => admittedIn(root, place, line)) : [];
243
+ // Executed views use the place's declared mounts before observation, as pass
244
+ // does. Prospective folds and public standing queries do not lay releases.
245
+ if (!told.length && !readOnly())
246
+ layReleases(storeOf(root), root, standingOf(root, under).standing, storeOf(root));
227
247
  const fold = told.length
228
248
  ? foldPlace(root, entryOf(root), under, false, { standing: standingOf(root, under, ledger), observed: observedClaims(root, storeOf(root), { wait: false }), free: true })
229
249
  : foldPlace(root, entryOf(root), under, true);
@@ -440,12 +460,18 @@ async function foldAt(root, under, argv) {
440
460
  async function foldRequested(root, under, argv) {
441
461
  const as = valueOf(argv, FLAGS.as);
442
462
  const prospective = as !== undefined && positionalsOf(argv).some(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
443
- if (as === NAMED_VIEWS.help || prospective || argv.includes(FLAGS.check))
463
+ if (as === 'release' || as?.split('@')[0] === 'act' || as === NAMED_VIEWS.help || prospective || argv.includes(FLAGS.check))
444
464
  return foldResolved(root, under, argv);
445
465
  return resolveRequested(digest => resolveSources(standingOf(root, under).standing, [digest]), () => foldResolved(root, under, argv));
446
466
  }
447
467
  async function foldResolved(root, under, argv) {
448
468
  const as = valueOf(argv, FLAGS.as);
469
+ if (as === 'release') {
470
+ const approvals = positionalsOf(argv).filter(one => !(existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory()));
471
+ if (approvals.length !== 1 || argv.includes(FLAGS.check))
472
+ throw Error('REFUSE·release name one approval file; check does not authorize delivery');
473
+ return withoutEffects(() => { printReleaseApproval(under ? resolve(root, under) : root, resolve(approvals[0])); return EXIT.closed; });
474
+ }
449
475
  const inputs = positionalsOf(argv).filter(one => existsSync(resolve(one)) && !lstatSync(resolve(one)).isDirectory());
450
476
  // Public content is authenticated by its digest, independently of owner history.
451
477
  // A named view does not request mounting every release of the instrument.
@@ -497,6 +523,11 @@ async function foldResolved(root, under, argv) {
497
523
  }
498
524
  const named = inputs;
499
525
  const coord = positionalsOf(argv).find((one) => one !== under?.replace(/\/$/, '') && !one.startsWith('-') && !(existsSync(resolve(one)) && lstatSync(resolve(one)).isDirectory()));
526
+ if (as === 'act') {
527
+ if (positionalsOf(argv).length !== 1)
528
+ throw Error('REFUSE·act name one committed identity');
529
+ return view(root, under, as, [], coord);
530
+ }
500
531
  if ((as === 'why' || as === NAMED_VIEWS.because || as.split('@')[0] === 'why') && coord !== undefined)
501
532
  return view(root, under, as === NAMED_VIEWS.because ? 'why' : as, [], coord, key);
502
533
  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
+ }
@@ -4,7 +4,7 @@ import { dirname, existsSync, join, mkdirSync, relative, resolve, statSync, writ
4
4
  import { byBytes, canonical, LOCK, parse, publicationOf, roleAt, roleClaimsOf } from '@lapxo/topos/wire';
5
5
  import { RECEIPTS, fieldOf, foldClaims, isConfig, isWire } from "./claims.js";
6
6
  import { bytesDigest as hashBytes, fullDigest as hashFull } from "../host/digest.js";
7
- import { worldAt } from "./places.js";
7
+ import { mountedCoordinatesIn, worldAt } from "./places.js";
8
8
  import { ownLockOf } from "./signed.js";
9
9
  import { viewsOf } from "./views.js";
10
10
  import { storeAt, storeOf } from "../land/ledger.js";
@@ -31,6 +31,7 @@ const lockStamp = (root, place) => {
31
31
  return 'none';
32
32
  }
33
33
  };
34
+ const publishedMounts = (root) => mountedCoordinatesIn(root, (observeText(join(root, LOCK)) ?? '').split('\n').filter(isWire));
34
35
  function filesOf(root, place, region, inputs) {
35
36
  const step = region.replace(/^receipts\//, '').replace(/\/$/, '');
36
37
  const here = join(root, place);
@@ -41,7 +42,10 @@ function filesOf(root, place, region, inputs) {
41
42
  const record = parse(line);
42
43
  return record.kind === 'fact' ? [record.value.fields] : [];
43
44
  }));
44
- const owned = coordinatesUnder(join(here, step), root).filter(rel => roleAt(relative(here, join(root, rel)), claims) !== 'foreign');
45
+ const localMounts = [...mountedCoordinatesIn(here), ...publishedMounts(here)];
46
+ const mounts = [...new Set([...mountedCoordinatesIn(root), ...publishedMounts(root), ...localMounts.map(coordinate => relative(root, join(here, coordinate)))])];
47
+ const owned = coordinatesUnder(join(here, step), root).filter(rel => roleAt(relative(here, join(root, rel)), claims) !== 'foreign'
48
+ && !mounts.some(mount => rel === mount || rel.startsWith(`${mount}/`)));
45
49
  // Foreign worlds are not the place's tree. Retain the external files actually handed to its readers.
46
50
  const observed = inputs.filter(line => fieldOf(line, 'measure') === 'observed').flatMap(line => {
47
51
  const coordinate = fieldOf(line, 'scope');
@@ -101,11 +105,13 @@ function namedReceipts(root, store) {
101
105
  const text = observeText(join(root, RECEIPTS));
102
106
  const carried = carriedFrom(store, (text ?? '').split('\n').filter(isWire));
103
107
  const out = new Map();
108
+ const mounted = new Set(mountedCoordinatesIn(root));
104
109
  for (const line of carried ?? []) {
105
110
  if (!isWire(line) || fieldOf(line, 'measure') !== 'digest')
106
111
  continue;
107
112
  const place = /^write\/([^/]+)\/receipts\.bound$/.exec(fieldOf(line, 'scope'))?.[1];
108
- if (place)
113
+ // A retained dependency receipt is input evidence, not a child-place closure.
114
+ if (place && !mounted.has(place))
109
115
  out.set(place, fieldOf(line, 'value'));
110
116
  }
111
117
  return out;
@@ -1,5 +1,6 @@
1
+ import { ledgerLines, ledgerWriters } from "../land/ledger.js";
1
2
  import { readOnly } from "../host/read-only.js";
2
- import { appendFileSync, dirname, join, mkdirSync, readdirSync, relative, resolve } from "../host/io.js";
3
+ import { appendFileSync, dirname, join, mkdirSync, readdirSync, realpathSync, relative, resolve } from "../host/io.js";
3
4
  import { bytesDigest as bytesWith, fullDigest as fullWith } from "../host/digest.js";
4
5
  import { coordinatesUnder, observeFile, observeText, stampOf } from "../observe/files.js";
5
6
  import { releaseOf, signedLockLines } from "./keys.js";
@@ -32,10 +33,8 @@ function namedAlgorithm(store) {
32
33
  const contract = published === undefined ? undefined : wireLine(published, 'digest-algorithms');
33
34
  return contract === undefined ? undefined : fieldOf(contract, 'value').split('|')[0]?.split(':')[0];
34
35
  }
35
- for (const entry of readdirSync(join(store, 'ledger'), { withFileTypes: true })) {
36
- if (!entry.isFile() || !entry.name.endsWith(EXTENSION))
37
- continue;
38
- const text = observeText(join(store, 'ledger', entry.name)) ?? '';
36
+ for (const writer of ledgerWriters(store)) {
37
+ const text = ledgerLines(store, writer).join('\n');
39
38
  let from = 0;
40
39
  while (from < text.length) {
41
40
  const at = (() => {
@@ -176,14 +175,19 @@ export function instrumentCoordinates(store, entry, standing = [], root = dirnam
176
175
  const digest = coordinateDigest(store, at);
177
176
  if (digest === undefined)
178
177
  return;
179
- seen.set(at, digest);
180
- for (const one of importsOf(store, at, digest)) {
178
+ // The host loader resolves links before loading a module. Count that module
179
+ // once, while retaining distinct modules even when their bytes are equal.
180
+ const module = realpathSync(at);
181
+ if (seen.has(module))
182
+ return;
183
+ seen.set(module, digest);
184
+ for (const one of importsOf(store, module, digest)) {
181
185
  if (one.startsWith('node:') || builtinModules.includes(one))
182
186
  continue;
183
187
  if (one.startsWith('.'))
184
- walk(resolve(dirname(at), one));
188
+ walk(resolve(dirname(module), one));
185
189
  else
186
- walk(createRequire(at).resolve(one));
190
+ walk(createRequire(module).resolve(one));
187
191
  }
188
192
  };
189
193
  walk(resolve(entry));
@@ -23,7 +23,7 @@ export declare function movedPoints(store: string, now: readonly string[], key:
23
23
  readonly came: readonly string[];
24
24
  } | undefined;
25
25
  export declare const foldsElsewhere: () => number;
26
- export declare function keptPlace(store: string, under?: string): PlaceFold | undefined;
26
+ export declare function keptPlace(store: string, under?: string, currentInstrument?: string): PlaceFold | undefined;
27
27
  export declare const keptRender: (store: string, key: string, name: string) => string | undefined;
28
28
  export declare const keepRender: (store: string, _signer: string, key: string, name: string, _epoch: number, text_: string) => void;
29
29
  export declare function keepFold(store: string, key: string, fold: PlaceFold, points: readonly string[], instrument?: string): void;