@amalgm/shell 0.1.142 → 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);