@volter/world-core 3.0.138 → 3.0.140
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/dist/src/changeset.d.ts +11 -0
- package/dist/src/changeset.js +39 -4
- package/dist/src/derived-core.js +11 -6
- package/dist/src/log.js +16 -5
- package/dist/src/pack-fetch.js +13 -2
- package/dist/src/runtime.d.ts +1 -1
- package/dist/src/runtime.js +1 -1
- package/package.json +1 -1
- package/src/changeset.ts +42 -4
- package/src/derived-core.ts +12 -6
- package/src/log.ts +16 -5
- package/src/pack-fetch.ts +11 -2
- package/src/runtime.ts +1 -1
package/dist/src/changeset.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type HistoryReference } from './history.js';
|
|
2
2
|
import type { TwinActionRevertSpec } from './actions.js';
|
|
3
3
|
import type { TwinAction, TwinActionPrecondition } from './actions.js';
|
|
4
|
+
import { type FieldChange } from './hash.js';
|
|
4
5
|
/** One twin's action ledger, addressed the way the control plane addresses state:
|
|
5
6
|
* `worldPaths(stateService, controlRoot)`. `service` is the caller's label for it (in a world:
|
|
6
7
|
* the world service id); `stateService` is the control-plane service whose ledger this is.
|
|
@@ -60,7 +61,10 @@ export type LedgerDelta = {
|
|
|
60
61
|
actions: ChangesetAction[];
|
|
61
62
|
vendors: VendorSummary[];
|
|
62
63
|
total: number;
|
|
64
|
+
/** Presentation only, outside changeset bodies: fields immediately before and after each write. */
|
|
65
|
+
fieldChanges?: Record<string, Record<string, FieldChange>>;
|
|
63
66
|
};
|
|
67
|
+
export declare const fieldDiffKey: (service: string, stateService: string, id: string) => string;
|
|
64
68
|
/** A verifier: one deterministic check against post-replay projected state — the CI assertion
|
|
65
69
|
* of the operational PR (docs/concepts/the-model.md v1). The `assert` is the kernel's ONE check
|
|
66
70
|
* expression (`TwinActionPrecondition`: subject/field/op/value, evaluated by
|
|
@@ -189,6 +193,8 @@ export declare function diffLedgers(opts: {
|
|
|
189
193
|
ledgers: LedgerRef[];
|
|
190
194
|
base: WorldMarker;
|
|
191
195
|
}): LedgerDelta;
|
|
196
|
+
/** Read each write's pre-state through the authoritative kernel projection. No UI history fold. */
|
|
197
|
+
export declare function describeActionFields(ledgers: LedgerRef[], actions: ChangesetAction[]): NonNullable<LedgerDelta['fieldChanges']>;
|
|
192
198
|
/** Group a delta by vendor, then by operation — both in FIRST-OCCURRENCE order, so the rollup
|
|
193
199
|
* reads in the same causal order as the actions themselves. */
|
|
194
200
|
export declare function summarizeByVendor(actions: ChangesetAction[]): VendorSummary[];
|
|
@@ -403,6 +409,11 @@ export type CompensationEntry = {
|
|
|
403
409
|
export type ChangesetApplication = {
|
|
404
410
|
at: string;
|
|
405
411
|
contentHash: string;
|
|
412
|
+
/** A push receipt names its destination; absent on legacy and non-push applications. */
|
|
413
|
+
destination?: {
|
|
414
|
+
url: string;
|
|
415
|
+
namespace: string;
|
|
416
|
+
};
|
|
406
417
|
outcome: 'applied' | 'partial' | 'refused';
|
|
407
418
|
forced?: boolean;
|
|
408
419
|
refusal?: string[];
|
package/dist/src/changeset.js
CHANGED
|
@@ -20,7 +20,7 @@ import { worldPaths } from "./storage.js";
|
|
|
20
20
|
// silently reporting the wrong delta.
|
|
21
21
|
//
|
|
22
22
|
// DELTA every action appended after the marker, across every ledger, ordered by
|
|
23
|
-
// `occurredAt` (
|
|
23
|
+
// `occurredAt` with stable ledger/row ties (a timeline, not cross-vendor causality), grouped by vendor
|
|
24
24
|
// for rendering.
|
|
25
25
|
//
|
|
26
26
|
// CHANGESET a delta frozen into a content-addressed object: `{id, world, base, actions,
|
|
@@ -57,14 +57,15 @@ import { worldPaths } from "./storage.js";
|
|
|
57
57
|
// the control root that holds it) and never learns what a world is. `volter-world` supplies
|
|
58
58
|
// discovery and the CLI verbs; the state-level logic lives here, beside the ledgers it reads.
|
|
59
59
|
import { createHash } from 'node:crypto';
|
|
60
|
-
import { aliasesFrom } from "./log.js";
|
|
60
|
+
import { aliasesFrom, readTreeMap, wholeLog } from "./log.js";
|
|
61
61
|
import { appendActionIfAbsent, checkPrecondition, listActions, projectedPreconditionValue, projectResources, isTwinBookkeeping } from "./actions.js";
|
|
62
|
-
import { canonicalJson, hashFieldValue } from "./hash.js";
|
|
62
|
+
import { canonicalJson, diffSubjectFields, hashFieldValue } from "./hash.js";
|
|
63
63
|
import { applyTwinWrite } from "./serve.js";
|
|
64
64
|
export const MARKER_KIND = 'volter.world.marker.v1';
|
|
65
65
|
export const CHANGESET_KIND = 'volter.world.changeset.v1';
|
|
66
66
|
/** The id reserved for the synthetic "everything this world has ever recorded" base. */
|
|
67
67
|
export const WORLD_BOOT_MARKER_ID = 'world-boot';
|
|
68
|
+
export const fieldDiffKey = (service, stateService, id) => JSON.stringify([service, stateService, id]);
|
|
68
69
|
/** Names that address a file on disk (markers, changesets): no separators, no traversal. */
|
|
69
70
|
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
70
71
|
export function assertSafeChangesetName(name, what = 'changeset') {
|
|
@@ -155,7 +156,36 @@ export function diffLedgers(opts) {
|
|
|
155
156
|
return a.rowIndex - b.rowIndex;
|
|
156
157
|
});
|
|
157
158
|
const actions = ordered.map((o) => o.entry);
|
|
158
|
-
return { world: opts.world, base: opts.base, actions, vendors: summarizeByVendor(actions), total: actions.length };
|
|
159
|
+
return { world: opts.world, base: opts.base, actions, vendors: summarizeByVendor(actions), total: actions.length, fieldChanges: describeActionFields(opts.ledgers, actions) };
|
|
160
|
+
}
|
|
161
|
+
/** Read each write's pre-state through the authoritative kernel projection. No UI history fold. */
|
|
162
|
+
export function describeActionFields(ledgers, actions) {
|
|
163
|
+
const fieldChanges = {};
|
|
164
|
+
for (const ledger of ledgers) {
|
|
165
|
+
const entries = wholeLog(ledger.stateService, ledger.controlRoot);
|
|
166
|
+
// An observation is atomic: its internal positions cannot be projected. Compare its
|
|
167
|
+
// writes with the state at the start of that batch, through the same kernel reader.
|
|
168
|
+
const positions = new Map();
|
|
169
|
+
let batchStart = 0;
|
|
170
|
+
entries.forEach((e, i) => {
|
|
171
|
+
if (!e.batch || e.batch !== entries[i - 1]?.batch)
|
|
172
|
+
batchStart = i;
|
|
173
|
+
if (!positions.has(e.id))
|
|
174
|
+
positions.set(e.id, e.batch ? batchStart : i);
|
|
175
|
+
});
|
|
176
|
+
for (const { action, service, stateService } of actions.filter(a => a.service === ledger.service && a.stateService === ledger.stateService)) {
|
|
177
|
+
const at = positions.get(action.id);
|
|
178
|
+
if (at === undefined || !action.fields)
|
|
179
|
+
continue;
|
|
180
|
+
const subject = readTreeMap(stateService, ledger.controlRoot, { at }).get(`${action.subject.type}:${action.subject.id}`);
|
|
181
|
+
const fields = subject?.fields ?? {};
|
|
182
|
+
fieldChanges[fieldDiffKey(service, stateService, action.id)] = diffSubjectFields({
|
|
183
|
+
subject: action.subject, fields, fieldHashes: Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, hashFieldValue(v)])),
|
|
184
|
+
latestEventId: '', updatedAt: '',
|
|
185
|
+
}, action.fields);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return fieldChanges;
|
|
159
189
|
}
|
|
160
190
|
/** Group a delta by vendor, then by operation — both in FIRST-OCCURRENCE order, so the rollup
|
|
161
191
|
* reads in the same causal order as the actions themselves. */
|
|
@@ -198,6 +228,11 @@ export function formatLedgerDelta(delta, opts = {}) {
|
|
|
198
228
|
for (const vendor of delta.vendors) {
|
|
199
229
|
const parts = vendor.operations.map((o) => `${o.count} ${o.operation}${renderIds(o.subjectIds)}`);
|
|
200
230
|
lines.push(` ${vendor.service}: ${parts.join(', ')}`);
|
|
231
|
+
for (const item of delta.actions.filter(a => a.service === vendor.service)) {
|
|
232
|
+
for (const [field, change] of Object.entries(delta.fieldChanges?.[fieldDiffKey(item.service, item.stateService, item.action.id)] ?? {})) {
|
|
233
|
+
lines.push(` ${item.action.subject.type}:${item.action.subject.id}.${field}: ${change.before === undefined ? '(absent)' : JSON.stringify(change.before)} → ${JSON.stringify(change.after)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
201
236
|
}
|
|
202
237
|
lines.push('', ` ${plural(delta.total, 'action')} across ${plural(delta.vendors.length, 'twin')}`);
|
|
203
238
|
return `${lines.join('\n')}\n`;
|
package/dist/src/derived-core.js
CHANGED
|
@@ -679,13 +679,18 @@ async function keptFiles(m, call, root) {
|
|
|
679
679
|
}
|
|
680
680
|
return out;
|
|
681
681
|
}
|
|
682
|
+
/** Presentation tag only: keep provisioning in history without copying credential request data. */
|
|
683
|
+
function provisioningInput(call) {
|
|
684
|
+
return call.request.headers.get('x-volter-world-activity') === 'provisioning' ? { input: { activity: 'provisioning' } } : {};
|
|
685
|
+
}
|
|
682
686
|
function recordedInput(m, call, merged) {
|
|
683
687
|
const id = call.operation.id;
|
|
684
688
|
const sent = sentOf.get(call);
|
|
685
|
-
const
|
|
689
|
+
const activity = call.request.headers.get('x-volter-world-activity') === 'provisioning' ? { activity: 'provisioning' } : {};
|
|
690
|
+
const asSent = sent ? { ...activity, path: sent.path, query: sent.query, body: sent.body, headers: sent.headers, ...(sent.lane ? { lane: sent.lane } : {}), ...(sent.requestId ? { requestId: sent.requestId } : {}), ...(sent.type ? { contentType: sent.type } : {}) } : { ...activity, path: call.params, body: merged };
|
|
686
691
|
if (id === 'graphql') {
|
|
687
692
|
const b = (sent?.body ?? {});
|
|
688
|
-
return { graphql: { query: b.query, variables: b.variables ?? {}, ...(b.operationName ? { operationName: b.operationName } : {}) }, ...(sent?.lane ? { lane: sent.lane } : {}), headers: sent?.headers ?? {} };
|
|
693
|
+
return { ...activity, graphql: { query: b.query, variables: b.variables ?? {}, ...(b.operationName ? { operationName: b.operationName } : {}) }, ...(sent?.lane ? { lane: sent.lane } : {}), headers: sent?.headers ?? {} };
|
|
689
694
|
}
|
|
690
695
|
if (id === 'clock')
|
|
691
696
|
return { clock: true };
|
|
@@ -1381,7 +1386,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1381
1386
|
return mintId(m, resource, root, at);
|
|
1382
1387
|
const { value } = await applyTwinWriteAtomic(m.service, (snapshot) => {
|
|
1383
1388
|
const id = mintId(m, resource, root, at, snapshot);
|
|
1384
|
-
return { kind: 'write', value: id, write: { operation: `${resource.slice(1)}.issue`, subjectType: storedType(m, resource), subjectId: id, fields: { issuedAt: at }, occurredAt: at, actor: { kind: 'system' } } };
|
|
1389
|
+
return { kind: 'write', value: id, write: { operation: `${resource.slice(1)}.issue`, subjectType: storedType(m, resource), subjectId: id, fields: { issuedAt: at }, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } } };
|
|
1385
1390
|
}, root);
|
|
1386
1391
|
return value;
|
|
1387
1392
|
},
|
|
@@ -1392,7 +1397,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1392
1397
|
throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
|
|
1393
1398
|
if (id !== undefined) {
|
|
1394
1399
|
const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
|
|
1395
|
-
const { result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, occurredAt: at, actor: { kind: 'system' } }, root);
|
|
1400
|
+
const { result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } }, root);
|
|
1396
1401
|
if (operation)
|
|
1397
1402
|
observeStateWrite(m, result, before, root, stateChecks);
|
|
1398
1403
|
return id;
|
|
@@ -1411,7 +1416,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1411
1416
|
}
|
|
1412
1417
|
return `${prefix}${max + 1n}`;
|
|
1413
1418
|
})();
|
|
1414
|
-
return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, occurredAt: at, actor: { kind: 'system' } } };
|
|
1419
|
+
return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } } };
|
|
1415
1420
|
}, root, () => resourcesOfType(m.service, type, root));
|
|
1416
1421
|
if (operation && result)
|
|
1417
1422
|
observeStateWrite(m, result, undefined, root, stateChecks);
|
|
@@ -1488,7 +1493,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1488
1493
|
const w = d.write;
|
|
1489
1494
|
if (transitionObserver)
|
|
1490
1495
|
before = resources.find((r) => r.type === storedType(m, w.resource) && r.id === w.id);
|
|
1491
|
-
return { kind: 'write', value: d.value, write: { operation: w.operation, subjectType: storedType(m, w.resource), subjectId: w.id, fields: w.fields, occurredAt: at, actor: { kind: 'agent' } } };
|
|
1496
|
+
return { kind: 'write', value: d.value, write: { operation: w.operation, subjectType: storedType(m, w.resource), subjectId: w.id, fields: w.fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'agent' } } };
|
|
1492
1497
|
}, root);
|
|
1493
1498
|
if (result)
|
|
1494
1499
|
observeStateWrite(m, result, before, root, stateChecks);
|
package/dist/src/log.js
CHANGED
|
@@ -31,6 +31,8 @@ import { hashFieldValue } from "./hash.js";
|
|
|
31
31
|
import { worldPaths } from "./storage.js";
|
|
32
32
|
import { getActiveWorldStore } from "./world-store.js";
|
|
33
33
|
export const CHECKPOINT_EVERY = 100;
|
|
34
|
+
// Retained checkpoints and process memos are disposable projections, never history.
|
|
35
|
+
const TREE_PROJECTION_VERSION = 2;
|
|
34
36
|
export const DELTA_SUFFIX = '.delta';
|
|
35
37
|
export function parentLogPath(service, root) { return worldPaths(service, root).events; }
|
|
36
38
|
export function branchLogPath(service, root) { return join(dirname(worldPaths(service, root).events), 'actions.jsonl'); }
|
|
@@ -389,7 +391,13 @@ export function foldHistory(entries, layout, service) {
|
|
|
389
391
|
const baseCount = layout.base ? historyLength(layout.base) : 0;
|
|
390
392
|
const tree = layout.base ? foldHistory(entries.slice(0, baseCount), layout.base, service) : new Map();
|
|
391
393
|
const parent = entries.slice(0, baseCount + layout.parent);
|
|
392
|
-
|
|
394
|
+
const inheritedIds = landedIds(parent.slice(0, baseCount));
|
|
395
|
+
const ownParent = parent.slice(baseCount);
|
|
396
|
+
// A local acknowledgement is provenance for an inherited write, not a newer write.
|
|
397
|
+
// Reapplying it would overwrite later upstream fields (including vendor counters).
|
|
398
|
+
// Deployment outcomes remain observations and must still fold their returned fields.
|
|
399
|
+
const acknowledged = new Set([...inheritedIds, ...ownParent.filter(e => e.landsId && e.receipt?.status === 'landed' && inheritedIds.has(e.landsId)).map(e => e.id)]);
|
|
400
|
+
foldEntries(tree, ownParent, acknowledged);
|
|
393
401
|
foldEntries(tree, entries.slice(baseCount + layout.parent, baseCount + layout.parent + layout.branch), landedIds(parent));
|
|
394
402
|
foldAliases(tree, aliasesFrom(parent), service);
|
|
395
403
|
return tree;
|
|
@@ -415,7 +423,7 @@ function statKey(path) { const s = getActiveWorldStore().stat(path); return s ?
|
|
|
415
423
|
/** What a service's PARENT side at `root` is folded from (its parent log, branch.json and inherited history), as one
|
|
416
424
|
* string: while it stands, its aliases and landed ids are the same, whatever the branch's own log does. */
|
|
417
425
|
export function parentStamp(service, root) { return parentKeyOf(service, root); }
|
|
418
|
-
function parentKeyOf(service, root) { return `${statKey(parentLogPath(service, root))}|${getActiveWorldStore().read(branchMetaPath(service, root)) ?? ''}|${inheritedHistoryKey(service, root)}`; }
|
|
426
|
+
function parentKeyOf(service, root) { return `${TREE_PROJECTION_VERSION}|${statKey(parentLogPath(service, root))}|${getActiveWorldStore().read(branchMetaPath(service, root)) ?? ''}|${inheritedHistoryKey(service, root)}`; }
|
|
419
427
|
/** What a service's tree (and every subject's history) at `root` is folded from, as one string: while it
|
|
420
428
|
* stands, nothing was written. A pack that projects the tree into its own state memoizes on it. */
|
|
421
429
|
export function treeStamp(service, root) { return `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}`; }
|
|
@@ -595,7 +603,7 @@ function readTreeMapFull(service, root, opts = {}) {
|
|
|
595
603
|
const branchAll = branchEntries(service, root);
|
|
596
604
|
const until = opts.until === undefined ? -1 : branchAll.findIndex(e => e.id === opts.until);
|
|
597
605
|
const branch = until < 0 ? branchAll : branchAll.slice(0, until);
|
|
598
|
-
const parentDigest = hashFieldValue({ inherited, own });
|
|
606
|
+
const parentDigest = hashFieldValue({ projectionVersion: TREE_PROJECTION_VERSION, inherited, own });
|
|
599
607
|
const branchDigest = hashFieldValue(branch);
|
|
600
608
|
const raw = opts.until === undefined ? getActiveWorldStore().read(checkpointPath(service, root)) : null;
|
|
601
609
|
const cp = raw === null ? null : parseExactJson(raw);
|
|
@@ -622,7 +630,7 @@ export function cutCheckpoint(service, root) {
|
|
|
622
630
|
const subjects = [...readTreeMap(service, root).values()];
|
|
623
631
|
const inherited = inheritedHistory(service, root);
|
|
624
632
|
const own = ownParentEntries(service, root);
|
|
625
|
-
getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest: hashFieldValue({ inherited, own }), branchDigest: hashFieldValue(branchEntries(service, root)), branchCount: branchEntries(service, root).length, subjects }));
|
|
633
|
+
getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest: hashFieldValue({ projectionVersion: TREE_PROJECTION_VERSION, inherited, own }), branchDigest: hashFieldValue(branchEntries(service, root)), branchCount: branchEntries(service, root).length, subjects }));
|
|
626
634
|
});
|
|
627
635
|
}
|
|
628
636
|
/** The branch's entries the parent does not hold: `set` rows, not reverted, not landed. */
|
|
@@ -776,8 +784,11 @@ export function rebaseBranch(service, root, opts = {}) {
|
|
|
776
784
|
// a field both sides set to an INSTANT (updated_at, created) is when each wrote, not what: never a conflict
|
|
777
785
|
const instant = (v) => typeof v === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d/.test(v) && Number.isFinite(Date.parse(v));
|
|
778
786
|
const currentAliases = aliasesFrom(historyEntries(next));
|
|
787
|
+
const incomingLanded = landedIds(historyEntries(next));
|
|
779
788
|
const conflicts = [];
|
|
780
|
-
|
|
789
|
+
// Acknowledged writes, including their counter effects, now belong to the incoming parent.
|
|
790
|
+
// Comparing them again against a later upstream write creates conflicts with already-landed work.
|
|
791
|
+
for (const e of unpushedEntries(service, root).filter(e => !incomingLanded.has(e.id))) {
|
|
781
792
|
if (e.op !== 'set' || !e.fields || !existed.has(`${e.subject.type}:${e.subject.id}`))
|
|
782
793
|
continue;
|
|
783
794
|
const fields = resolveReferences(service, e.subject.type, e.fields, currentAliases);
|
package/dist/src/pack-fetch.js
CHANGED
|
@@ -11,7 +11,7 @@ import { ORIGINAL_PATH_HEADER } from "./sigv4.js";
|
|
|
11
11
|
import { placedCookies, TWIN_PREFIX_HEADER } from "./twin-fetch.js";
|
|
12
12
|
import { PREFIX_PARAMS_HEADER } from "./derived.js";
|
|
13
13
|
import { withHeaders } from "./with-headers.js";
|
|
14
|
-
import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, scenarioDecisionHeader, coreFor, crossCutting, derivedRequestScopes, vendorError } from "./derived-core.js";
|
|
14
|
+
import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, scenarioDecisionHeader, storedType, coreFor, crossCutting, derivedRequestScopes, vendorError } from "./derived-core.js";
|
|
15
15
|
import { compileSurface, createDerivedFetch, matchOperation, pathParams } from "./derived.js";
|
|
16
16
|
import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from "./request-scope.js";
|
|
17
17
|
import { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from "./scenario.js";
|
|
@@ -428,7 +428,18 @@ export function createPackFetch(parts, options = {}) {
|
|
|
428
428
|
const discoveryDoors = [...(m.doors ?? []), ...(m.machines ? [{ id: 'machine-pool', method: 'POST', path: '/_twin/machine-pool', note: 'Enroll the World execution pool: { kind: light | docker | none | fake, images?: { image: { context, command?, runtime? } } }.' }] : [])];
|
|
429
429
|
// the workspaces a person can be shown (Viewing a World): each built workspace screen's id and path
|
|
430
430
|
const laneDiscovery = await laneDoor(request, '/twin');
|
|
431
|
-
const
|
|
431
|
+
const resources = {};
|
|
432
|
+
for (const [type, spec] of Object.entries(m.resources)) {
|
|
433
|
+
const resource = resources[storedType(m, type)] ??= { relations: [] };
|
|
434
|
+
const relations = [
|
|
435
|
+
...(spec.parent ? [{ field: spec.parent.field, to: storedType(m, spec.parent.resource) }] : []),
|
|
436
|
+
...Object.entries(spec.embeds ?? {}).map(([field, to]) => ({ field, to: storedType(m, to) })),
|
|
437
|
+
];
|
|
438
|
+
for (const relation of relations)
|
|
439
|
+
if (!resource.relations.some(r => r.field === relation.field && r.to === relation.to))
|
|
440
|
+
resource.relations.push(relation);
|
|
441
|
+
}
|
|
442
|
+
const shown = { resources, ...(workspaces().length ? { screens: workspaces() } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) };
|
|
432
443
|
const identity = d.identity ?? (m.auth?.held ? 'Authenticate as the vendor does, using a credential issued in this World; unknown credentials are refused.' : undefined);
|
|
433
444
|
// a model vendor's: its state, and what its scenario scripts, with the scenario's live counts
|
|
434
445
|
if (parts.scenario && d.behavior) {
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export { forkTwin, isFork, readForkMeta, } from './fork.js';
|
|
|
46
46
|
export type { ForkMeta } from './fork.js';
|
|
47
47
|
export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction, listActions, listTransactionCommits, pendingActions, pushablePendingActions, isTwinBookkeeping, pendingTransactionCommits, projectResources, projectOwnerResources, OwnerStoreAmbiguousError, revertAction, TwinActionPreconditionError, subjectAliases, resolveSubjectId, runWithCorrelationId, currentCorrelationId } from './actions.js';
|
|
48
48
|
export type { ActionProjection, ProjectedDelivery, ProjectedResource, ProjectedResourcePatch, ProjectedResourceRef, TwinAction, TwinActionOp, TwinActionPrecondition, TwinActionPreconditionOp, TwinActionRevertSpec, TwinTransactionCommit, TwinTransactionCommitOp, TwinTransactionPrecondition, TwinTransactionRevertSpec, } from './actions.js';
|
|
49
|
-
export { approveChangeset, assertMarkerBelongsTo, assertSafeChangesetName, assertValidVerifiers, buildChangeset, captureMarker, changesetContentHash, changesetHashMatches, changesetReadiness, approverOf, CHANGESET_KIND, diffLedgers, formatApplication, formatChangeset, formatRebaseReport, narrateActions, narrationDrift, rebaseChangeset, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, MARKER_KIND, normalizeChangeset, parseVerifierExpression, replayChangeset, runChangesetVerifiers, summarizeByVendor, twinWriteShape, withApplication, withVerification, worldBootMarker, WORLD_BOOT_MARKER_ID, } from './changeset.js';
|
|
49
|
+
export { approveChangeset, assertMarkerBelongsTo, assertSafeChangesetName, assertValidVerifiers, buildChangeset, captureMarker, changesetContentHash, changesetHashMatches, changesetReadiness, approverOf, CHANGESET_KIND, diffLedgers, describeActionFields, fieldDiffKey, formatApplication, formatChangeset, formatRebaseReport, narrateActions, narrationDrift, rebaseChangeset, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, MARKER_KIND, normalizeChangeset, parseVerifierExpression, replayChangeset, runChangesetVerifiers, summarizeByVendor, twinWriteShape, withApplication, withVerification, worldBootMarker, WORLD_BOOT_MARKER_ID, } from './changeset.js';
|
|
50
50
|
export type { ApplyReceipt, RebaseActionResult, RebaseReport, RebaseTarget, Changeset, ChangesetAction, ChangesetApplication, CompensationEntry, ChangesetApproval, ChangesetReadiness, ChangesetVerification, ChangesetVerifier, ChangesetVerifierResult, LedgerDelta, LedgerPosition, LedgerRef, ReplayActionResult, ReplayReport, ReplayTarget, VendorSummary, WorldMarker, } from './changeset.js';
|
|
51
51
|
export { getActivePackAssets, setActivePackAssets, packAsset, assetContentType, confinedAssetPath, LocalPackAssets, BindingPackAssets, type PackAssetStore } from './pack-assets.js';
|
|
52
52
|
export { installedPackageDir, installedVendorPacks, packVendor, packRepository, isDerivedPack, vendorPackageDir, vendorOfPackModule } from './pack-paths.js';
|
package/dist/src/runtime.js
CHANGED
|
@@ -66,7 +66,7 @@ export { appendAction, appendTransactionCommit, checkPrecondition, confirmAction
|
|
|
66
66
|
// the content-addressed, replayable changeset over that delta. State-level by construction
|
|
67
67
|
// (it reads the same per-service action ledgers `actions.ts` writes); `volter-world` supplies
|
|
68
68
|
// world discovery and thin verbs on top.
|
|
69
|
-
export { approveChangeset, assertMarkerBelongsTo, assertSafeChangesetName, assertValidVerifiers, buildChangeset, captureMarker, changesetContentHash, changesetHashMatches, changesetReadiness, approverOf, CHANGESET_KIND, diffLedgers, formatApplication, formatChangeset, formatRebaseReport, narrateActions, narrationDrift, rebaseChangeset, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, MARKER_KIND, normalizeChangeset, parseVerifierExpression, replayChangeset, runChangesetVerifiers, summarizeByVendor, twinWriteShape, withApplication, withVerification, worldBootMarker, WORLD_BOOT_MARKER_ID, } from "./changeset.js";
|
|
69
|
+
export { approveChangeset, assertMarkerBelongsTo, assertSafeChangesetName, assertValidVerifiers, buildChangeset, captureMarker, changesetContentHash, changesetHashMatches, changesetReadiness, approverOf, CHANGESET_KIND, diffLedgers, describeActionFields, fieldDiffKey, formatApplication, formatChangeset, formatRebaseReport, narrateActions, narrationDrift, rebaseChangeset, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, MARKER_KIND, normalizeChangeset, parseVerifierExpression, replayChangeset, runChangesetVerifiers, summarizeByVendor, twinWriteShape, withApplication, withVerification, worldBootMarker, WORLD_BOOT_MARKER_ID, } from "./changeset.js";
|
|
70
70
|
export { getActivePackAssets, setActivePackAssets, packAsset, assetContentType, confinedAssetPath, LocalPackAssets, BindingPackAssets } from "./pack-assets.js";
|
|
71
71
|
export { installedPackageDir, installedVendorPacks, packVendor, packRepository, isDerivedPack, vendorPackageDir, vendorOfPackModule } from "./pack-paths.js";
|
|
72
72
|
// The git plane (contract "The git plane is a kernel library"): objects, packs, refs, smart HTTP.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.140",
|
|
4
4
|
"description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
package/src/changeset.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { worldPaths } from './storage.ts';
|
|
|
20
20
|
// silently reporting the wrong delta.
|
|
21
21
|
//
|
|
22
22
|
// DELTA every action appended after the marker, across every ledger, ordered by
|
|
23
|
-
// `occurredAt` (
|
|
23
|
+
// `occurredAt` with stable ledger/row ties (a timeline, not cross-vendor causality), grouped by vendor
|
|
24
24
|
// for rendering.
|
|
25
25
|
//
|
|
26
26
|
// CHANGESET a delta frozen into a content-addressed object: `{id, world, base, actions,
|
|
@@ -57,14 +57,14 @@ import { worldPaths } from './storage.ts';
|
|
|
57
57
|
// the control root that holds it) and never learns what a world is. `volter-world` supplies
|
|
58
58
|
// discovery and the CLI verbs; the state-level logic lives here, beside the ledgers it reads.
|
|
59
59
|
import { createHash } from 'node:crypto';
|
|
60
|
-
import { aliasesFrom, parentEntries, type Entry } from './log.ts';
|
|
60
|
+
import { aliasesFrom, parentEntries, readTreeMap, wholeLog, type Entry } from './log.ts';
|
|
61
61
|
import { appendActionIfAbsent, checkPrecondition, listActions, projectedPreconditionValue, projectResources, isTwinBookkeeping } from './actions.ts';
|
|
62
62
|
import type { TwinActionRevertSpec } from './actions.ts';
|
|
63
63
|
import { touchedSubjects } from './actions.ts';
|
|
64
64
|
import type { PerformAction } from './head.ts';
|
|
65
65
|
import type { RemoteExecute } from './remote-execute.ts';
|
|
66
66
|
import type { TwinAction, TwinActionPrecondition, TwinActionPreconditionOp } from './actions.ts';
|
|
67
|
-
import { canonicalJson, hashFieldValue } from './hash.ts';
|
|
67
|
+
import { canonicalJson, diffSubjectFields, hashFieldValue, type FieldChange } from './hash.ts';
|
|
68
68
|
import { applyTwinWrite } from './serve.ts';
|
|
69
69
|
import type { TwinResource } from './serve.ts';
|
|
70
70
|
|
|
@@ -130,8 +130,12 @@ export type LedgerDelta = {
|
|
|
130
130
|
actions: ChangesetAction[];
|
|
131
131
|
vendors: VendorSummary[];
|
|
132
132
|
total: number;
|
|
133
|
+
/** Presentation only, outside changeset bodies: fields immediately before and after each write. */
|
|
134
|
+
fieldChanges?: Record<string, Record<string, FieldChange>>;
|
|
133
135
|
};
|
|
134
136
|
|
|
137
|
+
export const fieldDiffKey = (service: string, stateService: string, id: string): string => JSON.stringify([service, stateService, id]);
|
|
138
|
+
|
|
135
139
|
/** A verifier: one deterministic check against post-replay projected state — the CI assertion
|
|
136
140
|
* of the operational PR (docs/concepts/the-model.md v1). The `assert` is the kernel's ONE check
|
|
137
141
|
* expression (`TwinActionPrecondition`: subject/field/op/value, evaluated by
|
|
@@ -331,7 +335,34 @@ export function diffLedgers(opts: { world: string; ledgers: LedgerRef[]; base: W
|
|
|
331
335
|
});
|
|
332
336
|
|
|
333
337
|
const actions = ordered.map((o) => o.entry);
|
|
334
|
-
return { world: opts.world, base: opts.base, actions, vendors: summarizeByVendor(actions), total: actions.length };
|
|
338
|
+
return { world: opts.world, base: opts.base, actions, vendors: summarizeByVendor(actions), total: actions.length, fieldChanges: describeActionFields(opts.ledgers, actions) };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Read each write's pre-state through the authoritative kernel projection. No UI history fold. */
|
|
342
|
+
export function describeActionFields(ledgers: LedgerRef[], actions: ChangesetAction[]): NonNullable<LedgerDelta['fieldChanges']> {
|
|
343
|
+
const fieldChanges: NonNullable<LedgerDelta['fieldChanges']> = {};
|
|
344
|
+
for (const ledger of ledgers) {
|
|
345
|
+
const entries = wholeLog(ledger.stateService, ledger.controlRoot);
|
|
346
|
+
// An observation is atomic: its internal positions cannot be projected. Compare its
|
|
347
|
+
// writes with the state at the start of that batch, through the same kernel reader.
|
|
348
|
+
const positions = new Map<string, number>();
|
|
349
|
+
let batchStart = 0;
|
|
350
|
+
entries.forEach((e, i) => {
|
|
351
|
+
if (!e.batch || e.batch !== entries[i - 1]?.batch) batchStart = i;
|
|
352
|
+
if (!positions.has(e.id)) positions.set(e.id, e.batch ? batchStart : i);
|
|
353
|
+
});
|
|
354
|
+
for (const { action, service, stateService } of actions.filter(a => a.service === ledger.service && a.stateService === ledger.stateService)) {
|
|
355
|
+
const at = positions.get(action.id);
|
|
356
|
+
if (at === undefined || !action.fields) continue;
|
|
357
|
+
const subject = readTreeMap(stateService, ledger.controlRoot, { at }).get(`${action.subject.type}:${action.subject.id}`);
|
|
358
|
+
const fields = subject?.fields ?? {};
|
|
359
|
+
fieldChanges[fieldDiffKey(service, stateService, action.id)] = diffSubjectFields({
|
|
360
|
+
subject: action.subject, fields, fieldHashes: Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, hashFieldValue(v)])),
|
|
361
|
+
latestEventId: '', updatedAt: '',
|
|
362
|
+
}, action.fields);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return fieldChanges;
|
|
335
366
|
}
|
|
336
367
|
|
|
337
368
|
/** Group a delta by vendor, then by operation — both in FIRST-OCCURRENCE order, so the rollup
|
|
@@ -376,6 +407,11 @@ export function formatLedgerDelta(delta: LedgerDelta, opts: { title?: string } =
|
|
|
376
407
|
for (const vendor of delta.vendors) {
|
|
377
408
|
const parts = vendor.operations.map((o) => `${o.count} ${o.operation}${renderIds(o.subjectIds)}`);
|
|
378
409
|
lines.push(` ${vendor.service}: ${parts.join(', ')}`);
|
|
410
|
+
for (const item of delta.actions.filter(a => a.service === vendor.service)) {
|
|
411
|
+
for (const [field, change] of Object.entries(delta.fieldChanges?.[fieldDiffKey(item.service, item.stateService, item.action.id)] ?? {})) {
|
|
412
|
+
lines.push(` ${item.action.subject.type}:${item.action.subject.id}.${field}: ${change.before === undefined ? '(absent)' : JSON.stringify(change.before)} → ${JSON.stringify(change.after)}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
379
415
|
}
|
|
380
416
|
lines.push('', ` ${plural(delta.total, 'action')} across ${plural(delta.vendors.length, 'twin')}`);
|
|
381
417
|
return `${lines.join('\n')}\n`;
|
|
@@ -984,6 +1020,8 @@ export type CompensationEntry = {
|
|
|
984
1020
|
export type ChangesetApplication = {
|
|
985
1021
|
at: string;
|
|
986
1022
|
contentHash: string;
|
|
1023
|
+
/** A push receipt names its destination; absent on legacy and non-push applications. */
|
|
1024
|
+
destination?: { url: string; namespace: string };
|
|
987
1025
|
outcome: 'applied' | 'partial' | 'refused';
|
|
988
1026
|
forced?: boolean;
|
|
989
1027
|
refusal?: string[];
|
package/src/derived-core.ts
CHANGED
|
@@ -1149,13 +1149,19 @@ async function keptFiles(m: DerivedManifest, call: DerivedCall, root: string | u
|
|
|
1149
1149
|
return out;
|
|
1150
1150
|
}
|
|
1151
1151
|
|
|
1152
|
+
/** Presentation tag only: keep provisioning in history without copying credential request data. */
|
|
1153
|
+
function provisioningInput(call: DerivedCall): { input?: { activity: 'provisioning' } } {
|
|
1154
|
+
return call.request.headers.get('x-volter-world-activity') === 'provisioning' ? { input: { activity: 'provisioning' } } : {};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1152
1157
|
function recordedInput(m: DerivedManifest, call: DerivedCall, merged: Record<string, unknown>): Record<string, unknown> {
|
|
1153
1158
|
const id = call.operation.id;
|
|
1154
1159
|
const sent = sentOf.get(call);
|
|
1155
|
-
const
|
|
1160
|
+
const activity = call.request.headers.get('x-volter-world-activity') === 'provisioning' ? { activity: 'provisioning' } : {};
|
|
1161
|
+
const asSent = sent ? { ...activity, path: sent.path, query: sent.query, body: sent.body, headers: sent.headers, ...(sent.lane ? { lane: sent.lane } : {}), ...(sent.requestId ? { requestId: sent.requestId } : {}), ...(sent.type ? { contentType: sent.type } : {}) } : { ...activity, path: call.params, body: merged };
|
|
1156
1162
|
if (id === 'graphql') {
|
|
1157
1163
|
const b = (sent?.body ?? {}) as { query?: unknown; variables?: unknown; operationName?: unknown };
|
|
1158
|
-
return { graphql: { query: b.query, variables: b.variables ?? {}, ...(b.operationName ? { operationName: b.operationName } : {}) }, ...(sent?.lane ? { lane: sent.lane } : {}), headers: sent?.headers ?? {} };
|
|
1164
|
+
return { ...activity, graphql: { query: b.query, variables: b.variables ?? {}, ...(b.operationName ? { operationName: b.operationName } : {}) }, ...(sent?.lane ? { lane: sent.lane } : {}), headers: sent?.headers ?? {} };
|
|
1159
1165
|
}
|
|
1160
1166
|
if (id === 'clock') return { clock: true };
|
|
1161
1167
|
if (id === 'unmatched') return { gap: true, ...asSent };
|
|
@@ -1994,7 +2000,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
|
|
|
1994
2000
|
if (scope.readOnly === true || isReadOnlyRequest(call.request)) return mintId(m, resource, root, at);
|
|
1995
2001
|
const { value } = await applyTwinWriteAtomic(m.service, (snapshot) => {
|
|
1996
2002
|
const id = mintId(m, resource, root, at, snapshot);
|
|
1997
|
-
return { kind: 'write', value: id, write: { operation: `${resource.slice(1)}.issue`, subjectType: storedType(m, resource), subjectId: id, fields: { issuedAt: at }, occurredAt: at, actor: { kind: 'system' } } };
|
|
2003
|
+
return { kind: 'write', value: id, write: { operation: `${resource.slice(1)}.issue`, subjectType: storedType(m, resource), subjectId: id, fields: { issuedAt: at }, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } } };
|
|
1998
2004
|
}, root);
|
|
1999
2005
|
return value;
|
|
2000
2006
|
},
|
|
@@ -2004,7 +2010,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
|
|
|
2004
2010
|
if (!type.startsWith('_')) throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
|
|
2005
2011
|
if (id !== undefined) {
|
|
2006
2012
|
const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
|
|
2007
|
-
const { result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, occurredAt: at, actor: { kind: 'system' } }, root);
|
|
2013
|
+
const { result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } }, root);
|
|
2008
2014
|
if (operation) observeStateWrite(m, result, before, root, stateChecks);
|
|
2009
2015
|
return id;
|
|
2010
2016
|
}
|
|
@@ -2020,7 +2026,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
|
|
|
2020
2026
|
}
|
|
2021
2027
|
return `${prefix}${max + 1n}`;
|
|
2022
2028
|
})();
|
|
2023
|
-
return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, occurredAt: at, actor: { kind: 'system' } } };
|
|
2029
|
+
return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } } };
|
|
2024
2030
|
}, root, () => resourcesOfType(m.service, type, root));
|
|
2025
2031
|
if (operation && result) observeStateWrite(m, result, undefined, root, stateChecks);
|
|
2026
2032
|
return value;
|
|
@@ -2088,7 +2094,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
|
|
|
2088
2094
|
if (!d.write) return { kind: 'skip', value: d.value };
|
|
2089
2095
|
const w = d.write;
|
|
2090
2096
|
if (transitionObserver) before = resources.find((r) => r.type === storedType(m, w.resource) && r.id === w.id);
|
|
2091
|
-
return { kind: 'write', value: d.value, write: { operation: w.operation, subjectType: storedType(m, w.resource), subjectId: w.id, fields: w.fields, occurredAt: at, actor: { kind: 'agent' } } };
|
|
2097
|
+
return { kind: 'write', value: d.value, write: { operation: w.operation, subjectType: storedType(m, w.resource), subjectId: w.id, fields: w.fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'agent' } } };
|
|
2092
2098
|
},
|
|
2093
2099
|
root,
|
|
2094
2100
|
);
|
package/src/log.ts
CHANGED
|
@@ -92,6 +92,8 @@ export type BranchMeta = {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
export const CHECKPOINT_EVERY = 100;
|
|
95
|
+
// Retained checkpoints and process memos are disposable projections, never history.
|
|
96
|
+
const TREE_PROJECTION_VERSION = 2;
|
|
95
97
|
export const DELTA_SUFFIX = '.delta';
|
|
96
98
|
|
|
97
99
|
export function parentLogPath(service: string, root?: string): string { return worldPaths(service, root).events; }
|
|
@@ -434,7 +436,13 @@ export function foldHistory(entries: Entry[], layout: HistoryLayout, service?: s
|
|
|
434
436
|
const baseCount = layout.base ? historyLength(layout.base) : 0;
|
|
435
437
|
const tree: Tree = layout.base ? foldHistory(entries.slice(0, baseCount), layout.base, service) : new Map();
|
|
436
438
|
const parent = entries.slice(0, baseCount + layout.parent);
|
|
437
|
-
|
|
439
|
+
const inheritedIds = landedIds(parent.slice(0, baseCount));
|
|
440
|
+
const ownParent = parent.slice(baseCount);
|
|
441
|
+
// A local acknowledgement is provenance for an inherited write, not a newer write.
|
|
442
|
+
// Reapplying it would overwrite later upstream fields (including vendor counters).
|
|
443
|
+
// Deployment outcomes remain observations and must still fold their returned fields.
|
|
444
|
+
const acknowledged = new Set([...inheritedIds, ...ownParent.filter(e => e.landsId && e.receipt?.status === 'landed' && inheritedIds.has(e.landsId)).map(e => e.id)]);
|
|
445
|
+
foldEntries(tree, ownParent, acknowledged);
|
|
438
446
|
foldEntries(tree, entries.slice(baseCount + layout.parent, baseCount + layout.parent + layout.branch), landedIds(parent));
|
|
439
447
|
foldAliases(tree, aliasesFrom(parent), service);
|
|
440
448
|
return tree;
|
|
@@ -476,7 +484,7 @@ function statKey(path: string): string { const s = getActiveWorldStore().stat(pa
|
|
|
476
484
|
/** What a service's PARENT side at `root` is folded from (its parent log, branch.json and inherited history), as one
|
|
477
485
|
* string: while it stands, its aliases and landed ids are the same, whatever the branch's own log does. */
|
|
478
486
|
export function parentStamp(service: string, root?: string): string { return parentKeyOf(service, root); }
|
|
479
|
-
function parentKeyOf(service: string, root?: string): string { return `${statKey(parentLogPath(service, root))}|${getActiveWorldStore().read(branchMetaPath(service, root)) ?? ''}|${inheritedHistoryKey(service, root)}`; }
|
|
487
|
+
function parentKeyOf(service: string, root?: string): string { return `${TREE_PROJECTION_VERSION}|${statKey(parentLogPath(service, root))}|${getActiveWorldStore().read(branchMetaPath(service, root)) ?? ''}|${inheritedHistoryKey(service, root)}`; }
|
|
480
488
|
/** What a service's tree (and every subject's history) at `root` is folded from, as one string: while it
|
|
481
489
|
* stands, nothing was written. A pack that projects the tree into its own state memoizes on it. */
|
|
482
490
|
export function treeStamp(service: string, root?: string): string { return `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}`; }
|
|
@@ -612,7 +620,7 @@ function readTreeMapFull(service: string, root?: string, opts: { until?: string;
|
|
|
612
620
|
const branchAll = branchEntries(service, root);
|
|
613
621
|
const until = opts.until === undefined ? -1 : branchAll.findIndex(e => e.id === opts.until);
|
|
614
622
|
const branch = until < 0 ? branchAll : branchAll.slice(0, until);
|
|
615
|
-
const parentDigest = hashFieldValue({ inherited, own }); const branchDigest = hashFieldValue(branch);
|
|
623
|
+
const parentDigest = hashFieldValue({ projectionVersion: TREE_PROJECTION_VERSION, inherited, own }); const branchDigest = hashFieldValue(branch);
|
|
616
624
|
const raw = opts.until === undefined ? getActiveWorldStore().read(checkpointPath(service, root)) : null;
|
|
617
625
|
const cp = raw === null ? null : parseExactJson(raw) as Checkpoint;
|
|
618
626
|
if (cp?.parentDigest === parentDigest && cp.branchDigest === branchDigest) return new Map(cp.subjects.map(s => [`${s.type}:${s.id}`, { ...s, fields: { ...s.fields } }]));
|
|
@@ -633,7 +641,7 @@ export function cutCheckpoint(service: string, root?: string): void {
|
|
|
633
641
|
dropCheckpoint(service, root);
|
|
634
642
|
const subjects = [...readTreeMap(service, root).values()];
|
|
635
643
|
const inherited = inheritedHistory(service, root); const own = ownParentEntries(service, root);
|
|
636
|
-
getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest: hashFieldValue({ inherited, own }), branchDigest: hashFieldValue(branchEntries(service, root)), branchCount: branchEntries(service, root).length, subjects }));
|
|
644
|
+
getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest: hashFieldValue({ projectionVersion: TREE_PROJECTION_VERSION, inherited, own }), branchDigest: hashFieldValue(branchEntries(service, root)), branchCount: branchEntries(service, root).length, subjects }));
|
|
637
645
|
});
|
|
638
646
|
}
|
|
639
647
|
|
|
@@ -773,8 +781,11 @@ export function rebaseBranch(service: string, root?: string, opts: { origin?: bo
|
|
|
773
781
|
// a field both sides set to an INSTANT (updated_at, created) is when each wrote, not what: never a conflict
|
|
774
782
|
const instant = (v: unknown): boolean => typeof v === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d/.test(v) && Number.isFinite(Date.parse(v));
|
|
775
783
|
const currentAliases = aliasesFrom(historyEntries(next));
|
|
784
|
+
const incomingLanded = landedIds(historyEntries(next));
|
|
776
785
|
const conflicts: Array<{ entryId: string; subject: { type: string; id: string }; field: string; op: string; expected?: unknown; actual?: unknown }> = [];
|
|
777
|
-
|
|
786
|
+
// Acknowledged writes, including their counter effects, now belong to the incoming parent.
|
|
787
|
+
// Comparing them again against a later upstream write creates conflicts with already-landed work.
|
|
788
|
+
for (const e of unpushedEntries(service, root).filter(e => !incomingLanded.has(e.id))) {
|
|
778
789
|
if (e.op !== 'set' || !e.fields || !existed.has(`${e.subject.type}:${e.subject.id}`)) continue;
|
|
779
790
|
const fields = resolveReferences(service, e.subject.type, e.fields, currentAliases);
|
|
780
791
|
for (const p of since) {
|
package/src/pack-fetch.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { ORIGINAL_PATH_HEADER } from './sigv4.ts';
|
|
|
11
11
|
import { placedCookies, TWIN_PREFIX_HEADER } from './twin-fetch.ts';
|
|
12
12
|
import { PREFIX_PARAMS_HEADER } from './derived.ts';
|
|
13
13
|
import { withHeaders } from './with-headers.ts';
|
|
14
|
-
import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, scenarioDecisionHeader, coreFor, crossCutting, derivedRequestScopes, vendorError, type CoreScope, type DerivedManifest, type ErrorSpec, type Handler, type HandlerContext, type LanesDecl, type VendorManifest } from './derived-core.ts';
|
|
14
|
+
import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, scenarioDecisionHeader, storedType, coreFor, crossCutting, derivedRequestScopes, vendorError, type CoreScope, type DerivedManifest, type ErrorSpec, type Handler, type HandlerContext, type LanesDecl, type VendorManifest } from './derived-core.ts';
|
|
15
15
|
import { compileSurface, createDerivedFetch, matchOperation, pathParams, type DerivedFetch, type DerivedOperation, type DerivedOwner, type DerivedSurface, type Workspace, type BoardFrame } from './derived.ts';
|
|
16
16
|
import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from './request-scope.ts';
|
|
17
17
|
import { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest, type PackScenarioAdapter } from './scenario.ts';
|
|
@@ -457,7 +457,16 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
457
457
|
const discoveryDoors = [...(m.doors ?? []), ...(m.machines ? [{ id: 'machine-pool', method: 'POST', path: '/_twin/machine-pool', note: 'Enroll the World execution pool: { kind: light | docker | none | fake, images?: { image: { context, command?, runtime? } } }.' }] : [])];
|
|
458
458
|
// the workspaces a person can be shown (Viewing a World): each built workspace screen's id and path
|
|
459
459
|
const laneDiscovery = await laneDoor(request, '/twin');
|
|
460
|
-
const
|
|
460
|
+
const resources: Record<string, { relations: Array<{ field: string; to: string }> }> = {};
|
|
461
|
+
for (const [type, spec] of Object.entries(m.resources)) {
|
|
462
|
+
const resource = resources[storedType(m, type)] ??= { relations: [] };
|
|
463
|
+
const relations = [
|
|
464
|
+
...(spec.parent ? [{ field: spec.parent.field, to: storedType(m, spec.parent.resource) }] : []),
|
|
465
|
+
...Object.entries(spec.embeds ?? {}).map(([field, to]) => ({ field, to: storedType(m, to) })),
|
|
466
|
+
];
|
|
467
|
+
for (const relation of relations) if (!resource.relations.some(r => r.field === relation.field && r.to === relation.to)) resource.relations.push(relation);
|
|
468
|
+
}
|
|
469
|
+
const shown = { resources, ...(workspaces().length ? { screens: workspaces() } : {}), ...(Object.keys(laneDiscovery).length ? { lanes: laneDiscovery } : {}) };
|
|
461
470
|
const identity = d.identity ?? (m.auth?.held ? 'Authenticate as the vendor does, using a credential issued in this World; unknown credentials are refused.' : undefined);
|
|
462
471
|
// a model vendor's: its state, and what its scenario scripts, with the scenario's live counts
|
|
463
472
|
if (parts.scenario && d.behavior) {
|