@lapxo/head 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/TARGET.bound +204 -29
- 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/answers/answered.d.ts +4 -0
- package/dist/src/answers/answered.js +22 -0
- package/dist/src/claims/claim.d.ts +9 -0
- package/dist/src/claims/claim.js +40 -0
- package/dist/src/compile/pin.js +2 -6
- package/dist/src/core/answer.d.ts +1 -0
- package/dist/src/core/device.d.ts +1 -4
- package/dist/src/core/device.js +2 -5
- package/dist/src/core/package.js +4 -3
- package/dist/src/flows/act.d.ts +1 -5
- package/dist/src/flows/act.js +1 -5
- package/dist/src/flows/call.d.ts +2 -9
- package/dist/src/flows/call.js +13 -13
- package/dist/src/flows/read.d.ts +1 -7
- package/dist/src/flows/read.js +26 -31
- package/dist/src/index.js +16 -7
- package/dist/src/kept/kept.d.ts +30 -0
- package/dist/src/kept/kept.js +20 -0
- package/dist/src/links/inspect.d.ts +9 -0
- package/dist/src/links/inspect.js +44 -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/onboard/first.js +1 -1
- package/dist/src/places/admit.js +1 -1
- package/dist/src/places/found.js +5 -9
- package/dist/src/ports/adapters/request.d.ts +1 -0
- package/dist/src/ports/adapters/request.js +2 -2
- package/dist/src/ports/bound.d.ts +7 -8
- package/dist/src/ports/bound.js +27 -17
- 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/tools/look.js +5 -5
- package/dist/src/use/named.d.ts +12 -0
- package/dist/src/use/named.js +25 -0
- package/dist/src/verbs/ask.d.ts +1 -9
- package/dist/src/verbs/ask.js +4 -11
- package/dist/src/verbs/card.d.ts +2 -2
- package/dist/src/verbs/card.js +3 -3
- package/package.json +3 -3
|
@@ -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,9 @@
|
|
|
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
|
+
* Asked why, it shows the transport it reads by: the profile's lines, laid at the place's birth by head, never the person's.
|
|
8
|
+
*/
|
|
9
|
+
export declare const linked: (asked: Asked, context: Context) => Promise<readonly string[]>;
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
* Asked why, it shows the transport it reads by: the profile's lines, laid at the place's birth by head, never the person's.
|
|
14
|
+
*/
|
|
15
|
+
export const linked = (asked, context) => inPlace(asked, context, async (place) => {
|
|
16
|
+
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'))];
|
|
17
|
+
if ('said' in held)
|
|
18
|
+
return held.said;
|
|
19
|
+
if (asked['why'] === 'true')
|
|
20
|
+
return [...held.lines.filter((l) => admits.includes(l.scope ?? '') || l.scope?.startsWith('transport/')).map((l) => canonical(l)), say('why-transport', { place })];
|
|
21
|
+
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' }));
|
|
22
|
+
let reference;
|
|
23
|
+
try {
|
|
24
|
+
reference = decodeReference(link);
|
|
25
|
+
}
|
|
26
|
+
catch (refused) {
|
|
27
|
+
return [say('link-refused', { link, why: refused instanceof Error ? refused.message : String(refused) })];
|
|
28
|
+
}
|
|
29
|
+
if (![admits[0], `transport/${new URL(reference.source).protocol.slice(0, -1)}`].every((s) => held.lines.some((l) => l.scope === s)))
|
|
30
|
+
return [say('transport-missing', { place, lines: lines.join('\n') })];
|
|
31
|
+
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)}`];
|
|
32
|
+
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) ?? ''];
|
|
33
|
+
if ('said' in root)
|
|
34
|
+
return root.said;
|
|
35
|
+
if (printed?.code !== 0 && !why.includes(word('link/unarrived')))
|
|
36
|
+
return [say('link-refused', { link, why })];
|
|
37
|
+
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([]));
|
|
38
|
+
const due = { at: 'policy:head/link', by: 'target', form: 'alphabet', measure: 'id', role: 'demands', scope, value: link };
|
|
39
|
+
if (printed?.code !== 0)
|
|
40
|
+
return [say('link-owed', { place, source: reference.source }), ...await settled(owed ? [] : [canonical(due)])];
|
|
41
|
+
const named = linesOf(new TextEncoder().encode(printed.out.filter((l) => l.startsWith('TOPOS ')).map((l) => l.slice('TOPOS '.length)).join('\n')));
|
|
42
|
+
const sources = named.filter((l) => /^sources\/[^/]+$/.test(l.scope ?? '')).map((l) => l.scope.split('/')[1]);
|
|
43
|
+
return [say('inspected', { source: reference.source, sources: sources.join(', ') || '—', digest: reference.digest }), say('sharer-unverified', {}), ...await settled(owed ? [canonical({ ...due, value: 'withdraw' })] : [])];
|
|
44
|
+
});
|
|
@@ -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
|
+
};
|
|
@@ -28,7 +28,7 @@ export const firstRun = async (asked, context, then) => {
|
|
|
28
28
|
const connected = await then(dir, true), unread = connected.some((l) => l.startsWith('said/head/document-unread ')), held = unread ? undefined : await context.policy(dir);
|
|
29
29
|
if (held !== undefined && ('refused' in held || !held.lines.some((l) => l.scope === `uses/${source}`)))
|
|
30
30
|
return [...born, ...connected];
|
|
31
|
-
const line = (scope, measure, value, role = 'writes') => canonical({ at: 'policy:
|
|
31
|
+
const line = (scope, measure, value, role = 'writes') => canonical({ at: 'policy:person/grant', by: 'target', form: 'alphabet', measure, role, scope, value });
|
|
32
32
|
const owed = unread ? [line(`${word('place/needs')}/bytes/${source}`, 'id', document, 'demands')] : [];
|
|
33
33
|
const landed = await context.landRoot(dir, [line(word('grant/family'), 'sources', source), line(word('grant/family'), 'until', until), ...owed], root.account);
|
|
34
34
|
if ('refused' in landed)
|
package/dist/src/places/admit.js
CHANGED
|
@@ -26,7 +26,7 @@ export const admitted = (asked, context) => inPlace(asked, context, async (place
|
|
|
26
26
|
const refused = await unless(context, say('elicit-admit', { name, place, coverage }));
|
|
27
27
|
if (refused)
|
|
28
28
|
return refused;
|
|
29
|
-
const lot = admissionOf(context.lock, key, 'head', { id: name, coverage, at: `policy:
|
|
29
|
+
const lot = admissionOf(context.lock, key, 'head', { id: name, coverage, at: `policy:person/admit/${new Date(context.now()).toISOString()}` });
|
|
30
30
|
const landed = await context.landRoot(place, lot, root.account);
|
|
31
31
|
return 'refused' in landed ? [say('not-landed', { why: landed.refused })] : [say('admitted', { name, place, coverage }), ...landed.lines];
|
|
32
32
|
});
|
package/dist/src/places/found.js
CHANGED
|
@@ -2,19 +2,15 @@ import { isAbsolute, basename } from 'node:path';
|
|
|
2
2
|
import { canonical, LOCK } from '@lapxo/topos/wire';
|
|
3
3
|
import { unless } from "../core/answer.js";
|
|
4
4
|
import { admissionOf } from "../core/device.js";
|
|
5
|
-
import { valueOf } from "../core/lock.js";
|
|
5
|
+
import { members, valueOf } from "../core/lock.js";
|
|
6
6
|
import { deviceKey } from "../flows/device.js";
|
|
7
7
|
import { walkable } from "../walk/context.js";
|
|
8
8
|
import { fill, sayIn } from "../core/surface.js";
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
* birth/ — the wire, the regions and views a world is read through, and the lifetime every reader is bounded by.
|
|
13
|
-
*/
|
|
14
|
-
const birthOf = (lock, root, head, person, place) => [
|
|
15
|
-
...[['class', 'authorize'], ['coverage', '*'], ['signer', valueOf(lock, 'device/head/signer')], ['public-key', root]].map(([measure, value]) => canonical({ at: 'policy:head/birth', by: 'target', form: 'alphabet', measure: measure, role: 'writes', scope: valueOf(lock, 'founding/root'), value: value })),
|
|
9
|
+
/** Found the place from the declared prelude; personal lines retain the person's provenance. */
|
|
10
|
+
const birthOf = (lock, root, head, person, place, mine = `${valueOf(lock, 'founding/person-at')}birth`) => [
|
|
11
|
+
...[['class', 'authorize'], ['coverage', '*'], ['signer', valueOf(lock, 'device/head/signer')], ['public-key', root]].map(([measure, value]) => canonical({ at: mine, by: 'target', form: 'alphabet', measure: measure, role: 'writes', scope: valueOf(lock, 'founding/root'), value: value })),
|
|
16
12
|
...admissionOf(lock, head, 'head'), ...admissionOf(lock, person, 'person'),
|
|
17
|
-
...lock.filter((l) => l.scope?.startsWith('birth/')).map(({ about: _about, scope, ...line }) => canonical({ ...line, value: fill(line.value ?? '', { place }), scope:
|
|
13
|
+
...lock.filter((l) => l.scope?.startsWith('birth/')).map(({ about: _about, scope, ...line }) => ((s) => canonical({ ...line, value: fill(line.value ?? '', { place }), scope: s, at: members(valueOf(lock, 'founding/person')).includes(s) ? mine : 'policy:head/birth', by: 'target' }))(scope.slice('birth/'.length))),
|
|
18
14
|
];
|
|
19
15
|
/** A place founded in a folder: a root key made in the keychain under the place's name — never a file — the birth lines written, and the first land signed by that root. */
|
|
20
16
|
const founded = async (context, dir, name) => {
|
|
@@ -19,8 +19,8 @@ const requestCapped = async (method, url, headers, body, most, wait, redirect =
|
|
|
19
19
|
}
|
|
20
20
|
return size > most ? { kind: 'over' } : { kind: 'got', status: response.status, bytes: Buffer.concat(chunks), type: response.headers.get('content-type') ?? '', ...((at) => (at ? { location: at } : {}))(response.headers.get('location')), ...((asks) => (asks ? { authenticate: asks } : {}))(response.headers.get('www-authenticate')) };
|
|
21
21
|
}
|
|
22
|
-
catch {
|
|
23
|
-
return { kind: 'failed' };
|
|
22
|
+
catch (error) {
|
|
23
|
+
return { kind: 'failed', why: String(error?.cause?.code ?? error?.name ?? '') };
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
const loopback = new Set(['127.0.0.1', 'localhost', '[::1]']);
|
|
@@ -17,20 +17,18 @@ export type Folded = {
|
|
|
17
17
|
} | {
|
|
18
18
|
readonly refused: string;
|
|
19
19
|
};
|
|
20
|
-
/**
|
|
21
|
-
* The lines a place stands on, from bound's public views alone: its folded normative standing (the lines view) — its
|
|
22
|
-
* policy, all that a reader of its pins, keys and views needs, read alone where nothing else is — and from
|
|
23
|
-
* its authenticated evidence what this device's own keys signed. Receipts are history, each kept as it was made; any other
|
|
24
|
-
* line of a device key, the person's own attestation, stands only while it is the one line of its scope and measure,
|
|
25
|
-
* unwithdrawn and beside no line of the place's own: anything else needs a fold bound does not publish, and is refused by
|
|
26
|
-
* name. A place whose land prints no act result is refused before anything lands in it. Any refusal of bound is no standing.
|
|
27
|
-
*/
|
|
20
|
+
/** Read normative standing plus authenticated device evidence; ambiguous device history refuses. */
|
|
28
21
|
export declare const policyAt: (name: string, place: string, signal?: AbortSignal) => Promise<Folded>;
|
|
29
22
|
export declare const standingAt: (name: string, place: string, devicesOf: (lines: readonly Said[]) => readonly string[], isHistory: (line: Said) => boolean, signal?: AbortSignal) => Promise<Folded>;
|
|
30
23
|
export declare const sign: (name: string, place: string, lot: readonly string[], key: string, signer: string, env: Readonly<Record<string, string>>, signal?: AbortSignal) => Promise<Outcome>;
|
|
31
24
|
/** A lot signed and landed in one process, its result read from what bound printed of the committed act: the records it admitted, never the receipt beside them; a place that declares no act result is refused by name. */
|
|
32
25
|
/** A place's receipts rendered beside its lock, so bound alone checks the place: nothing when they closed, else why they did not. */
|
|
33
26
|
export declare const closing: (name: string, place: string, signal?: AbortSignal) => Promise<string | undefined>;
|
|
27
|
+
/** A place's own lines in the person's words, beside its lock, the text they read and share: each line head lands at the person's at, drafted with no signature, gone with its withdraw. Bound reads the file as it reads the place, so it is written with the land, never while a fold runs. Set from head's lock. */
|
|
28
|
+
export declare const birth: {
|
|
29
|
+
at: string;
|
|
30
|
+
file: string;
|
|
31
|
+
}, birthed: (place: string, lot: readonly string[]) => void;
|
|
34
32
|
export declare const landAct: (name: string, place: string, lot: readonly string[], key: string, signer: string, env: Readonly<Record<string, string>>, signal?: AbortSignal) => Promise<Outcome>;
|
|
35
33
|
export declare const land: (name: string, place: string, signed: readonly string[], signal?: AbortSignal) => Promise<Outcome>;
|
|
36
34
|
export declare const told: (name: string, place: string, signed: readonly string[], view: string, signal?: AbortSignal) => Promise<readonly string[]>;
|
|
@@ -57,5 +55,6 @@ export declare const words: {
|
|
|
57
55
|
profile: string;
|
|
58
56
|
receipts: string;
|
|
59
57
|
refusal: string;
|
|
58
|
+
identity: string;
|
|
60
59
|
};
|
|
61
60
|
export declare const settledView: (name: string, place: string, view: string, signal?: AbortSignal) => Promise<Folded>;
|
package/dist/src/ports/bound.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import { parse } from '@lapxo/topos/wire';
|
|
5
|
+
import { canonical, LOCK, 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,21 +36,23 @@ 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) });
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
};
|
|
49
|
+
/** Read normative standing plus authenticated device evidence; ambiguous device history refuses. */
|
|
45
50
|
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` }))(
|
|
51
|
+
: { 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
52
|
export const standingAt = async (name, place, devicesOf, isHistory, signal) => {
|
|
48
|
-
const folded = await policyAt(name, place, signal);
|
|
53
|
+
const [folded, evidence] = await Promise.all([policyAt(name, place, signal), nativeView(name, place, words.evidence, signal)]);
|
|
49
54
|
if ('refused' in folded)
|
|
50
55
|
return folded;
|
|
51
|
-
const evidence = readOf(await run(name, place, [words.read, words.view, words.evidence], {}, signal));
|
|
52
56
|
if ('refused' in evidence)
|
|
53
57
|
return evidence;
|
|
54
58
|
const ids = new Set(devicesOf(folded.lines)), own = evidence.lines.filter((line) => ids.has(line.by ?? ''));
|
|
@@ -62,14 +66,20 @@ export const sign = async (name, place, lot, key, signer, env, signal) => ((got)
|
|
|
62
66
|
/** A lot signed and landed in one process, its result read from what bound printed of the committed act: the records it admitted, never the receipt beside them; a place that declares no act result is refused by name. */
|
|
63
67
|
/** A place's receipts rendered beside its lock, so bound alone checks the place: nothing when they closed, else why they did not. */
|
|
64
68
|
export const closing = async (name, place, signal) => ((got) => (got !== undefined && got.code === 0 ? undefined : why(got) ?? 'nothing printed'))(await run(name, place, [words.read, words.view, words.receipts], {}, signal));
|
|
69
|
+
/** A place's own lines in the person's words, beside its lock, the text they read and share: each line head lands at the person's at, drafted with no signature, gone with its withdraw. Bound reads the file as it reads the place, so it is written with the land, never while a fold runs. Set from head's lock. */
|
|
70
|
+
export const birth = { at: '', file: '' }, birthed = (place, lot) => {
|
|
71
|
+
const [file, mine] = [join(place, birth.file), facts(lot).filter((l) => birth.file && l['at']?.startsWith(birth.at))], bare = (l) => JSON.stringify(Object.entries(l).filter(([k]) => !['about', 'value', 'sig', 'epoch', 'by'].includes(k)).sort());
|
|
72
|
+
if (mine.length)
|
|
73
|
+
writeFileSync(file, [...(existsSync(file) ? facts(readFileSync(file, 'utf8').split('\n')) : []).filter((k) => !mine.some((m) => bare(m) === bare(k))), ...mine.filter((m) => m['value'] !== 'withdraw')].map(({ sig: _sig, epoch: _epoch, ...l }) => `${canonical({ ...l, by: 'target' })}\n`).join(''));
|
|
74
|
+
};
|
|
65
75
|
export const landAct = async (name, place, lot, key, signer, env, signal) => ((got) => {
|
|
66
76
|
if (got === undefined || got.code !== 0)
|
|
67
77
|
return { refused: why(got) ?? '' };
|
|
68
78
|
const at = got.out.findIndex((line) => line.startsWith(words.act));
|
|
69
79
|
return at < 0 ? { refused: `REFUSE·act bound printed no act result, and the place declares ${words.profile}: what was admitted is bound's to say` }
|
|
70
|
-
: { lines: got.out.slice(at + 1).filter((line) => parse(line).kind === 'fact').slice(0, -1) };
|
|
80
|
+
: (birthed(place, lot), { lines: got.out.slice(at + 1).filter((line) => parse(line).kind === 'fact').slice(0, -1) });
|
|
71
81
|
})(await run(name, place, [words.land, lotFile(lot), words.key, key, words.signer, signer], env, signal));
|
|
72
|
-
export const land = async (name, place, signed, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: signed } : { refused: why(got) ?? '' }))(await run(name, place, [words.land, lotFile(signed)], {}, signal));
|
|
82
|
+
export const land = async (name, place, signed, signal) => ((got) => (got !== undefined && got.code === 0 ? (birthed(place, signed), { lines: signed }) : { refused: why(got) ?? '' }))(await run(name, place, [words.land, lotFile(signed)], {}, signal));
|
|
73
83
|
export const told = async (name, place, signed, view, signal) => ((got) => [...(got?.out ?? []), ...(got?.err ?? [])])(await run(name, place, [words.read, words.view, view, lotFile(signed)], {}, signal));
|
|
74
84
|
export const pinnedAt = async (name, place, file, signal) => {
|
|
75
85
|
const printed = await run(name, place, [words.read, file], {}, signal);
|
|
@@ -80,6 +90,6 @@ export const pinnedAt = async (name, place, file, signal) => {
|
|
|
80
90
|
const bytes = printed.out.filter((line) => line.startsWith(words.topos)).map((line) => `${line.slice(words.topos.length)}\n`).join('');
|
|
81
91
|
return pin === undefined || !bytes ? undefined : { pin, bytes, lines: read.lines };
|
|
82
92
|
};
|
|
83
|
-
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: '' };
|
|
93
|
+
export const founded = async (name, place, key, signer, env, signal) => ((got) => (got !== undefined && got.code === 0 ? (birthed(place, readFileSync(join(place, LOCK), 'utf8').split('\n')), { lines: got.out }) : { refused: why(got) ?? '' }))(await run(name, place, [words.land, words.key, key, words.signer, signer], env, signal));
|
|
94
|
+
export const words = { read: '', view: '', sign: '', land: '', key: '', signer: '', pin: '', topos: '', standing: '', evidence: '', act: '', profile: '', receipts: '', refusal: '', identity: '' };
|
|
85
95
|
export const settledView = async (name, place, view, signal) => readOf(await run(name, place, [words.read, words.view, view], {}, signal));
|
|
@@ -32,7 +32,7 @@ export const fetchedAs = async (context, at, via, got, named, accept) => {
|
|
|
32
32
|
return { said: [say('fetch-redirect', { document: at })] };
|
|
33
33
|
return ((bytes) => (bytes ? { bytes, kind: kindOf(at) } : { said: [say('document-unread', { document: at })] }))(await transport.published(new URL(answer.location, at).href, named, most, wait));
|
|
34
34
|
}
|
|
35
|
-
return answer.status < 300 ? { bytes: answer.bytes, kind: kindOf(at, answer.type) } : { said: [say('fetch-status', { document: at, via, status: String(answer.status) })] };
|
|
35
|
+
return answer.status < 300 ? { bytes: answer.bytes, kind: kindOf(at, answer.type) } : { said: [say('fetch-status', { document: at, via, status: String(answer.status), at: new Date(context.now()).toISOString() })] };
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
38
|
* Every byte a package's lines name, by the path each names: read where a line of the same scope says — an encrypted
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Printed } from './process.ts';
|
|
2
|
+
/** Only the installed instrument names a view's identity and the result bound to it. */
|
|
3
|
+
export declare const readingOf: (printed: Printed | undefined, view: string, expected?: string) => {
|
|
4
|
+
readonly identity: string;
|
|
5
|
+
readonly text?: string;
|
|
6
|
+
} | {
|
|
7
|
+
readonly refused: string;
|
|
8
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Only the installed instrument names a view's identity and the result bound to it. */
|
|
2
|
+
export const readingOf = (printed, view, expected) => {
|
|
3
|
+
if (!printed || printed.code !== 0)
|
|
4
|
+
return { refused: printed?.refusal?.['code'] ?? 'reading-identity/unavailable' };
|
|
5
|
+
try {
|
|
6
|
+
const value = printed.out.length === 1 ? JSON.parse(printed.out[0]) : undefined;
|
|
7
|
+
if (value?.['kind'] !== 'fact' || value['view'] !== view || typeof value['identity'] !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(value['identity']) || (expected !== undefined && (value['identity'] !== expected || typeof value['text'] !== 'string')))
|
|
8
|
+
return { refused: 'reading-identity/context' };
|
|
9
|
+
return { identity: value['identity'], ...(expected === undefined ? {} : { text: value['text'] }) };
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return { refused: 'reading-identity/format' };
|
|
13
|
+
}
|
|
14
|
+
};
|
package/dist/src/ports/spare.js
CHANGED
|
@@ -18,7 +18,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
18
18
|
const [program] = process.argv.slice(2);
|
|
19
19
|
const url = pathToFileURL(program), pipe = new Socket({ fd: 3, readable: true, writable: true });
|
|
20
20
|
const asked = new Promise((resolve) => { let text = ''; pipe.setEncoding('utf8').on('data', (chunk) => { text += chunk; }).on('error', () => undefined).on('close', () => resolve(text)); });
|
|
21
|
-
await Promise.all([...readFileSync(program, 'utf8').matchAll(
|
|
21
|
+
await Promise.all([...readFileSync(program, 'utf8').matchAll(/(?:^|[;\n])\s*import\s*(?:(?:[^'";]*?)\s*from\s*)?['"](\.{1,2}\/[^'"]+)['"]/g)].map(([, path]) => import(__rewriteRelativeImportExtension(new URL(path, url).href)).catch(() => undefined)));
|
|
22
22
|
pipe.write('ready\n');
|
|
23
23
|
const text = await asked;
|
|
24
24
|
if (!text)
|
package/dist/src/ports/warm.js
CHANGED
|
@@ -40,7 +40,6 @@ export const warmed = (program, args, at, bounds) => {
|
|
|
40
40
|
const spare = index < 0 ? undefined : live.splice(index, 1)[0];
|
|
41
41
|
if (spare === undefined)
|
|
42
42
|
return undefined;
|
|
43
|
-
void kept(program);
|
|
44
43
|
spare.child.stdio[3].end(`${JSON.stringify({ cwd: at.cwd, args, env: at.env })}\n`);
|
|
45
|
-
return watched(spare.child, bounds);
|
|
44
|
+
return watched(spare.child, bounds).finally(() => { void kept(program); });
|
|
46
45
|
};
|
|
@@ -9,6 +9,7 @@ import { paidBy, pinIn } from "../compile/pin.js";
|
|
|
9
9
|
import { adopted } from "../adopt/adopt.js";
|
|
10
10
|
import { firstRun } from "../onboard/first.js";
|
|
11
11
|
import { resolved } from "../onboard/site.js";
|
|
12
|
+
import { linked } from "../links/inspect.js";
|
|
12
13
|
import { rootIn } from "../flows/device.js";
|
|
13
14
|
import { attachedFor } from "./access.js";
|
|
14
15
|
/**
|
|
@@ -22,7 +23,8 @@ import { attachedFor } from "./access.js";
|
|
|
22
23
|
* its pins rest in, its pin held to the package's; one that differs is refused by name, and the place holds what it
|
|
23
24
|
* compiled. Nothing the sharing place was granted comes with it: access is asked here, issuer by issuer.
|
|
24
25
|
*/
|
|
25
|
-
export const connected = async (asked, context, covered = false) => ((
|
|
26
|
+
export const connected = async (asked, context, covered = false) => ((asked['document'] ?? '').startsWith(valueOf(context.lock, 'link/prefix')) ? linked(asked, context)
|
|
27
|
+
: ((got) => ('said' in got ? got.said : connectedAs(got.asked, context, covered)))(await resolved(asked, context)));
|
|
26
28
|
const connectedAs = async (asked, context, covered = false) => (asked['source'] && !(await placeOf(asked, context)) ? firstRun(asked, context, (dir, covered) => connected({ ...asked, place: dir }, context, covered))
|
|
27
29
|
: ((started) => inPlace(asked, context, async (place) => {
|
|
28
30
|
const say = sayIn(context.lock), digest = digestIn(context.lock), { documentOf, fetchedAs, kindOf } = context.files.read;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { claimed as answer } from '../claims/claim.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { claimed as answer } from "../claims/claim.js";
|
package/dist/src/tools/look.js
CHANGED
|
@@ -24,18 +24,18 @@ export const answer = (asked, context) => inPlace(asked, context, async (place)
|
|
|
24
24
|
if ('unsettled' in held)
|
|
25
25
|
return [sayIn(context.lock)('unsettled', { source, why: held.unsettled })];
|
|
26
26
|
const [v, first, shallow] = [vocabOf(context.lock), asked['at'] === '0', word('look/first').split('|')];
|
|
27
|
-
const region = `${v.offers}/${source}/${operation ?? ''
|
|
27
|
+
const [region, uses] = [`${v.offers}/${source}/`, held.lines.filter((l) => l.scope?.startsWith(`${word('use/family')}/${source}/`))], named = operation ? [operation] : uses.map((l) => l.value ?? '');
|
|
28
28
|
const shown = held.lines.filter((l) => {
|
|
29
29
|
const scope = l.scope ?? '';
|
|
30
|
-
if (scope === `${v.region}/${source}`)
|
|
30
|
+
if (scope === `${v.region}/${source}` || (!operation && uses.includes(l)))
|
|
31
31
|
return true;
|
|
32
32
|
if (scope.startsWith(`${v.keys}/${source}/`))
|
|
33
33
|
return !first;
|
|
34
|
-
if (!(
|
|
34
|
+
if (!named.some((op) => scope === `${region}${op}` || scope.startsWith(`${region}${op}/`)) || scope.endsWith(`/${v.carry}`))
|
|
35
35
|
return false;
|
|
36
36
|
return !first || (!scope.endsWith(`/${v.form}`) && shallow.includes(l.measure ?? ''));
|
|
37
|
-
});
|
|
38
|
-
return shown.length ? [...shown.map((l) => canonical(l)).sort(), ...(first ? [] : trustOf(context.lock, held.lines, source)), ...(operation ? await readSource(asked, context) : [])] : [sayIn(context.lock)('unread', { source })];
|
|
37
|
+
}), more = held.lines.filter((l) => l.measure === v.class && l.scope?.startsWith(region) && !named.includes(l.scope.slice(region.length))).length;
|
|
38
|
+
return shown.length ? [...shown.map((l) => canonical(l)).sort(), ...(first ? [] : trustOf(context.lock, held.lines, source)), ...(more ? [sayIn(context.lock)('more-offers', { source, more: String(more) })] : []), ...(operation ? await readSource(asked, context) : [])] : [sayIn(context.lock)('unread', { source })];
|
|
39
39
|
}
|
|
40
40
|
const view = `${asked['view'] ?? ''}${asked['at'] ? `${word('bound/at')}${asked['at']}` : ''}`;
|
|
41
41
|
return printedOr(context, await context.bound(place, [word('bound/read'), word('bound/view'), view]));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Context } from '../core/answer.ts';
|
|
2
|
+
import { type Said } from '../core/lock.ts';
|
|
3
|
+
/** The offers of a source a place holds: head's own registry of it, local, never sent to the model whole. */
|
|
4
|
+
export declare const offersOf: (lock: readonly Said[], lines: readonly Said[], source: string) => readonly string[];
|
|
5
|
+
/** Resolve the offer against the local registry. Ambiguity asks the person; read-ahead creates no use line. */
|
|
6
|
+
export declare const namedIn: (context: Context, lines: readonly Said[], source: string, asked: string, ahead: boolean) => Promise<{
|
|
7
|
+
readonly operation: string;
|
|
8
|
+
readonly use?: string;
|
|
9
|
+
readonly scope?: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly said: readonly string[];
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
2
|
+
import { valueOf } from "../core/lock.js";
|
|
3
|
+
import { vocabOf } from "../core/read.js";
|
|
4
|
+
import { sayIn } from "../core/surface.js";
|
|
5
|
+
/** The words of a name as a person or a model says it: its runs of letters and digits, lowercase. */
|
|
6
|
+
const words = (text) => text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
7
|
+
/** The offers of a source a place holds: head's own registry of it, local, never sent to the model whole. */
|
|
8
|
+
export const offersOf = (lock, lines, source) => ((at) => lines.filter((l) => l.measure === vocabOf(lock).class && l.scope?.startsWith(at)).map((l) => l.scope.slice(at.length)))(`${vocabOf(lock).offers}/${source}/`);
|
|
9
|
+
/** Resolve the offer against the local registry. Ambiguity asks the person; read-ahead creates no use line. */
|
|
10
|
+
export const namedIn = async (context, lines, source, asked, ahead) => {
|
|
11
|
+
const [v, word, say] = [vocabOf(context.lock), (k) => valueOf(context.lock, k), sayIn(context.lock)];
|
|
12
|
+
const [family, name] = [word('use/family'), words(asked).join('-')], uses = lines.filter((l) => l.scope?.startsWith(`${family}/${source}/`)), offers = offersOf(context.lock, lines, source);
|
|
13
|
+
const used = uses.find((l) => l.scope === `${family}/${source}/${name}`)?.value;
|
|
14
|
+
if (used !== undefined || ahead || !asked)
|
|
15
|
+
return { operation: used ?? asked };
|
|
16
|
+
const named = (op) => words([op, ...lines.filter((l) => l.scope === `${v.offers}/${source}/${op}/${v.carry}`).map((l) => l.value ?? '')].join(' '));
|
|
17
|
+
const found = offers.includes(asked) ? [asked] : offers.filter((op) => words(asked).length > 0 && words(asked).every((w) => named(op).includes(w)));
|
|
18
|
+
const operation = found.length > 1 ? (await context.person.choose(say('pick-offer', { source, asked }), [{ name: family, options: found.map((o) => ({ value: o, label: o })) }]))?.[family] : found[0];
|
|
19
|
+
if (!found.length)
|
|
20
|
+
return { operation: asked };
|
|
21
|
+
if (operation === undefined || !found.includes(operation))
|
|
22
|
+
return { said: [say('pick-offer', { source, asked })] };
|
|
23
|
+
const scope = `${family}/${source}/${name}`;
|
|
24
|
+
return uses.some((l) => l.value === operation) ? { operation } : { operation, scope, use: canonical({ at: `${word('founding/person-at')}use/${new Date(context.now()).toISOString()}`, by: 'target', form: 'alphabet', measure: 'operation', role: 'writes', scope, value: operation }) };
|
|
25
|
+
};
|
package/dist/src/verbs/ask.d.ts
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
1
|
import type { Answer } from '../core/answer.ts';
|
|
2
|
-
/**
|
|
3
|
-
* What a person asks, answered by the question it is. A view, or the region of a source or of one operation in it (what
|
|
4
|
-
* it offers, one read of it, why that answer stands, what was kept of it), is answered by look; drafted lines by plan;
|
|
5
|
-
* what is open by card; anything else by places. Each form is read with no resolution unless the lock declares one for a
|
|
6
|
-
* person; asked why, at the one it declares to explain, and where it declares none, the same answer, said so. Nothing
|
|
7
|
-
* here writes one. An answer about one place ends with its card, taken off the fold the answer reads, valid from it.
|
|
8
|
-
* With no place at all, nothing is founded: it says so, and that connecting an API founds the person's place, the question
|
|
9
|
-
* carried along. A place still owing a source's bytes is answered only once they arrive.
|
|
10
|
-
*/
|
|
2
|
+
/** Route a question at the declared resolution, then attach the card of the place it read. */
|
|
11
3
|
export declare const asked: Answer;
|
package/dist/src/verbs/ask.js
CHANGED
|
@@ -5,18 +5,11 @@ import { valueOf } from "../core/lock.js";
|
|
|
5
5
|
import { sayIn } from "../core/surface.js";
|
|
6
6
|
import { paidIn } from "../onboard/owed.js";
|
|
7
7
|
import { pendingIn } from "../sources/refused.js";
|
|
8
|
+
import { compared } from "../marks/compare.js";
|
|
8
9
|
import { cardAt } from "./card.js";
|
|
9
|
-
/**
|
|
10
|
-
* What a person asks, answered by the question it is. A view, or the region of a source or of one operation in it (what
|
|
11
|
-
* it offers, one read of it, why that answer stands, what was kept of it), is answered by look; drafted lines by plan;
|
|
12
|
-
* what is open by card; anything else by places. Each form is read with no resolution unless the lock declares one for a
|
|
13
|
-
* person; asked why, at the one it declares to explain, and where it declares none, the same answer, said so. Nothing
|
|
14
|
-
* here writes one. An answer about one place ends with its card, taken off the fold the answer reads, valid from it.
|
|
15
|
-
* With no place at all, nothing is founded: it says so, and that connecting an API founds the person's place, the question
|
|
16
|
-
* carried along. A place still owing a source's bytes is answered only once they arrive.
|
|
17
|
-
*/
|
|
10
|
+
/** Route a question at the declared resolution, then attach the card of the place it read. */
|
|
18
11
|
export const asked = async (asked, context) => {
|
|
19
|
-
const why = asked['why'] === 'true', at = (form) => valueOf(context.lock, `resolution/${form}/${why ? 'why' : 'person'}`)
|
|
12
|
+
const why = asked['why'] === 'true', at = (form) => ((v) => (v && v !== valueOf(context.lock, 'resolution/own') ? v : undefined))(valueOf(context.lock, `resolution/${form}/${why ? 'why' : 'person'}`));
|
|
20
13
|
const read = async (form, tool, more = {}) => [...(why && at(form) === undefined ? [sayIn(context.lock)('why-undeclared', { form })] : []),
|
|
21
14
|
...await (await answerOf('tools', tool))({ ...asked, ...more, at: at(form) }, context)];
|
|
22
15
|
const place = await placeOf(asked, context), founded = place !== undefined && context.files.has(place, LOCK), carded = asked['card'] === 'true';
|
|
@@ -28,7 +21,7 @@ export const asked = async (asked, context) => {
|
|
|
28
21
|
const owed = asked['source'] && founded ? await paidIn(asked, context, place) : undefined;
|
|
29
22
|
if (owed !== undefined && !owed.answer)
|
|
30
23
|
return owed.said;
|
|
31
|
-
const answer = asked['lines'] ? await read('plan', 'plan') : asked['view'] ? await read('view', 'look') : asked['source'] ? await read('offers', 'look')
|
|
24
|
+
const answer = asked['sources'] && founded ? await compared(asked, context, place) : asked['lines'] ? await read('plan', 'plan') : asked['view'] ? await read('view', 'look') : asked['source'] ? await read('offers', 'look')
|
|
32
25
|
: !carded ? await read('places', 'places') : founded ? [...await read('card', 'card', { place }), ...await pendingIn(context, place)] : [];
|
|
33
26
|
const after = held === undefined || 'said' in held || place === undefined || !context.moved(place) ? held : await cardAt(context, place);
|
|
34
27
|
return [...(owed?.said ?? []), ...answer, ...(after === undefined || 'said' in after ? [] : [sayIn(context.lock)('card', { place: place ?? '', card: after.card, from: 'ask' })])];
|
package/dist/src/verbs/card.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Context } from '../core/answer.ts';
|
|
2
2
|
/**
|
|
3
|
-
* The card an act is made on: the place's standing as bound folds it, with the reads it kept
|
|
4
|
-
* device acts with. Anything landed, a grant withdrawn or a key admitted or changed moves it; a read never does. A folder
|
|
3
|
+
* The card an act is made on: the place's standing as bound folds it, with the reads it kept and the uses they named set
|
|
4
|
+
* aside, and the keys this device acts with. Anything landed, a grant withdrawn or a key admitted or changed moves it; a read never does. A folder
|
|
5
5
|
* that holds no place yet has the card of nothing.
|
|
6
6
|
*/
|
|
7
7
|
export declare const cardAt: (context: Context, place: string | undefined) => Promise<{
|