@amalgm/shell 0.1.141 → 0.1.143
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/PURPOSE.md +15 -3
- package/dist/detection/runtime.d.ts +6 -0
- package/dist/detection/runtime.js +27 -10
- package/dist/detection/runtime.js.map +1 -1
- package/dist/entity-apply-host.d.ts +3 -0
- package/dist/entity-apply-host.js +262 -9
- package/dist/entity-apply-host.js.map +1 -1
- package/dist/user-ground-host.d.ts +3 -1
- package/dist/user-ground-host.js +169 -18
- package/dist/user-ground-host.js.map +1 -1
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlin
|
|
|
3
3
|
import { open, readFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAmalgmDir, shippedUserHomeDeclaration, } from "@amalgm/core/identity";
|
|
6
|
-
import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, containedRecords, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, decodeAcceptedEntityRecord, deriveUploadManifest, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, pathIsSuspect, CONTENT_REFS_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, sameRecords, snapshotFromRecords, stableJson, streamCargo, travelingRecords, upload as uploadArtifact, userGroundRecords, GRAPH_HEAD_OPERATION_KIND, } from "@amalgm/live";
|
|
6
|
+
import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, containedRecords, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, decodeAcceptedEntityRecord, deriveUploadManifest, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, materializedSlotNames, pathIsSuspect, CONTENT_REFS_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, sameRecords, snapshotFromRecords, stableJson, streamCargo, travelingRecords, upload as uploadArtifact, userGroundRecords, GRAPH_HEAD_OPERATION_KIND, } from "@amalgm/live";
|
|
7
7
|
import Database from "better-sqlite3";
|
|
8
8
|
import { atomicCopy, atomicWrite, ensurePrivateDir, ensureUserDir } from "./filesystem.js";
|
|
9
9
|
import { emitFilesStage, } from "./files-observability.js";
|
|
@@ -1095,6 +1095,7 @@ export class UserGroundHost {
|
|
|
1095
1095
|
measurements: { type: evidence.type, phase: evidence.phase },
|
|
1096
1096
|
});
|
|
1097
1097
|
},
|
|
1098
|
+
onLawRelocationStage: this.options.onLawRelocationStage,
|
|
1098
1099
|
now: () => this.options.now?.().getTime() ?? Date.now(),
|
|
1099
1100
|
});
|
|
1100
1101
|
const settledMaterialization = host.adoptVerifiedMaterialization();
|
|
@@ -2345,6 +2346,11 @@ export class UserGroundHost {
|
|
|
2345
2346
|
if (!committed) {
|
|
2346
2347
|
throw pipelineStageError("detection", new DetectionBasisAdvancedError());
|
|
2347
2348
|
}
|
|
2349
|
+
settleMaterializedSlotAddresses(this.databasePath(identity), new Set([
|
|
2350
|
+
...notebookBasis.flatMap(({ row }) => row?.parentUUID ? [row.parentUUID] : []),
|
|
2351
|
+
...acceptedMaterializedRows.flatMap((row) => row.parentUUID ? [row.parentUUID] : []),
|
|
2352
|
+
...acceptedNotebookRows.flatMap((row) => row.parentUUID ? [row.parentUUID] : []),
|
|
2353
|
+
]));
|
|
2348
2354
|
const recordCommitDuration = performance.now() - recordCommitStarted;
|
|
2349
2355
|
if (detectedRecords.length === 0) {
|
|
2350
2356
|
this.stage({
|
|
@@ -3783,6 +3789,80 @@ function commitDetectedState(file, input) {
|
|
|
3783
3789
|
database.close();
|
|
3784
3790
|
}
|
|
3785
3791
|
}
|
|
3792
|
+
/** A local sibling-set change can alter collision-derived disk addresses
|
|
3793
|
+
* without changing any portable record. Detect commits its portable truth
|
|
3794
|
+
* first, then settles only the affected sibling groups while it still owns
|
|
3795
|
+
* their ground cone. Rows move before bytes, matching Apply's recovery law;
|
|
3796
|
+
* Watch observes the resulting renames as the same UUIDs at their newly
|
|
3797
|
+
* derived addresses and therefore emits no Record. */
|
|
3798
|
+
function settleMaterializedSlotAddresses(file, parentUUIDs) {
|
|
3799
|
+
if (parentUUIDs.size === 0)
|
|
3800
|
+
return;
|
|
3801
|
+
const entityRows = readGroundRows(file, "entities");
|
|
3802
|
+
const notebookRows = readGroundRows(file, "detection_notebook");
|
|
3803
|
+
const current = new Map(entityRows.map((row) => [row.uuid, row]));
|
|
3804
|
+
for (const row of notebookRows)
|
|
3805
|
+
current.set(row.uuid, row);
|
|
3806
|
+
const relocations = [];
|
|
3807
|
+
for (const parentUUID of parentUUIDs) {
|
|
3808
|
+
const parent = current.get(parentUUID);
|
|
3809
|
+
if (!parent)
|
|
3810
|
+
continue;
|
|
3811
|
+
const siblings = [...current.values()].filter((row) => row.parentUUID === parentUUID && row.status === "active");
|
|
3812
|
+
const derived = materializedSlotNames(siblings);
|
|
3813
|
+
for (const row of siblings) {
|
|
3814
|
+
const name = derived.get(row.uuid) ?? row.name;
|
|
3815
|
+
const destination = join(parent.absolutePath, name);
|
|
3816
|
+
if (destination === row.absolutePath)
|
|
3817
|
+
continue;
|
|
3818
|
+
relocations.push({
|
|
3819
|
+
row,
|
|
3820
|
+
destination,
|
|
3821
|
+
relativePath: [parent.relativePath, name].filter(Boolean).join("/"),
|
|
3822
|
+
});
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
if (relocations.length === 0)
|
|
3826
|
+
return;
|
|
3827
|
+
const database = initializeDatabase(file);
|
|
3828
|
+
try {
|
|
3829
|
+
const update = (table) => database.prepare(`UPDATE ${table} SET relative_path = ?, absolute_path = ? WHERE uuid = ?`);
|
|
3830
|
+
const updateEntity = update("entities");
|
|
3831
|
+
const updateNotebook = update("detection_notebook");
|
|
3832
|
+
const refreshEntity = database.prepare("UPDATE entities SET changed_time_ms = ? WHERE uuid = ?");
|
|
3833
|
+
const refreshNotebook = database.prepare("UPDATE detection_notebook SET changed_time_ms = ? WHERE uuid = ?");
|
|
3834
|
+
const pending = [...relocations];
|
|
3835
|
+
while (pending.length > 0) {
|
|
3836
|
+
const sources = new Set(pending.map(({ row }) => row.absolutePath));
|
|
3837
|
+
const index = pending.findIndex(({ destination }) => !sources.has(destination) && !existsSync(destination));
|
|
3838
|
+
if (index < 0)
|
|
3839
|
+
throw new Error("derived address relocations have no free destination");
|
|
3840
|
+
const relocation = pending.splice(index, 1)[0];
|
|
3841
|
+
if (!relocation)
|
|
3842
|
+
throw new Error("derived address relocation vanished");
|
|
3843
|
+
const { row, destination, relativePath: destinationRelative } = relocation;
|
|
3844
|
+
if (!existsSync(row.absolutePath))
|
|
3845
|
+
continue;
|
|
3846
|
+
const entityDescendants = entityRows.filter((candidate) => pathWithin(row.absolutePath, candidate.absolutePath));
|
|
3847
|
+
const notebookDescendants = notebookRows.filter((candidate) => pathWithin(row.absolutePath, candidate.absolutePath));
|
|
3848
|
+
database.transaction(() => {
|
|
3849
|
+
for (const candidate of entityDescendants) {
|
|
3850
|
+
updateEntity.run(destinationRelative + candidate.relativePath.slice(row.relativePath.length), destination + candidate.absolutePath.slice(row.absolutePath.length), candidate.uuid);
|
|
3851
|
+
}
|
|
3852
|
+
for (const candidate of notebookDescendants) {
|
|
3853
|
+
updateNotebook.run(destinationRelative + candidate.relativePath.slice(row.relativePath.length), destination + candidate.absolutePath.slice(row.absolutePath.length), candidate.uuid);
|
|
3854
|
+
}
|
|
3855
|
+
})();
|
|
3856
|
+
renameSync(row.absolutePath, destination);
|
|
3857
|
+
const moved = lstatSync(destination);
|
|
3858
|
+
refreshEntity.run(moved.ctimeMs, row.uuid);
|
|
3859
|
+
refreshNotebook.run(moved.ctimeMs, row.uuid);
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
finally {
|
|
3863
|
+
database.close();
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3786
3866
|
/** Journal the newest snapshot publication for its resource. A journal row
|
|
3787
3867
|
* has exactly three meanings: `pending` and never submitted — a local intent
|
|
3788
3868
|
* the authority has never seen, superseded freely (last write wins);
|
|
@@ -4877,7 +4957,11 @@ async function reconcileRoot(input) {
|
|
|
4877
4957
|
...(registeredBoundaryId ? { fixedUUID: registeredBoundaryId } : {}),
|
|
4878
4958
|
type,
|
|
4879
4959
|
parentUUID,
|
|
4880
|
-
name
|
|
4960
|
+
// The row at this exact address owns its portable name. Collision
|
|
4961
|
+
// suffixes are materialization only, so a complete walk freezes the
|
|
4962
|
+
// pre-pass address-to-identity map instead of letting an earlier
|
|
4963
|
+
// sibling deletion turn a later suffixed child into a fake rename.
|
|
4964
|
+
name: existing?.name ?? child.name,
|
|
4881
4965
|
relativePath,
|
|
4882
4966
|
absolutePath,
|
|
4883
4967
|
payloadVersion,
|
|
@@ -5465,6 +5549,23 @@ async function reconcileRoot(input) {
|
|
|
5465
5549
|
}
|
|
5466
5550
|
function portablePaths(records, boundaryUUIDs = new Set()) {
|
|
5467
5551
|
const byUuid = new Map(records.map((record) => [record.uuid, record]));
|
|
5552
|
+
const siblingsByParent = new Map();
|
|
5553
|
+
for (const record of records) {
|
|
5554
|
+
if (record.status !== "active" || record.parentUUID === null)
|
|
5555
|
+
continue;
|
|
5556
|
+
const siblings = siblingsByParent.get(record.parentUUID) ?? [];
|
|
5557
|
+
siblings.push(record);
|
|
5558
|
+
siblingsByParent.set(record.parentUUID, siblings);
|
|
5559
|
+
}
|
|
5560
|
+
const derivedNames = new Map();
|
|
5561
|
+
for (const siblings of siblingsByParent.values()) {
|
|
5562
|
+
for (const [uuid, name] of materializedSlotNames(siblings.map((record) => ({
|
|
5563
|
+
uuid: record.uuid,
|
|
5564
|
+
name: record.name,
|
|
5565
|
+
type: record.type,
|
|
5566
|
+
}))))
|
|
5567
|
+
derivedNames.set(uuid, name);
|
|
5568
|
+
}
|
|
5468
5569
|
const memo = new Map();
|
|
5469
5570
|
const resolving = new Set();
|
|
5470
5571
|
const resolve = (record) => {
|
|
@@ -5479,7 +5580,8 @@ function portablePaths(records, boundaryUUIDs = new Set()) {
|
|
|
5479
5580
|
const parent = byUuid.get(record.parentUUID);
|
|
5480
5581
|
if (!parent)
|
|
5481
5582
|
throw new Error(`cloud entity ${record.uuid} has no parent`);
|
|
5482
|
-
path = [resolve(parent), record.name]
|
|
5583
|
+
path = [resolve(parent), derivedNames.get(record.uuid) ?? record.name]
|
|
5584
|
+
.filter(Boolean).join("/");
|
|
5483
5585
|
}
|
|
5484
5586
|
resolving.delete(record.uuid);
|
|
5485
5587
|
memo.set(record.uuid, path);
|
|
@@ -5513,6 +5615,40 @@ function recordRootResolver(records) {
|
|
|
5513
5615
|
};
|
|
5514
5616
|
return rootOf;
|
|
5515
5617
|
}
|
|
5618
|
+
/** Resolve the final root of every accepted tail result. A snapshot-membership
|
|
5619
|
+
* lookup answers where an entity used to live; it cannot place a child whose
|
|
5620
|
+
* parent was itself created or moved after the snapshot watermark. The
|
|
5621
|
+
* complete accepted prefix is therefore the first ancestry source, with the
|
|
5622
|
+
* snapshot used only when that prefix leaves an ancestor unchanged. */
|
|
5623
|
+
function acceptedResultRootResolver(accepted, snapshotMembership) {
|
|
5624
|
+
const results = new Map(accepted.map((record) => [record.uuid, record]));
|
|
5625
|
+
const memo = new Map();
|
|
5626
|
+
const resolving = new Set();
|
|
5627
|
+
const rootOf = (record) => {
|
|
5628
|
+
const known = memo.get(record.uuid);
|
|
5629
|
+
if (known !== undefined)
|
|
5630
|
+
return known;
|
|
5631
|
+
if (record.parentUUID === null) {
|
|
5632
|
+
memo.set(record.uuid, record.uuid);
|
|
5633
|
+
return record.uuid;
|
|
5634
|
+
}
|
|
5635
|
+
if (resolving.has(record.uuid)) {
|
|
5636
|
+
throw new Error("accepted entity tail has a parent cycle");
|
|
5637
|
+
}
|
|
5638
|
+
resolving.add(record.uuid);
|
|
5639
|
+
const changedParent = results.get(record.parentUUID);
|
|
5640
|
+
const rootId = changedParent
|
|
5641
|
+
? rootOf(changedParent)
|
|
5642
|
+
: snapshotMembership.get(record.parentUUID);
|
|
5643
|
+
resolving.delete(record.uuid);
|
|
5644
|
+
if (rootId === undefined) {
|
|
5645
|
+
throw new Error(`accepted entity ${record.uuid} descends from no committed root`);
|
|
5646
|
+
}
|
|
5647
|
+
memo.set(record.uuid, rootId);
|
|
5648
|
+
return rootId;
|
|
5649
|
+
};
|
|
5650
|
+
return rootOf;
|
|
5651
|
+
}
|
|
5516
5652
|
/** The roots one publication rebuilds, each with its complete new records.
|
|
5517
5653
|
*
|
|
5518
5654
|
* A walked root is rebuilt from its ground alone: materialized ground is the
|
|
@@ -5542,13 +5678,17 @@ async function publicationRoots(graph, localRecords, accepted) {
|
|
|
5542
5678
|
// asks: which root holds each accepted result, and which root still files
|
|
5543
5679
|
// each record the walk re-rooted.
|
|
5544
5680
|
const located = await graph.membership(new Set([
|
|
5545
|
-
...accepted.
|
|
5681
|
+
...accepted.flatMap((result) => [
|
|
5682
|
+
result.uuid,
|
|
5683
|
+
...(result.parentUUID === null ? [] : [result.parentUUID]),
|
|
5684
|
+
]),
|
|
5546
5685
|
...localRecords.map((record) => record.uuid),
|
|
5547
5686
|
]));
|
|
5687
|
+
const acceptedRootOf = acceptedResultRootResolver(accepted, located);
|
|
5548
5688
|
const overlays = new Map();
|
|
5549
5689
|
for (const result of accepted) {
|
|
5550
|
-
const rootId =
|
|
5551
|
-
if (
|
|
5690
|
+
const rootId = acceptedRootOf(result);
|
|
5691
|
+
if (walked.has(rootId))
|
|
5552
5692
|
continue;
|
|
5553
5693
|
const overlay = overlays.get(rootId);
|
|
5554
5694
|
if (overlay)
|
|
@@ -5556,15 +5696,6 @@ async function publicationRoots(graph, localRecords, accepted) {
|
|
|
5556
5696
|
else
|
|
5557
5697
|
overlays.set(rootId, [result]);
|
|
5558
5698
|
}
|
|
5559
|
-
for (const [rootId, results] of overlays) {
|
|
5560
|
-
const byUuid = new Map((await graph.recordsFor(rootId))
|
|
5561
|
-
.map((record) => [record.uuid, record]));
|
|
5562
|
-
for (const result of results)
|
|
5563
|
-
byUuid.set(result.uuid, result);
|
|
5564
|
-
// Entity channels are independently ordered, so container membership is
|
|
5565
|
-
// derived once from the final prefix rather than by arrival order.
|
|
5566
|
-
rebuilt.set(rootId, recordsRetainingMissingEvidence([...byUuid.values()], []));
|
|
5567
|
-
}
|
|
5568
5699
|
// A records-shaped authority base carries no page identities, so it can
|
|
5569
5700
|
// prove nothing about a root the commit leaves alone: the first head pages
|
|
5570
5701
|
// the whole account at once, and never again.
|
|
@@ -5579,6 +5710,17 @@ async function publicationRoots(graph, localRecords, accepted) {
|
|
|
5579
5710
|
// such root is rebuilt without them, and a root that keeps nothing is left
|
|
5580
5711
|
// with no records at all — the rebuild then drops it from the graph.
|
|
5581
5712
|
const vacated = new Map();
|
|
5713
|
+
for (const result of accepted) {
|
|
5714
|
+
const oldRoot = located.get(result.uuid) ?? null;
|
|
5715
|
+
const newRoot = acceptedRootOf(result);
|
|
5716
|
+
if (oldRoot === null || oldRoot === newRoot || walked.has(oldRoot))
|
|
5717
|
+
continue;
|
|
5718
|
+
const gone = vacated.get(oldRoot);
|
|
5719
|
+
if (gone)
|
|
5720
|
+
gone.add(result.uuid);
|
|
5721
|
+
else
|
|
5722
|
+
vacated.set(oldRoot, new Set([result.uuid]));
|
|
5723
|
+
}
|
|
5582
5724
|
for (const record of localRecords) {
|
|
5583
5725
|
const rootId = located.get(record.uuid) ?? null;
|
|
5584
5726
|
if (rootId === null || walked.has(rootId))
|
|
@@ -5589,9 +5731,18 @@ async function publicationRoots(graph, localRecords, accepted) {
|
|
|
5589
5731
|
else
|
|
5590
5732
|
vacated.set(rootId, new Set([record.uuid]));
|
|
5591
5733
|
}
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
rebuilt.
|
|
5734
|
+
const affectedRoots = new Set([...overlays.keys(), ...vacated.keys()]);
|
|
5735
|
+
for (const rootId of affectedRoots) {
|
|
5736
|
+
const byUuid = new Map((rebuilt.get(rootId) ?? await graph.recordsFor(rootId))
|
|
5737
|
+
.map((record) => [record.uuid, record]));
|
|
5738
|
+
for (const uuid of vacated.get(rootId) ?? [])
|
|
5739
|
+
byUuid.delete(uuid);
|
|
5740
|
+
for (const result of overlays.get(rootId) ?? [])
|
|
5741
|
+
byUuid.set(result.uuid, result);
|
|
5742
|
+
// Entity channels are independently ordered. Derive ancestry and
|
|
5743
|
+
// container membership once from the complete final prefix, never from
|
|
5744
|
+
// the order in which independent entity records arrived.
|
|
5745
|
+
rebuilt.set(rootId, recordsRetainingMissingEvidence([...byUuid.values()], []));
|
|
5595
5746
|
}
|
|
5596
5747
|
return rebuilt;
|
|
5597
5748
|
}
|