@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.
@@ -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: child.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].filter(Boolean).join("/");
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.map((result) => result.uuid),
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 = located.get(result.uuid) ?? null;
5551
- if (rootId === null || walked.has(rootId))
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
- for (const [rootId, gone] of vacated) {
5593
- const held = rebuilt.get(rootId) ?? await graph.recordsFor(rootId);
5594
- rebuilt.set(rootId, held.filter((record) => !gone.has(record.uuid)));
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
  }