@aefree/pi-plastic 0.5.7 → 0.5.8

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/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project follows semantic versioning for public package releases.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## [0.5.8] - 2026-09-15
11
+
12
+ ### Fixed
13
+
14
+ - Resolve pending-diff workspace containment and Xlink ownership by filesystem identity when a workspace is accessed through a real filesystem alias, while retaining the caller's lexical path for Plastic commands and displayed paths. Missing deleted or moved ownership ancestors still fail closed.
15
+ - Retry only npm registry E404 availability checks for a bounded period after trusted publishing; a mismatched, malformed, or unauthorized release identity still fails immediately.
16
+
10
17
  ## [0.5.7] - 2026-09-14
11
18
 
12
19
  ### Fixed
package/README.md CHANGED
@@ -75,6 +75,7 @@ The Bash guards run only in Pi processes that actually load this package. A proj
75
75
  - `plastic_branchCreate` supports an explicit parent branch independent of the loaded workspace branch, defaults relative names to the current branch when no parent is supplied, and rejects top-level paths unless `allowRootBranch=true` is explicit.
76
76
  - `plastic_diff` remains a disabled alias by design. Use `plastic_status` for changed-path listing; do not diff as routine post-edit validation or checkin preflight. When change-boundary evidence is needed, use `plastic_diffFile` for one exact file, `plastic_workspaceDiff` with selected paths or explicit `allPending=true`, or `plastic_diffRevisions` for an explicit historical pair. Diff responses use small defaults and caller-controlled `maxChars` bounds. Automatic pending-file bases bind each status revision ID to its owning workspace or Xlink repository/server, including mixed-Xlink reviews. If ownership cannot be resolved, that comparison is reported unavailable rather than retrieving a bare revision from the caller repository. Historical and workspace bytes are copied to ASCII-safe package-owned paths before the backend runs, while normalized headers retain the logical Unicode labels.
77
77
  - `plastic_status(machineReadable=true, format="json")` returns parsed `items`, aggregate `summary`, and `itemCount` (`total`, `returned`, `omitted`) rather than requiring agents to parse Plastic rows. It returns at most 100 items by default; set `maxItems` (up to 500) when more are intentional. `includeRaw=true` adds the original unbounded Plastic output for diagnostics, so omit it for normal agent use to conserve context tokens.
78
+ - Pending-diff ownership containment uses filesystem identity, so a workspace reached through a symlink, junction, or platform filesystem alias still resolves its workspace and nested Xlink bases safely. Plastic commands and returned labels retain the caller's original lexical path; missing deleted or moved ownership ancestors remain unavailable rather than falling back to a parent repository.
78
79
  - Preflight is not routine confirmation. Use it for ambiguous or broad mutation scope, moved/deleted path rewriting, compound operations, or explicit preview requests; otherwise rely on exact targets and the tools' runtime guards.
79
80
  - `plastic_patch` generates review patches with `cm patch`, including `clean` and `integration` filters for branch review workflows. It does not expose patch apply. Unqualified `br:/...` selectors are qualified only with the current workspace selector's exact repository; otherwise pass `br:/<branch>@<repository>@<server>`.
80
81
  - Bash safety rails block `cm diff` and unsafe interactive `cm merge --merge` usage.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aefree/pi-plastic",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.19.0"
@@ -406,6 +406,37 @@ const findCanonicalExistingAncestor = (absolutePath: string): string | null =>
406
406
  }
407
407
  };
408
408
 
409
+ const toFilesystemIdentityPath = async (absolutePath: string): Promise<string> =>
410
+ {
411
+ let candidate = absolutePath;
412
+ const missingSegments: string[] = [];
413
+ while (true)
414
+ {
415
+ try
416
+ {
417
+ const canonicalAncestor = (await fs.realpath(candidate)).replace(/\\/g, "/");
418
+ return missingSegments.reduce((identityPath, segment) => join(identityPath, segment), canonicalAncestor).replace(/\\/g, "/");
419
+ }
420
+ catch (error)
421
+ {
422
+ const code = (error as NodeJS.ErrnoException).code;
423
+ if (code !== "ENOENT" && code !== "ENOTDIR")
424
+ {
425
+ throw error;
426
+ }
427
+
428
+ const parent = dirname(candidate);
429
+ if (parent === candidate)
430
+ {
431
+ throw error;
432
+ }
433
+
434
+ missingSegments.unshift(basename(candidate));
435
+ candidate = parent;
436
+ }
437
+ }
438
+ };
439
+
409
440
  const toggleFirstAsciiLetterCase = (value: string): string | null =>
410
441
  {
411
442
  const index = value.search(/[A-Za-z]/);
@@ -1814,9 +1845,10 @@ const createPendingBaseIdentityResolver = (cwd: string, workdir?: string): Pendi
1814
1845
  });
1815
1846
  return workspaceRepository;
1816
1847
  };
1817
- const getXlinkRepository = (candidate: string): Promise<RepositoryIdentity | null> =>
1848
+ const getXlinkRepository = async (candidate: string): Promise<RepositoryIdentity | null> =>
1818
1849
  {
1819
- const key = toPathComparisonKeyFromAbsolutePath(candidate);
1850
+ // Use the physical identity for cache keys, but keep candidate lexical for cm.
1851
+ const key = toPathComparisonKeyFromAbsolutePath(await toFilesystemIdentityPath(candidate));
1820
1852
  let lookup = xlinkLookups.get(key);
1821
1853
  if (!lookup)
1822
1854
  {
@@ -1851,15 +1883,22 @@ const createPendingBaseIdentityResolver = (cwd: string, workdir?: string): Pendi
1851
1883
  throw new Error(`Plastic status did not provide a base revision ID for '${item.workspacePath}'.`);
1852
1884
  }
1853
1885
  const root = await getWorkspaceRoot();
1886
+ const rootIdentity = await toFilesystemIdentityPath(root);
1854
1887
  const ownershipPath = item.kind === "moved" && item.sourceWorkspacePath
1855
1888
  ? toNormalizedAbsolutePath(item.sourceWorkspacePath, cwd)
1856
1889
  : item.normalizedPath;
1857
- if (!isWithinPathScope(toPathComparisonKeyFromAbsolutePath(ownershipPath), toPathComparisonKeyFromAbsolutePath(root)))
1890
+ if (!isWithinPathScope(
1891
+ toPathComparisonKeyFromAbsolutePath(await toFilesystemIdentityPath(ownershipPath)),
1892
+ toPathComparisonKeyFromAbsolutePath(rootIdentity),
1893
+ ))
1858
1894
  {
1859
1895
  throw new Error(`Plastic cannot resolve an owning repository for '${item.workspacePath}' outside the requested workspace.`);
1860
1896
  }
1861
1897
  let candidate = dirname(ownershipPath);
1862
- while (isWithinPathScope(toPathComparisonKeyFromAbsolutePath(candidate), toPathComparisonKeyFromAbsolutePath(root)))
1898
+ while (isWithinPathScope(
1899
+ toPathComparisonKeyFromAbsolutePath(await toFilesystemIdentityPath(candidate)),
1900
+ toPathComparisonKeyFromAbsolutePath(rootIdentity),
1901
+ ))
1863
1902
  {
1864
1903
  // A missing ancestor can be a removed Xlink mount. Status does not
1865
1904
  // retain ownership, so parent-repository fallback would be unsafe.
@@ -1873,7 +1912,8 @@ const createPendingBaseIdentityResolver = (cwd: string, workdir?: string): Pendi
1873
1912
  item.baseRepository = `${repository.repository}@${repository.server}`;
1874
1913
  return `revid:${item.revisionId}@rep:${repository.repository}@repserver:${repository.server}`;
1875
1914
  }
1876
- if (candidate === root)
1915
+ if (toPathComparisonKeyFromAbsolutePath(await toFilesystemIdentityPath(candidate))
1916
+ === toPathComparisonKeyFromAbsolutePath(rootIdentity))
1877
1917
  {
1878
1918
  break;
1879
1919
  }