@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
@@ -0,0 +1,100 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { requireEffects } from "../read-only.js";
4
+ import { underTheRegions } from "../../land/act.js";
5
+ import { publishBundle, nativePublicationStorage } from "./publication.js";
6
+ import { actIdentity, parse, canonical } from '@lapxo/topos/wire';
7
+ import { fullDigest } from "../digest.js";
8
+ const atOf = (store, identity) => {
9
+ if (!identity || identity.includes('\0'))
10
+ throw Error('REFUSE·act missing identity');
11
+ return join(store, 'ledger', 'acts', 'committed-' + encodeURIComponent(identity));
12
+ };
13
+ const read = (at) => {
14
+ try {
15
+ const placements = readdirSync(at).filter(name => name.startsWith('place-') && name.endsWith('.bound')).map(name => ({
16
+ place: decodeURIComponent(name.slice(6, -6)), records: readFileSync(join(at, name), 'utf8'),
17
+ }));
18
+ return {
19
+ records: readFileSync(join(at, 'records.bound'), 'utf8'),
20
+ receipt: readFileSync(join(at, 'receipt.bound'), 'utf8'),
21
+ ...(placements.length ? { placements } : {}),
22
+ };
23
+ }
24
+ catch (error) {
25
+ throw Error(`REFUSE·act committed result unreadable · ${error.code ?? 'read failed'}`);
26
+ }
27
+ };
28
+ export const actLines = (bytes) => {
29
+ if (!bytes.endsWith('\n'))
30
+ throw Error('REFUSE·act truncated committed lines');
31
+ return bytes.slice(0, -1).split('\n');
32
+ };
33
+ /** Journal integrity is not authority. Signature/coverage checks remain the
34
+ * same admission boundary; the hash algorithm here is the stored seal's named
35
+ * algorithm, not a new selection of the place's wire.
36
+ */
37
+ export function intactLocalAct(identity, bundle) {
38
+ if (!bundle.receipt.endsWith('\n') || bundle.receipt.slice(0, -1).includes('\n'))
39
+ return false;
40
+ const receipt = parse(bundle.receipt.slice(0, -1));
41
+ if (receipt.kind !== 'fact' || !bundle.placements?.length)
42
+ return false;
43
+ const f = receipt.value.fields;
44
+ if (canonical(f) !== bundle.receipt.slice(0, -1) || !/^(0|[1-9][0-9]*)$/.test(f['epoch'] ?? ''))
45
+ return false;
46
+ if (f['value'] !== identity || f['role'] !== 'writes' || f['form'] !== 'alphabet' || f['measure'] !== 'digest')
47
+ return false;
48
+ return actIdentity(actLines(bundle.records), { scope: f['scope'] ?? '', context: f['at'] ?? '',
49
+ localEpoch: Number(f['epoch']), digest: bytes => fullDigest(bytes, identity.slice(0, identity.indexOf(':'))),
50
+ placements: bundle.placements.map(p => ({ place: p.place, records: actLines(p.records) })),
51
+ }) === identity;
52
+ }
53
+ /** Committed segments are part of this ledger, not a second authority source. */
54
+ export function committedLocalActs(store) {
55
+ const root = join(store, 'ledger', 'acts');
56
+ if (!existsSync(root))
57
+ return [];
58
+ return readdirSync(root).filter(name => name.startsWith('committed-')).map(name => {
59
+ const identity = decodeURIComponent(name.slice(10)), bundle = read(join(root, name));
60
+ if (!intactLocalAct(identity, bundle))
61
+ throw Error('REFUSE·act committed journal seal does not verify');
62
+ return { identity, bundle };
63
+ }).sort((a, b) => {
64
+ const epoch = (one) => { const p = parse(one.receipt.trim()); return p.kind === 'fact' ? Number(p.value.fields['epoch']) : 0; };
65
+ return epoch(a.bundle) - epoch(b.bundle);
66
+ });
67
+ }
68
+ /** A public result is available only as a verified committed pair.
69
+ * No writer-tail guess, newest-result selection or implicit authority.
70
+ */
71
+ export function localActResult(store, identity, verify) {
72
+ const at = atOf(store, identity);
73
+ if (!existsSync(at))
74
+ return undefined;
75
+ const bundle = read(at);
76
+ if (verify(identity, bundle) !== true)
77
+ throw Error('REFUSE·act committed result does not verify');
78
+ return bundle;
79
+ }
80
+ /** One publication point for records and their native result. Re-delivery
81
+ * reads the original pair without preparation, signing or a new local epoch.
82
+ * The caller must also integrate this boundary into admission-history readers;
83
+ * this storage port alone does not activate a grammar or land authority.
84
+ */
85
+ export async function commitLocalAct(store, identity, regions, prepare, verify, storage = nativePublicationStorage) {
86
+ requireEffects('local act commitment');
87
+ return underTheRegions(store, regions, 'land', async () => {
88
+ const prior = localActResult(store, identity, verify);
89
+ if (prior)
90
+ return { kind: 'once', ...prior };
91
+ const bundle = await prepare();
92
+ if (!bundle.records || !bundle.receipt || verify(identity, bundle) !== true)
93
+ throw Error('REFUSE·act prepared result does not verify');
94
+ const files = { 'records.bound': bundle.records, 'receipt.bound': bundle.receipt };
95
+ for (const p of bundle.placements ?? [])
96
+ files['place-' + encodeURIComponent(p.place) + '.bound'] = p.records;
97
+ publishBundle(atOf(store, identity), files, storage);
98
+ return { kind: 'landed', ...bundle };
99
+ });
100
+ }
@@ -0,0 +1,10 @@
1
+ export interface PublicationStorage {
2
+ publish(staged: string, committed: string): void;
3
+ }
4
+ export declare const nativePublicationStorage: PublicationStorage;
5
+ /** Publish prepared bytes as one directory. No identity, grammar or authority
6
+ * is inferred here. Readers address the committed name, never staging files.
7
+ * A failure after rename is not evidence of rollback: the caller must retain
8
+ * the uncertainty and allow a verified read of the committed name.
9
+ */
10
+ export declare function publishBundle(committed: string, files: Readonly<Record<string, string>>, storage?: PublicationStorage): void;
@@ -0,0 +1,56 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { requireEffects } from "../read-only.js";
5
+ const syncDirectory = (path) => {
6
+ const fd = openSync(path, 'r');
7
+ try {
8
+ fsyncSync(fd);
9
+ }
10
+ finally {
11
+ closeSync(fd);
12
+ }
13
+ };
14
+ export const nativePublicationStorage = {
15
+ publish(staged, committed) { renameSync(staged, committed); syncDirectory(dirname(committed)); },
16
+ };
17
+ /** Publish prepared bytes as one directory. No identity, grammar or authority
18
+ * is inferred here. Readers address the committed name, never staging files.
19
+ * A failure after rename is not evidence of rollback: the caller must retain
20
+ * the uncertainty and allow a verified read of the committed name.
21
+ */
22
+ export function publishBundle(committed, files, storage = nativePublicationStorage) {
23
+ requireEffects('atomic publication');
24
+ const entries = Object.entries(files);
25
+ if (!entries.length || entries.some(([name]) => !name || name === '.' || name === '..' || /[\\/\0]/.test(name))) {
26
+ throw Error('REFUSE·commit invalid publication members');
27
+ }
28
+ const root = dirname(committed);
29
+ const created = mkdirSync(root, { recursive: true });
30
+ // Persist newly created directory entries up to their existing parent too.
31
+ if (created !== undefined)
32
+ for (let at = root;; at = dirname(at)) {
33
+ syncDirectory(at);
34
+ if (at === dirname(created))
35
+ break;
36
+ }
37
+ const staged = join(root, '.stage-' + randomUUID());
38
+ mkdirSync(staged, { mode: 0o700 });
39
+ try {
40
+ for (const [name, bytes] of entries) {
41
+ const fd = openSync(join(staged, name), 'wx', 0o600);
42
+ try {
43
+ writeFileSync(fd, bytes);
44
+ fsyncSync(fd);
45
+ }
46
+ finally {
47
+ closeSync(fd);
48
+ }
49
+ }
50
+ syncDirectory(staged);
51
+ storage.publish(staged, committed);
52
+ }
53
+ finally {
54
+ rmSync(staged, { recursive: true, force: true });
55
+ }
56
+ }
@@ -1,3 +1,4 @@
1
+ import type { ReaderLifetime, ReaderReference } from '@lapxo/topos/wire';
1
2
  import type { Capsule } from './capsule.ts';
2
3
  type File = {
3
4
  readonly place: string;
@@ -14,6 +15,8 @@ export declare function readingClosures(options: {
14
15
  capsule: Capsule;
15
16
  region: string;
16
17
  projection: string;
18
+ readerReference?: ReaderReference;
19
+ lifetime?: ReaderLifetime;
17
20
  allowsEmpty?: boolean;
18
21
  files: readonly File[];
19
22
  emit?: (line: string) => void;
@@ -18,7 +18,7 @@ export function readingClosures(options) {
18
18
  if (!readFileSync(join(root, file.place)).equals(Buffer.from(file.text)))
19
19
  throw Error('REFUSE·input changed ' + file.place); };
20
20
  verify();
21
- const projected = capsule.ask([{ protocol: PROTOCOL, verb: 'render', rootScope: '', region: projection, at: 1, reads: ['**'], lines: inputLines(files), files: [] }])[0];
21
+ const projected = capsule.ask([{ protocol: PROTOCOL, verb: 'render', rootScope: '', region: projection, at: 1, reads: ['**'], lines: inputLines(files), files: [] }], options.readerReference)[0];
22
22
  if (projected?.kind !== 'fact' || !Array.isArray(projected.lines))
23
23
  throw Error('REFUSE·input projection ' + (projected?.why ?? 'no lines'));
24
24
  const plans = new Map();
@@ -27,7 +27,8 @@ export function readingClosures(options) {
27
27
  if (!scope || plans.has(scope) || f.form !== 'alphabet' || f.measure !== 'reads' || f.role !== 'reads' || !resolution)
28
28
  throw Error('REFUSE·input projection ambiguous reading ' + scope);
29
29
  const inputs = (f.value ?? '').split('|').filter(Boolean), needs = (f.needs ?? '').split('|').filter(Boolean);
30
- if (!inputs.length || new Set(inputs).size !== inputs.length || inputs.some(input => !handed.has(input)) || new Set(needs).size !== needs.length)
30
+ // An aggregate reads its declared receipts; it need not reread a leaf file.
31
+ if ((!inputs.length && !needs.length) || new Set(inputs).size !== inputs.length || inputs.some(input => !handed.has(input)) || new Set(needs).size !== needs.length)
31
32
  throw Error('REFUSE·input projection has unhanded or duplicate dependencies ' + scope);
32
33
  const n = Number(resolution[1]);
33
34
  if (!Number.isSafeInteger(n))
@@ -44,7 +45,7 @@ export function readingClosures(options) {
44
45
  visit(need); visiting.delete(scope); done.add(scope); ordered.push(p); };
45
46
  for (const scope of plans.keys())
46
47
  visit(scope);
47
- const identity = hash([capsule.selection ?? '', capsule.digest, region, projection].join('\n')), by = speaker + ':' + identity.split(':')[1].slice(0, 12), journal = 'reading-closures/' + identity.split(':')[1];
48
+ const identity = hash([capsule.selection ?? '', capsule.digest, region, projection, ...(options.lifetime === undefined ? [] : [JSON.stringify(options.lifetime)])].join('\n')), by = speaker + ':' + identity.split(':')[1].slice(0, 12), journal = 'reading-closures/' + identity.split(':')[1];
48
49
  const prior = shardLines(store, speaker, journal), results = new Map(), out = [];
49
50
  const carried = (digest) => join(store, 'cas', 'carried', digest.split(':')[1] ?? '');
50
51
  const saved = (text) => { const digest = hash(text), path = carried(digest); mkdirSync(dirname(path), { recursive: true }); if (!existsSync(path) || hash(readFileSync(path, 'utf8')) !== digest)
@@ -74,7 +75,7 @@ export function readingClosures(options) {
74
75
  if (readOnly())
75
76
  throw Error('REFUSE·preview reading closure is unpaid ' + plan.scope);
76
77
  const started = process.hrtime.bigint();
77
- const dependencies = plan.needs.flatMap(need => results.get(need).lines), answer = capsule.ask([{ protocol: PROTOCOL, verb: 'read', rootScope: plan.scope, name: plan.scope, region, at: plan.resolution, reads: ['**'], lines: [...inputLines(plan.inputs.map(place => ({ place, text: handed.get(place) }))), ...dependencies], files: [] }])[0];
78
+ const dependencies = plan.needs.flatMap(need => results.get(need).lines), answer = capsule.ask([{ protocol: PROTOCOL, verb: 'read', rootScope: plan.scope, name: plan.scope, region, at: plan.resolution, reads: ['**'], lines: [...inputLines(plan.inputs.map(place => ({ place, text: handed.get(place) }))), ...dependencies], files: [] }], options.readerReference)[0];
78
79
  if (answer?.kind !== 'fact' || !Array.isArray(answer.claims))
79
80
  throw Error('REFUSE·reading ' + plan.scope + ' · ' + (answer?.why ?? 'no claims'));
80
81
  lines = answer.claims.map((unknown) => { if (typeof unknown?.scope !== 'string' || typeof unknown?.measure !== 'string' || typeof unknown?.role !== 'string' || !unknown.bound || !['interval', 'enumerated'].includes(unknown.bound.kind))
@@ -25,12 +25,12 @@ export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, ho
25
25
  const descriptor = capsuleDescriptor(topos, offer.scope ?? '');
26
26
  const loaded = descriptor === undefined ? load() : undefined;
27
27
  const capsule = loaded?.capsule ?? {
28
- digest: offer.value, selection: `${digest} ${offer.scope}`, ...descriptor, ask: requests => {
28
+ digest: offer.value, selection: `${digest} ${offer.scope}`, ...descriptor, ask: (requests, reader) => {
29
29
  if (!requests.length)
30
30
  return [];
31
31
  const actual = load();
32
32
  assertCapsuleDescriptor(descriptor.lines, actual.capsule.lines);
33
- return actual.capsule.ask(requests);
33
+ return actual.capsule.ask(requests, reader);
34
34
  },
35
35
  };
36
36
  const projected = loaded?.offer ?? canonical(offer);
@@ -40,10 +40,10 @@ export function selectedCapsules(pins, resolve = capsuleAt, select = toposAt, ho
40
40
  const wire = wireAt(fields, Number.MAX_SAFE_INTEGER), algorithm = wire && [...wire.digests][0];
41
41
  if (!algorithm)
42
42
  throw Error('REFUSE·input instrument has no admitted digest algorithm');
43
- const selected = { ...capsule, selection: `${digest} ${offer.scope}`, ask: requests => capsule.ask(requests.map(request => {
43
+ const selected = { ...capsule, selection: `${digest} ${offer.scope}`, ask: (requests, reader) => capsule.ask(requests.map(request => {
44
44
  const provider = providerInputs(topos, digest, offer.scope ?? '', offer.value, request.region ?? '', bytes => fullDigest(bytes, algorithm));
45
45
  return provider === undefined ? request : { ...request, provider };
46
- })) };
46
+ }), reader) };
47
47
  return { capsule: selected, offer: projected };
48
48
  }));
49
49
  }
@@ -1,10 +1,13 @@
1
1
  import type { StandingTopos } from '@lapxo/topos/standing';
2
- import { wireAt } from '@lapxo/topos/wire';
2
+ import { wireAt, fromLine } from '@lapxo/topos/wire';
3
3
  export interface SelectedTopos {
4
4
  readonly digest: string;
5
5
  readonly topos: StandingTopos;
6
6
  }
7
7
  export type ToposResolver = (digest: string) => StandingTopos;
8
+ /** Local grammar is sufficient for locally declared records. Resolve selected
9
+ * contracts only when a reader is missing; a local refusal remains a refusal. */
10
+ export declare function admittedLine(line: string, local: readonly Readonly<Record<string, string>>[], epoch: number, pins: readonly string[], resolve?: ToposResolver): ReturnType<typeof fromLine>;
8
11
  /** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
9
12
  export declare function toposAt(digest: string, store?: string): StandingTopos;
10
13
  export declare function contentAt(digest: string, store?: string): Uint8Array;
@@ -1,7 +1,22 @@
1
1
  import { verifiedContent } from "./content-store.js";
2
2
  import { readStanding } from '@lapxo/topos/standing';
3
- import { parse, wireAt } from '@lapxo/topos/wire';
3
+ import { parse, wireAt, fromLine } from '@lapxo/topos/wire';
4
4
  import { fieldOf } from "../fold/claims.js";
5
+ /** Local grammar is sufficient for locally declared records. Resolve selected
6
+ * contracts only when a reader is missing; a local refusal remains a refusal. */
7
+ export function admittedLine(line, local, epoch, pins, resolve = toposAt) {
8
+ const own = wireAt(local, epoch);
9
+ // A signature era alone is not a configuration grammar. An absent list
10
+ // must be supplied by the selected contract; an explicit local list remains
11
+ // authoritative and may refuse a field without opening unrelated content.
12
+ if (own && ['fields', 'required', 'roles', 'forms', 'at-classes'].every(name => own.lists.has(name))) {
13
+ const got = fromLine(line, own);
14
+ const needsObject = fieldOf(line, 'type') !== '' && !own.object;
15
+ if (got.kind === 'fact' || (got.kind === 'refuse' && !needsObject))
16
+ return got;
17
+ }
18
+ return fromLine(line, selectedWireAt(local, epoch, pins, resolve));
19
+ }
5
20
  /** Resolve content using the instrument wire's admitted digest algorithms. No transport interpretation. */
6
21
  export function toposAt(digest, store) {
7
22
  const topos = readStanding(Buffer.from(contentAt(digest, store)).toString('utf8'));
@@ -10,6 +10,7 @@ export declare function writerFile(store: string, writer: string): string;
10
10
  export declare function ledgerLines(store: string, writer: string): readonly string[];
11
11
  export declare function shardFile(store: string, writer: string, region: string): string;
12
12
  export declare const shardLines: (store: string, writer: string, region: string) => readonly string[];
13
+ export declare function ledgerWriters(store: string): readonly string[];
13
14
  export declare function landShard(store: string, writer: string, region: string, lines: readonly string[]): number;
14
15
  export declare function signedLines(store: string, writer: string): string[];
15
16
  export declare function blobAt(store: string, digest: string): string;
@@ -1,3 +1,5 @@
1
+ import { actLines, committedLocalActs } from "../host/ports/local-act.js";
2
+ import { readdirSync } from 'node:fs';
1
3
  import { requireEffects } from "../host/read-only.js";
2
4
  import { appendFileSync, closeSync, dirname, existsSync, join, linkSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync } from "../host/io.js";
3
5
  import { storeRoot } from "../host/ports/store.js";
@@ -21,12 +23,29 @@ export function writerFile(store, writer) {
21
23
  }
22
24
  const held = new Map();
23
25
  export function ledgerLines(store, writer) {
24
- return linesAt(writerFile(store, writer));
26
+ return journalLines(store, writer, '.', linesAt(writerFile(store, writer)));
25
27
  }
26
28
  export function shardFile(store, writer, region) {
27
29
  return storeAt(store, 'ledger', writer.replace(/[^a-zA-Z0-9_-]/g, ''), `${region.replace(/\/+$/, '') || '@'}${EXTENSION}`);
28
30
  }
29
- export const shardLines = (store, writer, region) => linesAt(shardFile(store, writer, region));
31
+ export const shardLines = (store, writer, region) => journalLines(store, writer, region, linesAt(shardFile(store, writer, region)));
32
+ const journalMemo = new Map();
33
+ function journalLines(store, writer, place, legacy) {
34
+ const fresh = committedLocalActs(store).flatMap(act => act.bundle.placements?.filter(p => p.place === place).flatMap(p => actLines(p.records).filter(line => fieldOf(line, 'by') === writer)) ?? []);
35
+ if (!fresh.length)
36
+ return legacy;
37
+ const lines = [...new Set([...legacy, ...fresh])], key = store + '\0' + writer + '\0' + place, last = journalMemo.get(key);
38
+ if (last?.length === lines.length && last.every((line, i) => line === lines[i]))
39
+ return last;
40
+ journalMemo.set(key, lines);
41
+ return lines;
42
+ }
43
+ export function ledgerWriters(store) {
44
+ const at = join(store, 'ledger');
45
+ const legacy = existsSync(at) ? readdirSync(at).filter(name => name.endsWith(EXTENSION)).map(name => name.slice(0, -EXTENSION.length)) : [];
46
+ const committed = committedLocalActs(store).flatMap(act => act.bundle.placements?.filter(p => p.place === '.').flatMap(p => actLines(p.records).map(line => fieldOf(line, 'by'))) ?? []);
47
+ return [...new Set([...legacy, ...committed])];
48
+ }
30
49
  export function landShard(store, writer, region, lines) {
31
50
  return appendNew(shardFile(store, writer, region), lines);
32
51
  }
@@ -20,4 +20,4 @@ export type Owned = {
20
20
  */
21
21
  export declare function ownedOf(root: string, signed: readonly string[], admits: (line: string) => boolean, coverage: (by: string) => readonly string[], named?: readonly string[], placedAt?: (line: string) => readonly string[] | undefined): Owned;
22
22
  /** A place's own lock gains the lines a batch landed there, appended whole: a reader never meets half of it, and every line already there keeps its bytes as they were. */
23
- export declare function landOwned(root: string, place: string, lines: readonly string[]): void;
23
+ export declare function landOwned(root: string, place: string, lines: readonly string[], committed?: boolean): void;
@@ -51,14 +51,15 @@ export function ownedOf(root, signed, admits, coverage, named = [], placedAt = (
51
51
  return { ledger, own, nowhere: [...placed, ...rest.filter((line) => !lands(line))], forked };
52
52
  }
53
53
  /** A place's own lock gains the lines a batch landed there, appended whole: a reader never meets half of it, and every line already there keeps its bytes as they were. */
54
- export function landOwned(root, place, lines) {
54
+ export function landOwned(root, place, lines, committed = false) {
55
55
  if (!lines.length)
56
56
  return tellOwn(place, []);
57
57
  const at = join(root, place, LOCK);
58
58
  const text = observeText(at) ?? '';
59
- for (const by of new Set(lines.map(line => fieldOf(line, 'by')))) {
60
- landShard(storeOf(root), by, `${place}/${LOCK}`, lines.filter(line => fieldOf(line, 'by') === by));
61
- }
59
+ if (!committed)
60
+ for (const by of new Set(lines.map(line => fieldOf(line, 'by')))) {
61
+ landShard(storeOf(root), by, `${place}/${LOCK}`, lines.filter(line => fieldOf(line, 'by') === by));
62
+ }
62
63
  writeFileSync(`${at}.${process.pid}.landing`, `${text.replace(/\n*$/, '\n')}${lines.join('\n')}\n`);
63
64
  renameSync(`${at}.${process.pid}.landing`, at);
64
65
  tellOwn(place, []);
@@ -0,0 +1,3 @@
1
+ /** A new world selection names standing bytes. Artifact aliases remain separate.
2
+ * Admission reads laid content only; it never fetches or executes a provider. */
3
+ export declare function admitStandingPins(root: string, lines: readonly string[], read?: (digest: string) => Uint8Array): void;
@@ -0,0 +1,49 @@
1
+ import { readStanding } from '@lapxo/topos/standing';
2
+ import { fieldOf } from "../fold/claims.js";
3
+ import { contentAt } from "../host/selected-topos.js";
4
+ import { storeOf } from "./ledger.js";
5
+ import { ContentNeeded } from "../host/content-needed.js";
6
+ import { observeFile } from "../observe/files.js";
7
+ import { createHash } from 'node:crypto';
8
+ import { resolve, relative } from 'node:path';
9
+ /** A declared local file is already held content. Check its bytes before
10
+ * admission; neither fetch it nor write a CAS entry during this inspection. */
11
+ function heldStanding(root, line) {
12
+ const digest = fieldOf(line, 'value');
13
+ try {
14
+ return contentAt(digest, storeOf(root));
15
+ }
16
+ catch (error) {
17
+ if (!(error instanceof ContentNeeded))
18
+ throw error;
19
+ }
20
+ const name = fieldOf(line, 'needs');
21
+ if (!name || name.includes('|') || name.includes('*'))
22
+ throw new ContentNeeded(digest);
23
+ const path = resolve(root, name), local = relative(resolve(root), path);
24
+ if (!local || local === '..' || local.startsWith('../'))
25
+ throw Error('standing coordinate is outside the place');
26
+ const bytes = observeFile(path);
27
+ if (bytes === undefined)
28
+ throw new ContentNeeded(digest);
29
+ const split = digest.indexOf(':'), algorithm = digest.slice(0, split), hex = digest.slice(split + 1);
30
+ if (createHash(algorithm).update(bytes).digest('hex') !== hex)
31
+ throw Error('standing coordinate content hash mismatch');
32
+ return bytes;
33
+ }
34
+ /** A new world selection names standing bytes. Artifact aliases remain separate.
35
+ * Admission reads laid content only; it never fetches or executes a provider. */
36
+ export function admitStandingPins(root, lines, read) {
37
+ for (const line of lines) {
38
+ const scope = fieldOf(line, 'scope'), value = fieldOf(line, 'value');
39
+ const parts = scope.split('/');
40
+ if (!(scope === 'uses' || parts.slice(0, -1).includes('uses')) || value === 'withdraw')
41
+ continue;
42
+ try {
43
+ readStanding(new TextDecoder('utf-8', { fatal: true }).decode(read ? read(value) : heldStanding(root, line)));
44
+ }
45
+ catch (error) {
46
+ throw Error(`REFUSE·pin ${scope} ${value} · ${error instanceof Error ? error.message : String(error)} · nothing landed`);
47
+ }
48
+ }
49
+ }
@@ -1,4 +1,4 @@
1
- import { existsSync, lstatSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
1
+ import { existsSync, lstatSync, statSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
2
2
  import { join, relative, sep } from 'node:path';
3
3
  /**
4
4
  * The one place the instrument reads the world: a coordinate's bytes, the entries of a directory, the coordinates under a place,
@@ -7,7 +7,17 @@ import { join, relative, sep } from 'node:path';
7
7
  const seen = new Map();
8
8
  /** A coordinate read once is kept by its stamp — its inode, size and time — and read again only when the stamp moves. */
9
9
  function bytesAt(at) {
10
- const st = stampable(at);
10
+ // Reading follows a link, so its cache stamp must describe the same target.
11
+ // Directory links have no file bytes; traversal keeps its separate link policy.
12
+ let st = stampable(at);
13
+ if (st?.isSymbolicLink()) {
14
+ try {
15
+ st = statSync(at);
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
11
21
  if (st === undefined || st.isDirectory() || !existsSync(at))
12
22
  return undefined;
13
23
  const stamp = `${st.ino}-${st.size}-${st.mtimeMs}`;
@@ -9,3 +9,19 @@
9
9
  # sign
10
10
 
11
11
  `bound sign <lot>` returns canonical signed lines after validating the whole lot. The admitted key selects a declared signer; `--signer` selects the same port explicitly. File and command mechanisms use one port. Topos signing-lot/1 frames canonical bytes in lot order, one invocation per lot, and returns signatures and public-key IDs. Verification checks the admitted public key, independently of the mechanism. A command is invoked without a shell and under declared bounds. No secret output is disclosed in refusals. Missing, invalid, partial, timed-out or failed responses refuse; `land` cannot commit their partial output. The sample compares file and command signatures and landed lines, checks invocation count and ledger hashes, and refuses an unknown wire version. The sample uses ephemeral keys outside the place; no owner key is included.
12
+
13
+ # Committed act results
14
+
15
+ The opt-in `local-act@1` contract is selected by `wire/act-result`, `wire/act-result-scope` and `wire/act-result-context`. An admitted `view/act measure=id value=act` makes `bound fold --as act <identity>` available. Missing or conflicting declarations refuse; an absent profile preserves the historical interface. Topos defines the canonical record and result contract.
16
+
17
+ Admission publishes the original signed records, their logical placements and one native result receipt atomically. A failure before commit writes no partial act. The result names the committed digest and local epoch; replay returns the original result without another epoch or origin. A post-commit error is not a rollback. Read the committed result before deciding whether to retry.
18
+
19
+ The query verifies the record signatures and coverage against the authority snapshot at admission, and the native receipt against its retained provenance. Later key retirement or profile replacement cannot authorize a new act but does not erase a verifiable historical result. Admission is not conformance: the output says `admitted · conformance not asserted`. Missing or corrupt results refuse by name. Host paths and delivery counts are not identity inputs.
20
+
21
+ ## Release approval
22
+
23
+ `fold --as release <approval-file>` verifies the external approval against the place authorizer, its admitted public key, coverage and signature era. Exactly one signed release/<tag> digest, release/artifact name and sources/archive URL are required. It prints `READY sha256:<digest> <name> <source>` or refuses with the coordinate and cause. This is an approval judgment, distinct from the place release-policy verdict. It never fetches, executes, signs or lands.
24
+
25
+ ## Standing admission
26
+
27
+ A uses/ selection admits canonical standing bytes, never an archive. The bytes must already be held under their verified digest or in the single local coordinate explicitly named by needs. Admission checks identity and canonical form without fetching, executing or writing the content store. Missing or corrupt bytes refuse the whole lot. A corrupt held copy cannot be hidden by another source. Archives remain separate dependency declarations.
@@ -4,3 +4,7 @@ The declared evidence@8 view projects authenticated delivery history of this pla
4
4
 
5
5
 
6
6
  The no sample specifies an uncovered write and tampered signed bytes. Both must refuse by name. Exact withdrawals remove only covered normative lines; authenticated evidence remains inspectable. The vectors assert status, signature verification and observable CLI output. No private key bytes are printed or shipped.
7
+
8
+ ## Folded standing
9
+
10
+ A place declares `view/standing role=demands form=alphabet measure=id value=standing`. `fold --as standing` authenticates each local line against its admission snapshot, then applies revisions and covered exact withdrawals through the existing fold. Every declared key class participates, including attest. Committed acts and historical journals have the same public output. Retiring a key does not erase its previously admitted evidence. Replay adds nothing. Foreign history grants no local authority. This projection is distinct from authenticated delivery history and does not create a new authority lock. An undeclared standing view refuses by name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lapxo/bound",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "Read the bounds you already wrote, fold them, and name the contradictions.",
6
6
  "main": "dist/cli/verb.js",
@@ -33,7 +33,7 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "@lapxo/obligations": "0.3.5",
36
- "@lapxo/topos": "^0.1.11",
36
+ "@lapxo/topos": "0.1.13",
37
37
  "acorn": "8.19.0"
38
38
  },
39
39
  "devDependencies": {