@lapxo/head 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/TARGET.bound +161 -17
- package/dist/src/ahead/budget.d.ts +6 -0
- package/dist/src/ahead/budget.js +11 -0
- package/dist/src/ahead/fold.d.ts +2 -0
- package/dist/src/ahead/fold.js +19 -0
- package/dist/src/ahead/read.d.ts +10 -0
- package/dist/src/ahead/read.js +18 -0
- package/dist/src/claims/claim.d.ts +9 -0
- package/dist/src/claims/claim.js +40 -0
- package/dist/src/core/answer.d.ts +1 -0
- package/dist/src/core/package.js +4 -3
- package/dist/src/flows/call.js +1 -1
- package/dist/src/flows/read.js +24 -23
- package/dist/src/index.js +10 -6
- package/dist/src/kept/kept.d.ts +30 -0
- package/dist/src/kept/kept.js +20 -0
- package/dist/src/links/inspect.d.ts +8 -0
- package/dist/src/links/inspect.js +41 -0
- package/dist/src/links/link.d.ts +7 -0
- package/dist/src/links/link.js +25 -0
- package/dist/src/marks/compare.d.ts +9 -0
- package/dist/src/marks/compare.js +48 -0
- package/dist/src/ports/bound.d.ts +1 -0
- package/dist/src/ports/bound.js +15 -3
- package/dist/src/ports/documents.js +1 -1
- package/dist/src/ports/reading.d.ts +8 -0
- package/dist/src/ports/reading.js +14 -0
- package/dist/src/ports/spare.js +1 -1
- package/dist/src/ports/warm.js +1 -2
- package/dist/src/sources/connect.js +3 -1
- package/dist/src/tools/claim.d.ts +1 -0
- package/dist/src/tools/claim.js +1 -0
- package/dist/src/verbs/ask.d.ts +1 -1
- package/dist/src/verbs/ask.js +3 -2
- package/package.json +3 -3
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Asked } from '../core/answer.ts';
|
|
2
|
+
import type { Said } from '../core/lock.ts';
|
|
3
|
+
/** Whether a read would spend past what the place allows: a question, past the place's rate; a read ahead, past the share of what the questions leave of it that reads ahead may spend. A read ahead is told by the mark beside its receipt, and counted once. */
|
|
4
|
+
export declare const overOf: (lines: readonly Said[], [family, at, mark]: readonly [string, string, string], source: string, now: number, rate: number, share: number, asked: Asked) => boolean;
|
|
5
|
+
/** The mark a read made ahead lands beside its receipt, under the receipt's own at, so what reads ahead spent is counted apart from what questions spent. */
|
|
6
|
+
export declare const markOf: (asked: Asked, [at, mark]: readonly [string, string], scope: string, digest: string, when: string) => readonly string[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
2
|
+
import { spentOf } from "../core/receipt.js";
|
|
3
|
+
const early = (asked) => asked['ahead'] === 'true';
|
|
4
|
+
/** Whether a read would spend past what the place allows: a question, past the place's rate; a read ahead, past the share of what the questions leave of it that reads ahead may spend. A read ahead is told by the mark beside its receipt, and counted once. */
|
|
5
|
+
export const overOf = (lines, [family, at, mark], source, now, rate, share, asked) => {
|
|
6
|
+
const ahead = lines.filter((l) => l.scope?.startsWith(`${family}/${source}/`) && l.scope.endsWith(`/${mark}`) && l.at?.startsWith(`${at}:`) && now - Date.parse(l.at.slice(at.length + 1)) < 3_600_000).length;
|
|
7
|
+
const calls = spentOf(lines, family, at, source, now) - ahead;
|
|
8
|
+
return calls >= rate || (early(asked) && ahead + 1 > ((rate - (calls - ahead)) * share) / 100);
|
|
9
|
+
};
|
|
10
|
+
/** The mark a read made ahead lands beside its receipt, under the receipt's own at, so what reads ahead spent is counted apart from what questions spent. */
|
|
11
|
+
export const markOf = (asked, [at, mark], scope, digest, when) => early(asked) ? [canonical({ at: `${at}:${when}`, by: 'target', form: 'alphabet', measure: 'digest', role: 'writes', scope: `${scope}/${mark}`, value: digest })] : [];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Cached projections are indexed only by identities returned by the instrument. */
|
|
2
|
+
const kept = new Map();
|
|
3
|
+
/** A refused identity cannot validate an old answer. The reader binds new text to the expected identity. */
|
|
4
|
+
export const folded = async (kind, place, identity, read) => {
|
|
5
|
+
const key = `${kind}\0${place}`, current = await identity(), held = kept.get(key);
|
|
6
|
+
if (current === undefined) {
|
|
7
|
+
kept.delete(key);
|
|
8
|
+
return read();
|
|
9
|
+
}
|
|
10
|
+
if (held?.identity === current)
|
|
11
|
+
return held.folded;
|
|
12
|
+
const reading = read(current);
|
|
13
|
+
kept.set(key, { identity: current, folded: reading });
|
|
14
|
+
const forget = () => { if (kept.get(key)?.folded === reading)
|
|
15
|
+
kept.delete(key); };
|
|
16
|
+
reading.then((result) => { if (result && typeof result === 'object' && 'refused' in result)
|
|
17
|
+
forget(); }, forget);
|
|
18
|
+
return reading;
|
|
19
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from '../core/answer.ts';
|
|
2
|
+
import type { Cell } from '../ports/surface.ts';
|
|
3
|
+
/**
|
|
4
|
+
* What an answer leaves to do before the next call, done after it is answered; another call coming gives up what is not
|
|
5
|
+
* begun, never what is, so no land is cut halfway:
|
|
6
|
+
* the reads a known next asks for, read in a context that asks the person nothing — a read only, never an act, nothing it
|
|
7
|
+
* would need a yes or a secret for — and then every place the answer names folded again where it moved, so the next
|
|
8
|
+
* question finds it folded. A fold calls no source, so it spends nothing of a place's rate.
|
|
9
|
+
*/
|
|
10
|
+
export declare const readAhead: (cells: readonly Cell[], context: Context, signal: AbortSignal) => Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readSource } from "../flows/read.js";
|
|
2
|
+
const none = async () => 'none', nothing = async () => undefined;
|
|
3
|
+
/**
|
|
4
|
+
* What an answer leaves to do before the next call, done after it is answered; another call coming gives up what is not
|
|
5
|
+
* begun, never what is, so no land is cut halfway:
|
|
6
|
+
* the reads a known next asks for, read in a context that asks the person nothing — a read only, never an act, nothing it
|
|
7
|
+
* would need a yes or a secret for — and then every place the answer names folded again where it moved, so the next
|
|
8
|
+
* question finds it folded. A fold calls no source, so it spends nothing of a place's rate.
|
|
9
|
+
*/
|
|
10
|
+
export const readAhead = async (cells, context, signal) => {
|
|
11
|
+
const quiet = { ...context, elicit: none, person: { consent: none, enter: nothing, choose: nothing, visit: nothing } };
|
|
12
|
+
for (const { next } of cells)
|
|
13
|
+
if (!signal.aborted && 'tool' in next && next.tool === 'ask' && next.args['source'] && next.args['operation'] && !next.args['why'] && !next.args['history'])
|
|
14
|
+
await readSource({ ...next.args, ahead: 'true' }, quiet);
|
|
15
|
+
await quiet.settle();
|
|
16
|
+
for (const place of new Set(cells.flatMap((c) => [c.fields['place'] ?? '', 'tool' in c.next ? c.next.args['place'] ?? '' : '']).filter((p) => p && !signal.aborted)))
|
|
17
|
+
await Promise.all([context.standing(place), context.policy(place)]).catch(() => undefined);
|
|
18
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Asked, type Context } from '../core/answer.ts';
|
|
2
|
+
/**
|
|
3
|
+
* What the person states, kept as theirs: a line their own key attests, their words exactly as given, in the place named;
|
|
4
|
+
* asked once a conversation whether what they state there is kept, and each statement said back as it goes in, so they see
|
|
5
|
+
* it, with who carried it here as the client says it is. Taken back by the same words: what a person attests is evidence, and
|
|
6
|
+
* stands for good, so taking it back is attesting that, beside it. A place whose lines admit no carrier is handed the line that does.
|
|
7
|
+
* Only this action keeps one, never a cell, an answer or a read ahead, so nothing the model concluded becomes the person's.
|
|
8
|
+
*/
|
|
9
|
+
export declare const claimed: (asked: Asked, context: Context) => Promise<readonly string[]>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
3
|
+
import { inPlace, standingOr, unless } from "../core/answer.js";
|
|
4
|
+
import { uncoveredIn } from "../core/device.js";
|
|
5
|
+
import { aboutOf, members, valueOf } from "../core/lock.js";
|
|
6
|
+
import { fill, sayIn } from "../core/surface.js";
|
|
7
|
+
/** Whether the person said, in this conversation, that what they state in a place is kept as theirs: asked once a place, a no kept as a yes is. */
|
|
8
|
+
const answered = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* What the person states, kept as theirs: a line their own key attests, their words exactly as given, in the place named;
|
|
11
|
+
* asked once a conversation whether what they state there is kept, and each statement said back as it goes in, so they see
|
|
12
|
+
* it, with who carried it here as the client says it is. Taken back by the same words: what a person attests is evidence, and
|
|
13
|
+
* stands for good, so taking it back is attesting that, beside it. A place whose lines admit no carrier is handed the line that does.
|
|
14
|
+
* Only this action keeps one, never a cell, an answer or a read ahead, so nothing the model concluded becomes the person's.
|
|
15
|
+
*/
|
|
16
|
+
export const claimed = (asked, context) => inPlace(asked, context, async (place) => {
|
|
17
|
+
const [say, word, said, held] = [sayIn(context.lock), (k) => valueOf(context.lock, k), (asked['said'] ?? '').trim(), await standingOr(context, place)];
|
|
18
|
+
if ('said' in held)
|
|
19
|
+
return held.said;
|
|
20
|
+
if (!said)
|
|
21
|
+
return [say('missing-param', { operation: 'claim', param: 'said' })];
|
|
22
|
+
const [family, withdraw, back] = [`${word('claim/family')}/${createHash('sha256').update(said).digest('hex').slice(0, 16)}`, asked['withdraw'] === 'true', word('claim/withdrawn')];
|
|
23
|
+
const standing = held.lines.filter((l) => l.scope?.startsWith(`${family}/`) && !l.scope.endsWith(`/${back}`) && !held.lines.some((w) => w.scope === `${l.scope}/${back}`)).map((l) => l.scope);
|
|
24
|
+
if (withdraw && !standing.length)
|
|
25
|
+
return [say('claim-none', { place, said })];
|
|
26
|
+
const wire = held.lines.find((l) => l.scope === 'wire/fields'), via = word('claim/via');
|
|
27
|
+
const bare = wire && Object.fromEntries(Object.entries(wire).filter(([k]) => !['sig', 'epoch', 'about'].includes(k)));
|
|
28
|
+
if (bare && !members(wire.value ?? '').includes(via))
|
|
29
|
+
return [say('claims-unadmitted', { place, lines: [canonical({ ...bare, by: 'target', value: 'withdraw' }), canonical({ ...bare, by: 'target', at: `policy:head/wire/${new Date(context.now()).toISOString()}`, value: `${wire.value}|${via}` })].join('\n') })];
|
|
30
|
+
const line = (scope) => canonical({ about: said, at: word('claim/at'), by: 'target', form: 'alphabet', measure: 'text', role: 'writes', scope, value: 'lock', [via]: context.client() ?? word('claim/via-unknown') });
|
|
31
|
+
const lot = withdraw ? standing.map((scope) => line(`${scope}/${back}`)) : [line(`${family}/${new Date(context.now()).toISOString().replace(/\D/g, '')}`)], outside = uncoveredIn(context.lock, held.lines, context.device('person'), place, lot);
|
|
32
|
+
if (outside)
|
|
33
|
+
return outside;
|
|
34
|
+
if (!withdraw && !answered.has(place))
|
|
35
|
+
answered.set(place, (await unless(context, fill(aboutOf(context.lock, 'said/elicit-claims'), { place }))) === undefined);
|
|
36
|
+
if (!withdraw && !answered.get(place))
|
|
37
|
+
return [say('claims-declined', { place, said })];
|
|
38
|
+
const landed = await context.landAs(place, lot, 'person', held.lines);
|
|
39
|
+
return 'refused' in landed ? [say('not-landed', { why: landed.refused })] : [say(withdraw ? 'unclaimed' : 'claimed', { place, said }), ...landed.lines];
|
|
40
|
+
});
|
|
@@ -15,6 +15,7 @@ export interface Context {
|
|
|
15
15
|
readonly lock: readonly Said[];
|
|
16
16
|
readonly bound: (cwd: string, args: readonly string[]) => Promise<Printed | undefined>;
|
|
17
17
|
readonly roots: () => Promise<readonly string[]>;
|
|
18
|
+
readonly client: () => string | undefined;
|
|
18
19
|
readonly keys: KeyStore;
|
|
19
20
|
readonly device: (role: string) => string | undefined;
|
|
20
21
|
readonly standing: (place: string) => Promise<{
|
package/dist/src/core/package.js
CHANGED
|
@@ -5,6 +5,7 @@ import { members, under, valueOf } from "./lock.js";
|
|
|
5
5
|
import { fieldsOf, vocabOf } from "./read.js";
|
|
6
6
|
import { sayIn } from "./surface.js";
|
|
7
7
|
import { digestIn, linesOf } from "./worlds.js";
|
|
8
|
+
import { linkOf } from "../links/link.js";
|
|
8
9
|
/** A line no package carries: one that is not a world, or a source's document, overlay or pin, said once by digest — with, beside a digest that names bytes, the encrypted address they are read at. */
|
|
9
10
|
export const foreignIn = (lines) => lines.find((l) => !/^(sources\/[a-z0-9-]+(\/overlay)?|uses\/[a-z0-9-]+)$/.test(l.scope ?? '') || lines.some((o) => o !== l && o.scope === l.scope && o.measure === l.measure)
|
|
10
11
|
|| (l.measure !== 'digest' && !(l.measure === 'id' && URL.canParse(l.value ?? '') && lines.some((o) => o.scope === l.scope && o.measure === 'digest' && o.needs))));
|
|
@@ -27,13 +28,13 @@ export const shared = (asked, context) => inPlace(asked, context, async (place)
|
|
|
27
28
|
return [say('share-to', { place, to })];
|
|
28
29
|
if ('said' in held)
|
|
29
30
|
return held.said;
|
|
30
|
-
const
|
|
31
|
+
const urls = ((u) => (u && typeof u === 'object' ? u : {}))((() => { try {
|
|
31
32
|
return JSON.parse(asked['urls'] || '{}');
|
|
32
33
|
}
|
|
33
34
|
catch {
|
|
34
35
|
return {};
|
|
35
|
-
} })())), bytes = new TextEncoder().encode(`${pack.lines.join('\n')}\n`);
|
|
36
|
-
return !pack.names.length ? [say('share-none', { place })] : ([...pack.files, [LOCK, bytes]].forEach(([at, laid]) => context.files.lay(to, at, laid)), [say('shared', { place, to, sources: pack.names.join(', '), digest: digestIn(context.lock)(bytes) }), ...pack.lines]);
|
|
36
|
+
} })()), pack = packed(context, place, held.lines, urls), bytes = new TextEncoder().encode(`${pack.lines.join('\n')}\n`);
|
|
37
|
+
return !pack.names.length ? [say('share-none', { place })] : ([...pack.files, [LOCK, bytes]].forEach(([at, laid]) => context.files.lay(to, at, laid)), [say('shared', { place, to, sources: pack.names.join(', '), digest: digestIn(context.lock)(bytes) }), ...linkOf(context, to, pack.lines, urls), ...pack.lines]);
|
|
37
38
|
});
|
|
38
39
|
/** What a package names, checked whole before anything is asked: every line a world or a source's document, overlay or pin, by digest; each world the one this head pins; an order its pins rest in. A source whose bytes did not arrive, or did not hash to their line, is open, and so is every source resting on it: absence is not a refusal, and the rest arrives. */
|
|
39
40
|
export const unpacked = (context, document, bytes, read) => {
|
package/dist/src/flows/call.js
CHANGED
|
@@ -64,7 +64,7 @@ export const called = async (asked, context, planOf, then) => {
|
|
|
64
64
|
if ('refused' in prepared)
|
|
65
65
|
return say(prepared.refused, { source, origin: plan.origin });
|
|
66
66
|
const most = (scope) => Number((lines.find((l) => l.scope === scope)?.value ?? '0..0').split('..')[1]);
|
|
67
|
-
const typed = async (status, bytes) => ((got) => ['lines' in got ? say('answered', { source, operation, status })[0] : say('untyped', { source, operation, status, why: got.untyped })[0], ...('lines' in got ? got.lines : [])])(await typedAnswer(context, place, lines, source, operation, prepared.key, bytes));
|
|
67
|
+
const typed = async (status, bytes) => ((got, at) => ['lines' in got ? say('answered', { source, operation, status, at })[0] : say('untyped', { source, operation, status, at, why: got.untyped })[0], ...('lines' in got ? got.lines : [])])(await typedAnswer(context, place, lines, source, operation, prepared.key, bytes), new Date(context.now()).toISOString());
|
|
68
68
|
const lands = (...lot) => context.landAs(place, lot, 'head', lines);
|
|
69
69
|
const attach = async () => ((got) => ('said' in got ? { said: say(got.said, { source, operation, kinds: plan.access.kind, input: got.input }) } : got))(await attachmentFor(context, place, lines, plan));
|
|
70
70
|
const uncovered = (role, scopes) => uncoveredIn(context.lock, lines, context.device(role), place, [], scopes);
|
package/dist/src/flows/read.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { under } from "../core/lock.js";
|
|
2
2
|
import { observedAt, planRead } from "../core/read.js";
|
|
3
|
-
import { basisOf, draftOf, receiptsOf, scopeOf
|
|
3
|
+
import { basisOf, draftOf, receiptsOf, scopeOf } from "../core/receipt.js";
|
|
4
4
|
import { redIn, sayIn } from "../core/surface.js";
|
|
5
5
|
import { grantedOf, isOpen, untilOf } from "../core/act.js";
|
|
6
6
|
import { canonical } from '@lapxo/topos/wire';
|
|
@@ -8,6 +8,8 @@ import { called } from "./call.js";
|
|
|
8
8
|
import { principalOf } from "../access/obtain.js";
|
|
9
9
|
import { jsonOf } from "../access/contract.js";
|
|
10
10
|
import { refusedAccess } from "../sources/refused.js";
|
|
11
|
+
import { hand, keptOf } from "../kept/kept.js";
|
|
12
|
+
import { markOf, overOf } from "../ahead/budget.js";
|
|
11
13
|
/** An intent an earlier act left open, closed by the read that looked at what it would have changed: a line naming that read-s digest, signed by head. */
|
|
12
14
|
const closeWith = async (intent, source, digest, { word, lines, lands }, context) => {
|
|
13
15
|
const say = sayIn(context.lock);
|
|
@@ -30,7 +32,7 @@ export const readSource = (asked, context) => called(asked, context, planRead, a
|
|
|
30
32
|
const closing = (digest) => closeWith(asked['closes'], source, digest, { word, lines, lands }, context);
|
|
31
33
|
const scopes = (principal) => [scopeOf(family, source, operation, prepared.key, principal), `${scopeOf(word('receipt/kept'), source, operation, prepared.key, principal)}/${basisOf(lines, word, source)}`];
|
|
32
34
|
const held = (digest) => ((bytes) => (bytes === undefined || digest === undefined ? undefined : { digest, bytes }))(digest === undefined ? undefined : context.cache.held(digest));
|
|
33
|
-
const keeping = (kept, answer) => (answer.length > 1 ? [draftOf(word('receipt/kept'), at, kept, context.cache.keep(Buffer.from(answer.join('\n'))), when)] : []);
|
|
35
|
+
const keeping = (kept, answer) => (answer.length > 1 ? [draftOf(word('receipt/kept'), at, kept, (asked['ahead'] === 'true' ? String : hand)(context.cache.keep(Buffer.from(answer.join('\n')))), when)] : []);
|
|
34
36
|
const allowed = (key, measure) => (lines.find((l) => l.scope === `${word(key)}/${source}` && l.measure === measure) ?? lines.find((l) => l.scope === word(key) && l.measure === measure))?.value;
|
|
35
37
|
if (asked['why'] === 'true') {
|
|
36
38
|
const asOf = principalOf(context, plan, false), [newest] = asOf === undefined ? [] : receiptsOf(lines, at, scopes(asOf)[0]), pin = lines.find((l) => l.scope === `uses/${source}`);
|
|
@@ -54,36 +56,35 @@ export const readSource = (asked, context) => called(asked, context, planRead, a
|
|
|
54
56
|
if (stopped)
|
|
55
57
|
return stopped;
|
|
56
58
|
if (!grantedOf(lines, word('grant/family'), source, context.now())) {
|
|
59
|
+
if (asked['ahead'] === 'true')
|
|
60
|
+
return [];
|
|
57
61
|
const sources = [...new Set(lines.filter((l) => l.scope?.startsWith(`${word('vocab/region')}/`) && l.measure === word('vocab/origins')).map((l) => l.scope.split('/')[1]))].sort();
|
|
58
62
|
const until = untilOf(word('access/period'), context.now());
|
|
59
|
-
const line = (measure, value) => canonical({ at:
|
|
60
|
-
const
|
|
63
|
+
const line = (measure, value) => canonical({ at: `policy:person/grants/${new Date(context.now()).toISOString()}`, by: 'target', form: 'alphabet', measure, role: 'writes', scope: word('grant/family'), value });
|
|
64
|
+
const before = [...new Set(lines.filter((l) => l.scope === word('grant/family')).map(({ about: _about, sig: _sig, epoch: _epoch, ...l }) => canonical({ ...l, by: 'target', value: 'withdraw' })))];
|
|
65
|
+
const granted = await widen(say('grant', { sources: sources.join(', '), place, until })[0], [...before, line('sources', sources.join('|')), line('until', until)]);
|
|
61
66
|
if ('said' in granted)
|
|
62
67
|
return granted.said;
|
|
63
68
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (body !== undefined) {
|
|
73
|
-
const cut = body.bytes.indexOf(10);
|
|
74
|
-
const answer = await typed(Buffer.from(body.bytes.subarray(0, cut)).toString('utf8'), body.bytes.subarray(cut + 1));
|
|
75
|
-
const keptLines = keeping(kept, answer);
|
|
76
|
-
const landed = keptLines.length ? await lands(...keptLines) : { lines: [] };
|
|
77
|
-
return [...answer, ...say('reread', { scope, digest: body.digest, at: newest.at }), ...('lines' in landed ? landed.lines : []), ...(await closing(body.digest))];
|
|
78
|
-
}
|
|
69
|
+
const was = principal === undefined ? undefined : keptOf(lines, at, scopes(principal), grantedOf(lines, word('grant/family'), source, context.now()), allowed('reuse/family', 'seconds'), context.now(), held);
|
|
70
|
+
if (was?.kind === 'kept')
|
|
71
|
+
return [...(was.bytes ? [...new TextDecoder().decode(was.bytes).split('\n'), ...say('reused', { scope: scopes(principal)[1], digest: hand(was.digest), at: was.at, receipt: was.receipt })] : say('kept', { source, operation, at: was.at, digest: was.digest, receipt: was.receipt })),
|
|
72
|
+
...(await closing(was.receipt)), ...redIn(context.lock)('answer/kept', `answer/${source}/${operation}`, context.begun, context.machine())];
|
|
73
|
+
if (was?.kind === 'reread') {
|
|
74
|
+
const answer = await typed(Buffer.from(was.bytes.subarray(0, was.bytes.indexOf(10))).toString('utf8'), was.bytes.subarray(was.bytes.indexOf(10) + 1)), keptLines = keeping(scopes(principal)[1], answer);
|
|
75
|
+
const landed = keptLines.length ? await lands(...keptLines) : { lines: [] };
|
|
76
|
+
return [...answer, ...say('reread', { scope: scopes(principal)[0], digest: was.digest, at: was.at, receipt: was.digest }), ...('lines' in landed ? landed.lines : []), ...(await closing(was.digest))];
|
|
79
77
|
}
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
const again = was?.kind === 'again' ? say(`kept-${was.why}`, { source, operation, at: was.at }) : [];
|
|
79
|
+
if (overOf(lines, [family, at, word('ahead/mark')], source, context.now(), most('read/rate'), Number(word('ahead/share').split('..')[1]), asked))
|
|
80
|
+
return asked['ahead'] === 'true' ? [] : say('rate', { source });
|
|
82
81
|
const attached = await attach();
|
|
83
82
|
if ('said' in attached)
|
|
84
83
|
return attached.said;
|
|
85
84
|
const [scope, kept] = scopes(attached.principal ?? '');
|
|
86
85
|
const got = await send(attached.attachment);
|
|
86
|
+
if (asked['ahead'] === 'true' && (got.kind !== 'got' || got.status >= 300))
|
|
87
|
+
return [];
|
|
87
88
|
if (got.kind !== 'got')
|
|
88
89
|
return say(got.kind === 'over' ? 'too-large' : 'unreachable-source', { source });
|
|
89
90
|
if (got.status === 401 && attached.principal !== 'anonymous')
|
|
@@ -93,6 +94,6 @@ export const readSource = (asked, context) => called(asked, context, planRead, a
|
|
|
93
94
|
const observed = plan.access.probe && got.status < 300 ? [canonical({ at: `${at}:${when}`, by: 'target', form: 'alphabet', measure: word('vocab/access'), role: 'writes', scope: observedAt(context.lock, source, operation), value: word('vocab/anonymous') })] : [];
|
|
94
95
|
const found = plan.access.probe && got.status === 401 && got.authenticate ? await (await context.access(word('access/adapter')))?.discover?.(plan.origin, got.authenticate, async (url) => jsonOf((await context.files.read.documentOf(context, place, url))?.bytes ?? new Uint8Array())) : undefined;
|
|
95
96
|
const by = `${word('access/family')}/${source}/${word('access/authorizer')}`, authorizer = Object.entries(found ?? {}).flatMap(([k, f]) => Object.entries(f).map(([measure, all]) => canonical({ at: `${at}:${when}`, by: 'target', form: 'alphabet', measure, role: 'writes', scope: k ? `${by}/${k}` : by, value: all.join('|') })));
|
|
96
|
-
const landed = await lands(draftOf(family, at, scope, digest, when), ...(attached.principal === undefined ? [] : keeping(kept, answer)), ...observed, ...authorizer);
|
|
97
|
-
return [...answer, ...('lines' in landed ? [...landed.lines, ...(await closing(digest))] : say('not-landed', { why: landed.refused })), ...(found?.['']?.['issuer'] && 'lines' in landed ? say('authorizer-observed', { source, issuer: found['']['issuer'][0] }) : [])];
|
|
97
|
+
const landed = await lands(draftOf(family, at, scope, digest, when), ...markOf(asked, [at, word('ahead/mark')], scope, digest, when), ...(attached.principal === undefined ? [] : keeping(kept, answer)), ...observed, ...authorizer);
|
|
98
|
+
return [...again, ...answer, ...('lines' in landed ? [...landed.lines, ...(await closing(digest))] : say('not-landed', { why: landed.refused })), ...(found?.['']?.['issuer'] && 'lines' in landed ? say('authorizer-observed', { source, issuer: found['']['issuer'][0] }) : [])];
|
|
98
99
|
});
|
package/dist/src/index.js
CHANGED
|
@@ -27,18 +27,21 @@ import { keychain } from "./ports/keychain.js";
|
|
|
27
27
|
import { algorithmOf, deviceOf, idIn } from "./core/device.js";
|
|
28
28
|
import { publicOf } from "./ports/keys.js";
|
|
29
29
|
import { personOf } from "./ports/access/person.js";
|
|
30
|
+
import { readAhead } from "./ahead/read.js";
|
|
30
31
|
const [ext, root] = ((self) => [self.slice(self.lastIndexOf('.')), fileURLToPath(new URL(self.endsWith('.ts') ? '..' : '../..', self))])(import.meta.url);
|
|
31
32
|
const lock = lockAt(root);
|
|
32
33
|
const surface = surfaceOf(lock);
|
|
33
34
|
const runs = valueOf(lock, 'runs');
|
|
34
|
-
Object.assign(words, Object.fromEntries(['read', 'view', 'sign', 'land', 'key', 'signer', 'pin', 'topos', 'standing', 'evidence', 'act', 'profile', 'receipts', 'refusal'].map((w) => [w, valueOf(lock, `bound/${w}`)])));
|
|
35
|
+
Object.assign(words, Object.fromEntries(['read', 'view', 'sign', 'land', 'key', 'signer', 'pin', 'topos', 'standing', 'evidence', 'act', 'profile', 'receipts', 'refusal', 'identity'].map((w) => [w, valueOf(lock, `bound/${w}`)])));
|
|
35
36
|
Object.assign(limits, { wait: Number(valueOf(lock, 'bound/wait').split('..')[1]), most: Number(valueOf(lock, 'bound/printed').split('..')[1]) });
|
|
36
37
|
Object.assign(warmth, { spares: Number(valueOf(lock, 'bound/spares').split('..')[1]), boot: valueOf(lock, 'bound/spare-env').split('|') });
|
|
37
38
|
/** One call at a time, each after the one before it and never on a timer, so one asked while another lands sees its receipt; bound started ahead for it, and nothing of it kept once it is answered. */
|
|
38
|
-
let last = Promise.resolve();
|
|
39
|
+
let [last, ahead] = [Promise.resolve(), new AbortController()];
|
|
39
40
|
const turn = (act) => ((next) => { last = next.catch(() => undefined); return next; })(last.then(() => { void warm(runs); return act().finally(cooled); }));
|
|
41
|
+
for (const end of ['SIGTERM', 'SIGINT'])
|
|
42
|
+
process.once(end, () => void last.finally(() => process.exit(0)));
|
|
40
43
|
const offered = (n) => (n ? { listChanged: false } : undefined);
|
|
41
|
-
const server = new Server({ name: surface.name, version: surface.version }, { capabilities: { tools: offered(surface.tools.length), resources: offered(surface.resources.length), prompts: offered(surface.prompts.length) } });
|
|
44
|
+
const server = new Server({ name: surface.name, version: surface.version }, { capabilities: { tools: offered(surface.tools.length), resources: offered(surface.resources.length), prompts: offered(surface.prompts.length) }, instructions: aboutOf(lock, 'prose/en/instructions') });
|
|
42
45
|
/** The person, asked as the client lets head ask; every wait on them is counted, so a time ceiling measures head's work and never the person's. */
|
|
43
46
|
let waited = 0;
|
|
44
47
|
const timed = (ask) => async (...args) => { const t = Date.now(); try {
|
|
@@ -103,7 +106,7 @@ const contextOf = (signal) => {
|
|
|
103
106
|
begun: Date.now() - waited,
|
|
104
107
|
keys,
|
|
105
108
|
device,
|
|
106
|
-
roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []),
|
|
109
|
+
roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []), client: () => ((c) => (c ? `${c.name}@${c.version}` : undefined))(server.getClientVersion()),
|
|
107
110
|
};
|
|
108
111
|
};
|
|
109
112
|
const askedOf = (raw) => Object.fromEntries(Object.entries(raw ?? {}).map(([k, v]) => [k, v === undefined ? undefined : typeof v === 'object' ? JSON.stringify(v) : String(v)]));
|
|
@@ -113,14 +116,15 @@ if (surface.tools.length)
|
|
|
113
116
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: surface.tools.map((tool) => ({ name: tool.name, description: tool.about,
|
|
114
117
|
inputSchema: schemaOf([...tool.fields]), outputSchema: shapeSchema(lock), annotations: Object.fromEntries(tool.hints.map((hint) => [`${hint}Hint`, true])) })) }));
|
|
115
118
|
if (surface.tools.length)
|
|
116
|
-
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => turn(async () => {
|
|
119
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => (ahead.abort(), turn(async () => {
|
|
117
120
|
const tool = surface.tools.find((one) => one.name === request.params.name);
|
|
118
121
|
if (tool === undefined)
|
|
119
122
|
return { isError: true, content: [] };
|
|
120
123
|
const refused = refusedOn(lock, process.platform);
|
|
121
124
|
const context = contextOf(extra.signal), answer = refused !== undefined ? [refused] : extra.signal.aborted ? [] : [...await (await answerOf('tools', tool.name))(askedOf(request.params.arguments), context), ...await context.settle()];
|
|
125
|
+
void ((early) => turn(() => readAhead(shapeOf(answer).cells, contextOf(), early.signal)))(ahead = new AbortController());
|
|
122
126
|
return { content: [{ type: 'text', text: answer.join('\n') }], structuredContent: shapeOf(answer) };
|
|
123
|
-
}));
|
|
127
|
+
})));
|
|
124
128
|
if (surface.resources.length)
|
|
125
129
|
server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
|
|
126
130
|
if (surface.resources.length)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Said } from '../core/lock.ts';
|
|
2
|
+
export declare const hand: (digest: string) => string;
|
|
3
|
+
type Held = (digest: string | undefined) => {
|
|
4
|
+
readonly digest: string;
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
} | undefined;
|
|
7
|
+
export type Was = {
|
|
8
|
+
readonly kind: 'kept';
|
|
9
|
+
readonly at: string;
|
|
10
|
+
readonly digest: string;
|
|
11
|
+
readonly receipt: string;
|
|
12
|
+
readonly bytes?: Uint8Array;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'reread';
|
|
15
|
+
readonly at: string;
|
|
16
|
+
readonly digest: string;
|
|
17
|
+
readonly bytes: Uint8Array;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'again';
|
|
20
|
+
readonly at: string;
|
|
21
|
+
readonly why: 'withdrawn' | 'changed' | 'expired';
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* A question read before, as its receipts and the place stand now. Where the place declares a reuse and the newest read
|
|
25
|
+
* stood, it is kept — its bytes only where this conversation does not hold them yet — or typed again from its receipt if
|
|
26
|
+
* nothing typed was kept; but read again, and why, where the person's grant had to be given again, the source is pinned
|
|
27
|
+
* otherwise than when it was read, or the read is older than the reuse allows. Never read, or no reuse: nothing is said.
|
|
28
|
+
*/
|
|
29
|
+
export declare const keptOf: (lines: readonly Said[], at: string, [read, kept]: readonly [string, string], granted: boolean, reuse: string | undefined, now: number, held: Held) => Was | undefined;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { receiptsOf } from "../core/receipt.js";
|
|
2
|
+
/** The typed answers this conversation already holds, by digest: head is one process a conversation, so they last as long as it. */
|
|
3
|
+
const handed = new Set();
|
|
4
|
+
export const hand = (digest) => (handed.add(digest), digest);
|
|
5
|
+
/**
|
|
6
|
+
* A question read before, as its receipts and the place stand now. Where the place declares a reuse and the newest read
|
|
7
|
+
* stood, it is kept — its bytes only where this conversation does not hold them yet — or typed again from its receipt if
|
|
8
|
+
* nothing typed was kept; but read again, and why, where the person's grant had to be given again, the source is pinned
|
|
9
|
+
* otherwise than when it was read, or the read is older than the reuse allows. Never read, or no reuse: nothing is said.
|
|
10
|
+
*/
|
|
11
|
+
export const keptOf = (lines, at, [read, kept], granted, reuse, now, held) => {
|
|
12
|
+
const [newest] = receiptsOf(lines, at, read), body = held(newest?.digest), window = Number(/^\d+\.\.(\d+)$/.exec(reuse ?? '')?.[1] ?? Number.NaN);
|
|
13
|
+
if (newest === undefined || body === undefined || Number.isNaN(window) || Number(Buffer.from(body.bytes.subarray(0, body.bytes.indexOf(10))).toString('utf8')) >= 300)
|
|
14
|
+
return undefined;
|
|
15
|
+
const typed = receiptsOf(lines, at, kept.slice(0, kept.lastIndexOf('/')), true).filter((k) => k.at >= newest.at), answered = held(typed.find((k) => k.scope === kept)?.digest);
|
|
16
|
+
const why = !granted ? 'withdrawn' : typed.length && !typed.some((k) => k.scope === kept) ? 'changed' : now - Date.parse(newest.at) > window * 1000 ? 'expired' : undefined;
|
|
17
|
+
if (why !== undefined)
|
|
18
|
+
return { kind: 'again', at: newest.at, why };
|
|
19
|
+
return answered === undefined ? { kind: 'reread', at: newest.at, digest: body.digest, bytes: body.bytes } : { kind: 'kept', at: newest.at, digest: answered.digest, receipt: newest.digest, ...(handed.has(answered.digest) ? {} : { bytes: answered.bytes }) };
|
|
20
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Asked, type Context } from '../core/answer.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A #bound: link connected: inspected by the instrument in the person's place, never authenticated — its bytes are the
|
|
4
|
+
* link's, and nothing shows who shared it — and what it names is shown; nothing is connected or granted, since a package
|
|
5
|
+
* is received by the digest its sharer sent apart. A place that admits no content transport is handed the lines that admit
|
|
6
|
+
* one, to sign; bytes that do not arrive are owed by the link until they do; bytes that are not what it names are refused.
|
|
7
|
+
*/
|
|
8
|
+
export declare const linked: (asked: Asked, context: Context) => Promise<readonly string[]>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { decodeReference } from '@lapxo/bound/reference';
|
|
2
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
3
|
+
import { inPlace, standingOr } from "../core/answer.js";
|
|
4
|
+
import { members, valueOf } from "../core/lock.js";
|
|
5
|
+
import { sayIn } from "../core/surface.js";
|
|
6
|
+
import { linesOf } from "../core/worlds.js";
|
|
7
|
+
import { rootIn } from "../flows/device.js";
|
|
8
|
+
/**
|
|
9
|
+
* A #bound: link connected: inspected by the instrument in the person's place, never authenticated — its bytes are the
|
|
10
|
+
* link's, and nothing shows who shared it — and what it names is shown; nothing is connected or granted, since a package
|
|
11
|
+
* is received by the digest its sharer sent apart. A place that admits no content transport is handed the lines that admit
|
|
12
|
+
* one, to sign; bytes that do not arrive are owed by the link until they do; bytes that are not what it names are refused.
|
|
13
|
+
*/
|
|
14
|
+
export const linked = (asked, context) => inPlace(asked, context, async (place) => {
|
|
15
|
+
const [say, word, link] = [sayIn(context.lock), (k) => valueOf(context.lock, k), asked['document'] ?? ''], [held, admits] = [await standingOr(context, place, context.policy), members(valueOf(context.lock, 'link/transport'))];
|
|
16
|
+
if ('said' in held)
|
|
17
|
+
return held.said;
|
|
18
|
+
const lines = context.lock.filter((l) => admits.some((s) => l.scope === `birth/${s}`)).map(({ about: _about, scope, ...l }) => canonical({ ...l, scope: scope.slice('birth/'.length), at: 'policy:person/transport', by: 'target' }));
|
|
19
|
+
let reference;
|
|
20
|
+
try {
|
|
21
|
+
reference = decodeReference(link);
|
|
22
|
+
}
|
|
23
|
+
catch (refused) {
|
|
24
|
+
return [say('link-refused', { link, why: refused instanceof Error ? refused.message : String(refused) })];
|
|
25
|
+
}
|
|
26
|
+
if (![admits[0], `transport/${new URL(reference.source).protocol.slice(0, -1)}`].every((s) => held.lines.some((l) => l.scope === s)))
|
|
27
|
+
return [say('transport-missing', { place, lines: lines.join('\n') })];
|
|
28
|
+
const [printed, scope] = [await context.bound(place, [word('bound/read'), link]), `${word('place/needs')}/bytes/${reference.blob.slice(reference.algorithm.length + 1, reference.algorithm.length + 17)}`];
|
|
29
|
+
const [owed, root, why] = [held.lines.find((l) => l.scope === scope && l.role === 'demands' && l.value === link), rootIn(context, place), printed?.err.filter((l) => !l.startsWith('{')).at(-1) ?? ''];
|
|
30
|
+
if ('said' in root)
|
|
31
|
+
return root.said;
|
|
32
|
+
if (printed?.code !== 0 && !why.includes(word('link/unarrived')))
|
|
33
|
+
return [say('link-refused', { link, why })];
|
|
34
|
+
const settled = (lot) => (lot.length ? context.landRoot(place, lot, root.account).then((got) => ('refused' in got ? say('not-landed', { why: got.refused }) : got.lines)) : Promise.resolve([]));
|
|
35
|
+
const due = { at: 'policy:head/link', by: 'target', form: 'alphabet', measure: 'id', role: 'demands', scope, value: link };
|
|
36
|
+
if (printed?.code !== 0)
|
|
37
|
+
return [say('link-owed', { place, source: reference.source }), ...await settled(owed ? [] : [canonical(due)])];
|
|
38
|
+
const named = linesOf(new TextEncoder().encode(printed.out.filter((l) => l.startsWith('TOPOS ')).map((l) => l.slice('TOPOS '.length)).join('\n')));
|
|
39
|
+
const sources = named.filter((l) => /^sources\/[^/]+$/.test(l.scope ?? '')).map((l) => l.scope.split('/')[1]);
|
|
40
|
+
return [say('inspected', { source: reference.source, sources: sources.join(', ') || '—', digest: reference.digest }), say('sharer-unverified', {}), ...await settled(owed ? [canonical({ ...due, value: 'withdraw' })] : [])];
|
|
41
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Context } from '../core/answer.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A package's link: its lines as one standing document, laid in its folder as it is to be published — the link names that
|
|
4
|
+
* document's bytes, never the package's files — and the #bound: link to it at the address the person publishes it at.
|
|
5
|
+
* With no address there is no link, said so: a link needs where its bytes are published, and the digest still goes apart.
|
|
6
|
+
*/
|
|
7
|
+
export declare const linkOf: (context: Context, to: string, lines: readonly string[], urls: Readonly<Record<string, string>>) => readonly string[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { encodeReference } from '@lapxo/bound/reference';
|
|
4
|
+
import { semanticStandingBytes } from '@lapxo/topos/standing';
|
|
5
|
+
import { members, valueOf } from "../core/lock.js";
|
|
6
|
+
import { sayIn } from "../core/surface.js";
|
|
7
|
+
const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
8
|
+
/**
|
|
9
|
+
* A package's link: its lines as one standing document, laid in its folder as it is to be published — the link names that
|
|
10
|
+
* document's bytes, never the package's files — and the #bound: link to it at the address the person publishes it at.
|
|
11
|
+
* With no address there is no link, said so: a link needs where its bytes are published, and the digest still goes apart.
|
|
12
|
+
*/
|
|
13
|
+
export const linkOf = (context, to, lines, urls) => {
|
|
14
|
+
const say = sayIn(context.lock), [name, profile, [kind, algorithm]] = [valueOf(context.lock, 'link/document'), valueOf(context.lock, 'link/profile'), members(valueOf(context.lock, 'link/reference'))];
|
|
15
|
+
const standing = semanticStandingBytes(lines), bytes = new TextEncoder().encode(JSON.stringify({ profile, standing })), at = urls[name], document = join(to, name);
|
|
16
|
+
context.files.lay(to, name, bytes);
|
|
17
|
+
if (at === undefined || !URL.canParse(at))
|
|
18
|
+
return [say('shared-unlinked', { document })];
|
|
19
|
+
try {
|
|
20
|
+
return [say('shared-link', { document, at, link: encodeReference({ class: kind, algorithm, digest: sha(standing), blob: `${algorithm}:${sha(bytes)}`, source: new URL(at).href }) })];
|
|
21
|
+
}
|
|
22
|
+
catch (refused) {
|
|
23
|
+
return [say('link-refused', { link: at, why: refused instanceof Error ? refused.message : String(refused) })];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Asked, Context } from '../core/answer.ts';
|
|
2
|
+
/**
|
|
3
|
+
* One question asked of several sources at once — the same operation, the same arguments — and their typed answers
|
|
4
|
+
* compared field by field. Where they agree, the field is marked by the sources that say it, each mark resting on the
|
|
5
|
+
* receipt of its read: ● for each its own, ○ for one that is the same source in another form or declared to share its
|
|
6
|
+
* lineage, with what the place declares of that lineage and nothing more. Where they differ, each is shown with its source
|
|
7
|
+
* and hour, and the loss between them is not measured. None is ever picked.
|
|
8
|
+
*/
|
|
9
|
+
export declare const compared: (asked: Asked, context: Context, place: string) => Promise<readonly string[]>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { standingOr } from "../core/answer.js";
|
|
3
|
+
import { members, valueOf } from "../core/lock.js";
|
|
4
|
+
import { sayIn } from "../core/surface.js";
|
|
5
|
+
import { linesOf } from "../core/worlds.js";
|
|
6
|
+
import { readSource } from "../flows/read.js";
|
|
7
|
+
import { shapeOf } from "../ports/surface.js";
|
|
8
|
+
/** What one source answered the question: its typed fields by name, the hour it said them, and the receipt the read rests on; or what it said instead. */
|
|
9
|
+
const readOf = async (asked, context, place, source) => {
|
|
10
|
+
const [word, raw] = [(k) => valueOf(context.lock, k), await readSource({ ...asked, sources: undefined, source, place }, context)], got = shapeOf(raw);
|
|
11
|
+
const cell = got.cells.filter((c) => ['head/answered', 'head/reused', 'head/kept', 'head/reread'].includes(c.code)).at(-1), family = `${word('receipt/family')}/${source}/`;
|
|
12
|
+
const receipt = got.lines.map((l) => linesOf(new TextEncoder().encode(l))[0]).find((l) => l?.scope?.startsWith(family) && l.measure === 'digest')?.value ?? cell?.fields['receipt'];
|
|
13
|
+
const typed = cell?.code === 'head/kept' ? new TextDecoder().decode(context.cache.held(cell.fields['digest'] ?? '') ?? new Uint8Array()).split('\n') : got.lines;
|
|
14
|
+
const fields = new Map(linesOf(new TextEncoder().encode(typed.join('\n'))).flatMap((l) => ((m) => (m ? [[m[1], l.value ?? '']] : []))(new RegExp(`^${word('vocab/answer')}/[0-9a-f]{16}/(.+)$`).exec(l.scope ?? ''))));
|
|
15
|
+
return cell === undefined || receipt === undefined || !fields.size ? { said: raw.filter((l) => l.startsWith('said/')) } : { source, at: cell.fields['at'] ?? '', receipt, fields };
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* One question asked of several sources at once — the same operation, the same arguments — and their typed answers
|
|
19
|
+
* compared field by field. Where they agree, the field is marked by the sources that say it, each mark resting on the
|
|
20
|
+
* receipt of its read: ● for each its own, ○ for one that is the same source in another form or declared to share its
|
|
21
|
+
* lineage, with what the place declares of that lineage and nothing more. Where they differ, each is shown with its source
|
|
22
|
+
* and hour, and the loss between them is not measured. None is ever picked.
|
|
23
|
+
*/
|
|
24
|
+
export const compared = async (asked, context, place) => {
|
|
25
|
+
const [say, word, held] = [sayIn(context.lock), (k) => valueOf(context.lock, k), await standingOr(context, place)], [own, mark] = [members(word('marks/own-signers')), members(word('marks/mark'))];
|
|
26
|
+
if ('said' in held)
|
|
27
|
+
return held.said;
|
|
28
|
+
const got = [];
|
|
29
|
+
for (const source of members(asked['sources'] ?? ''))
|
|
30
|
+
got.push(await readOf(asked, context, place, source));
|
|
31
|
+
const reads = got.filter((g) => 'fields' in g), line = (scope, measure) => held.lines.find((l) => l.scope === scope && (!measure || l.measure === measure));
|
|
32
|
+
const pinned = (s) => linesOf(context.files.take(join(place, line(`uses/${s}`)?.needs ?? '')) ?? new Uint8Array()).filter((l) => l.scope === `${word('vocab/region')}/${s}`);
|
|
33
|
+
const origin = (s) => ((region) => ({ origins: members(region.find((l) => l.measure === word('vocab/origins'))?.value ?? ''), pin: region.find((l) => l.measure === word('vocab/documents'))?.value, by: ((b) => (b && !own.includes(b) ? b : undefined))(region.find((l) => l.by)?.by) }))(pinned(s));
|
|
34
|
+
const same = (a, b) => ((x, y) => x.origins.some((o) => y.origins.includes(o)) || (x.pin !== undefined && x.pin === y.pin) || (x.by !== undefined && x.by === y.by))(origin(a), origin(b));
|
|
35
|
+
const shares = (s) => ((l) => (l === undefined ? undefined : members(l.value ?? '')))(line(`${word('marks/shares')}/${s}`));
|
|
36
|
+
const kin = (s, earlier) => earlier.find((e) => same(s, e.source) || shares(s)?.includes(e.source) || shares(e.source)?.includes(s));
|
|
37
|
+
return [...got.flatMap((g) => ('said' in g ? g.said : [])), ...[...new Set(reads.flatMap((r) => [...r.fields.keys()]))].flatMap((field) => {
|
|
38
|
+
const values = reads.filter((r) => r.fields.has(field));
|
|
39
|
+
if (new Set(values.map((r) => r.fields.get(field))).size > 1)
|
|
40
|
+
return [...values.map((r) => say('forked', { field, source: r.source, value: r.fields.get(field), at: r.at })), say('unmeasured', { what: `${word('marks/loss')} ${values.map((r) => r.source).join(', ')}`, needs: word('marks/loss-needs') })];
|
|
41
|
+
if (values.length < 2 || values.length < reads.length)
|
|
42
|
+
return values.map((r) => say('partial', { field, source: r.source, value: r.fields.get(field), at: r.at }));
|
|
43
|
+
const kinOf = values.map((r, i) => kin(r.source, values.slice(0, i))), first = kinOf.findIndex((k) => k !== undefined);
|
|
44
|
+
const base = { field, value: values[0].fields.get(field), sources: values.map((r) => r.source).join(', '), marks: kinOf.map((k) => (k ? mark[1] : mark[0])).join(''), receipts: values.map((r) => r.receipt).join('|') };
|
|
45
|
+
return [first >= 0 ? say('agreed-lineage', { ...base, source: values[first].source, other: kinOf[first].source })
|
|
46
|
+
: values.every((r) => line(`${word('marks/shares')}/${r.source}`)?.value === word('marks/undeclared')) ? say('agreed-undeclared', base) : say('agreed-unknown', base)];
|
|
47
|
+
})];
|
|
48
|
+
};
|
package/dist/src/ports/bound.js
CHANGED
|
@@ -5,6 +5,8 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { parse } from '@lapxo/topos/wire';
|
|
6
6
|
import { spawned } from "./process.js";
|
|
7
7
|
import { cooled, kept, warmed } from "./warm.js";
|
|
8
|
+
import { folded } from "../ahead/fold.js";
|
|
9
|
+
import { readingOf } from "./reading.js";
|
|
8
10
|
const binOf = (name) => {
|
|
9
11
|
try {
|
|
10
12
|
const manifest = createRequire(import.meta.url).resolve(join(name, 'package.json'));
|
|
@@ -34,6 +36,16 @@ export const warm = (name) => ((bin) => (bin === undefined ? Promise.resolve() :
|
|
|
34
36
|
export { cooled };
|
|
35
37
|
const facts = (out) => out.flatMap((line) => ((got) => (got.kind === 'fact' ? [got.value.fields] : []))(parse(line)));
|
|
36
38
|
const readOf = (printed) => (printed === undefined || printed.code !== 0 ? { refused: why(printed) ?? '' } : { lines: facts(printed.out) });
|
|
39
|
+
/** Native identities and expected-identity results come from the same public CLI contract. Without it, read fresh. */
|
|
40
|
+
const nativeView = (name, place, view, signal) => {
|
|
41
|
+
const read = async (expected) => {
|
|
42
|
+
if (expected === undefined)
|
|
43
|
+
return readOf(await run(name, place, [words.read, words.view, view], {}, signal));
|
|
44
|
+
const result = readingOf(await run(name, place, [words.read, words.view, `${words.identity}@${view}@${expected}`], {}, signal), view, expected);
|
|
45
|
+
return 'refused' in result ? result : { lines: facts(result.text.split('\n')) };
|
|
46
|
+
};
|
|
47
|
+
return !words.identity ? read() : folded(`${name}/${view}`, place, async () => { const result = readingOf(await run(name, place, [words.read, words.view, `${words.identity}@${view}`], {}, signal), view); return 'refused' in result ? undefined : result.identity; }, read);
|
|
48
|
+
};
|
|
37
49
|
/**
|
|
38
50
|
* The lines a place stands on, from bound's public views alone: its folded normative standing (the lines view) — its
|
|
39
51
|
* policy, all that a reader of its pins, keys and views needs, read alone where nothing else is — and from
|
|
@@ -43,12 +55,12 @@ const readOf = (printed) => (printed === undefined || printed.code !== 0 ? { ref
|
|
|
43
55
|
* name. A place whose land prints no act result is refused before anything lands in it. Any refusal of bound is no standing.
|
|
44
56
|
*/
|
|
45
57
|
export const policyAt = async (name, place, signal) => ((folded) => ('refused' in folded || folded.lines.some((line) => line.scope === words.profile) ? folded
|
|
46
|
-
: { refused: `REFUSE·act the place declares no ${words.profile}: land would print nothing of what it admits, so head lands nothing there; its root can land the local-act@1 profile and view/act` }))(
|
|
58
|
+
: { refused: `REFUSE·act the place declares no ${words.profile}: land would print nothing of what it admits, so head lands nothing there; its root can land the local-act@1 profile and view/act` }))(await nativeView(name, place, words.standing, signal));
|
|
47
59
|
export const standingAt = async (name, place, devicesOf, isHistory, signal) => {
|
|
48
60
|
const folded = await policyAt(name, place, signal);
|
|
49
61
|
if ('refused' in folded)
|
|
50
62
|
return folded;
|
|
51
|
-
const evidence =
|
|
63
|
+
const evidence = await nativeView(name, place, words.evidence, signal);
|
|
52
64
|
if ('refused' in evidence)
|
|
53
65
|
return evidence;
|
|
54
66
|
const ids = new Set(devicesOf(folded.lines)), own = evidence.lines.filter((line) => ids.has(line.by ?? ''));
|
|
@@ -81,5 +93,5 @@ export const pinnedAt = async (name, place, file, signal) => {
|
|
|
81
93
|
return pin === undefined || !bytes ? undefined : { pin, bytes, lines: read.lines };
|
|
82
94
|
};
|
|
83
95
|
export const founded = async (name, place, key, signer, env, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: got.out } : { refused: why(got) ?? '' }))(await run(name, place, [words.land, words.key, key, words.signer, signer], env, signal));
|
|
84
|
-
export const words = { read: '', view: '', sign: '', land: '', key: '', signer: '', pin: '', topos: '', standing: '', evidence: '', act: '', profile: '', receipts: '', refusal: '' };
|
|
96
|
+
export const words = { read: '', view: '', sign: '', land: '', key: '', signer: '', pin: '', topos: '', standing: '', evidence: '', act: '', profile: '', receipts: '', refusal: '', identity: '' };
|
|
85
97
|
export const settledView = async (name, place, view, signal) => readOf(await run(name, place, [words.read, words.view, view], {}, signal));
|