@latysh-development/monolith 0.1.0-alpha.3 → 0.1.0-alpha.4

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.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/dist/brief/cli.d.ts.map +1 -1
  3. package/dist/brief/cli.js +8 -2
  4. package/dist/brief/collect.d.ts.map +1 -1
  5. package/dist/brief/collect.js +11 -3
  6. package/dist/brief/freshness.d.ts +17 -2
  7. package/dist/brief/freshness.d.ts.map +1 -1
  8. package/dist/brief/freshness.js +22 -4
  9. package/dist/brief/git-signals.d.ts +32 -5
  10. package/dist/brief/git-signals.d.ts.map +1 -1
  11. package/dist/brief/git-signals.js +169 -39
  12. package/dist/brief/knowledge.d.ts +62 -0
  13. package/dist/brief/knowledge.d.ts.map +1 -0
  14. package/dist/brief/knowledge.js +123 -0
  15. package/dist/brief/reconcile.d.ts +7 -1
  16. package/dist/brief/reconcile.d.ts.map +1 -1
  17. package/dist/brief/reconcile.js +42 -2
  18. package/dist/cli/index.js +1 -1
  19. package/dist/cli/style.d.ts +26 -0
  20. package/dist/cli/style.d.ts.map +1 -0
  21. package/dist/cli/style.js +76 -0
  22. package/dist/cli/ui.d.ts.map +1 -1
  23. package/dist/cli/ui.js +33 -6
  24. package/dist/coordinatorium/cli.d.ts.map +1 -1
  25. package/dist/coordinatorium/cli.js +13 -0
  26. package/dist/finalize/cli.d.ts.map +1 -1
  27. package/dist/finalize/cli.js +30 -13
  28. package/dist/finalize/consistency.d.ts +51 -0
  29. package/dist/finalize/consistency.d.ts.map +1 -0
  30. package/dist/finalize/consistency.js +57 -0
  31. package/dist/finalize/core.d.ts +26 -0
  32. package/dist/finalize/core.d.ts.map +1 -1
  33. package/dist/finalize/core.js +140 -1
  34. package/dist/ideas/cli.d.ts +1 -0
  35. package/dist/ideas/cli.d.ts.map +1 -1
  36. package/dist/ideas/cli.js +14 -3
  37. package/dist/ideas/format.d.ts +31 -1
  38. package/dist/ideas/format.d.ts.map +1 -1
  39. package/dist/ideas/format.js +109 -1
  40. package/dist/ideas/store.d.ts +2 -0
  41. package/dist/ideas/store.d.ts.map +1 -1
  42. package/dist/ideas/store.js +4 -1
  43. package/dist/ideas/types.d.ts +10 -0
  44. package/dist/ideas/types.d.ts.map +1 -1
  45. package/dist/ideas/types.js +3 -0
  46. package/dist/readmodel/dashboard.d.ts.map +1 -1
  47. package/dist/readmodel/dashboard.js +19 -1
  48. package/dist/readmodel/index.d.ts +2 -1
  49. package/dist/readmodel/index.d.ts.map +1 -1
  50. package/dist/readmodel/index.js +1 -0
  51. package/dist/readmodel/roadmap.d.ts +34 -0
  52. package/dist/readmodel/roadmap.d.ts.map +1 -0
  53. package/dist/readmodel/roadmap.js +254 -0
  54. package/dist/readmodel/types.d.ts +88 -0
  55. package/dist/readmodel/types.d.ts.map +1 -1
  56. package/dist/root/safe-write.d.ts +15 -4
  57. package/dist/root/safe-write.d.ts.map +1 -1
  58. package/dist/root/safe-write.js +264 -50
  59. package/dist/ui/assets/index-BJKxUDPD.css +1 -0
  60. package/dist/ui/assets/index-ioC9DPHQ.js +28 -0
  61. package/dist/ui/index.html +2 -2
  62. package/dist/validate/index.d.ts.map +1 -1
  63. package/dist/validate/index.js +4 -3
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/package.json +5 -2
  67. package/dist/ui/assets/index-5zXAgig8.css +0 -1
  68. package/dist/ui/assets/index-CQgoMYLM.js +0 -28
@@ -18,7 +18,11 @@
18
18
  * temp name, never from an error code. UNKNOWN is never retried and never
19
19
  * silently downgraded;
20
20
  * - identity-checked cleanup: only a temp file THIS call created, and only if
21
- * the name still shows the `dev/ino` recorded from the descriptor.
21
+ * the name still shows the inode of a separate read-only IDENTITY descriptor,
22
+ * which stays open until publication and cleanup are finished (an open
23
+ * descriptor pins the inode, so it cannot be recycled for a foreign file: no
24
+ * ABA). The WRITING descriptor is closed before publication (ADR-012). The
25
+ * `lstat` -> `unlink` step itself remains non-atomic (residual race, ADR-012).
22
26
  *
23
27
  * Not routed through here: lock release and stale-lock recovery (`lock.ts`)
24
28
  * unlink the lock path directly, and `trusted-directory` scope creates its
@@ -484,13 +488,25 @@ function safeClose(ctx, fd) {
484
488
  }
485
489
  }
486
490
  /**
487
- * Remove `token`'s file iff the name still shows the recorded identity. Never
488
- * removes anything else. The unlink itself is not conditional (no `unlinkat`):
489
- * a name swapped between the check and the unlink is a residual race.
491
+ * Does `st` (an `lstat` of the temp name) show the file this call created?
492
+ * Throws if the proof itself cannot be obtained (fstat failed, or the
493
+ * descriptor no longer shows the recorded identity): callers treat a throw as
494
+ * "ownership NOT shown".
490
495
  */
491
- export function disposeOwned(parent, token, opts = {}) {
492
- const ctx = ctxOf(opts);
493
- const path = join(pdata(parent).path, token.name);
496
+ function showsOwned(ctx, held, st) {
497
+ if (!st.isFile())
498
+ return false;
499
+ if (held.fd === null)
500
+ return sameId(st, held.token.dev, held.token.ino);
501
+ if (held.closed)
502
+ throw new Error('descriptor already released; ownership cannot be proven');
503
+ const live = ctx.ops.fstatSync(held.fd, { bigint: true });
504
+ if (!live.isFile() || !sameId(live, held.token.dev, held.token.ino))
505
+ throw new Error('descriptor identity changed');
506
+ return sameId(st, live.dev, live.ino);
507
+ }
508
+ function disposeHeld(parent, held, ctx) {
509
+ const path = join(pdata(parent).path, held.token.name);
494
510
  let st;
495
511
  try {
496
512
  st = bigLstatOrNull(ctx, path);
@@ -500,7 +516,14 @@ export function disposeOwned(parent, token, opts = {}) {
500
516
  }
501
517
  if (st === null)
502
518
  return 'absent';
503
- if (!st.isFile() || !sameId(st, token.dev, token.ino))
519
+ let ours;
520
+ try {
521
+ ours = showsOwned(ctx, held, st);
522
+ }
523
+ catch {
524
+ return 'failed'; // ownership could not be established: do NOT unlink
525
+ }
526
+ if (!ours)
504
527
  return 'not-owned';
505
528
  try {
506
529
  ctx.ops.unlinkSync(path);
@@ -510,6 +533,42 @@ export function disposeOwned(parent, token, opts = {}) {
510
533
  return errnoOf(err) === 'ENOENT' ? 'absent' : 'failed';
511
534
  }
512
535
  }
536
+ /**
537
+ * Remove `token`'s file iff the name still shows the recorded identity. Never
538
+ * removes anything else. This token-only form has no live descriptor and so
539
+ * cannot exclude inode reuse after the caller closed its own; the writes in
540
+ * this module use `disposeHeld`. The unlink itself is not conditional (no
541
+ * `unlinkat`): a name swapped between the check and the unlink is a residual race.
542
+ */
543
+ export function disposeOwned(parent, token, opts = {}) {
544
+ return disposeHeld(parent, { token, fd: null, closed: true }, ctxOf(opts));
545
+ }
546
+ /** Close the held descriptor exactly once. Never throws; returns the close error, if any. */
547
+ function releaseHeld(ctx, held) {
548
+ if (held.fd === null || held.closed)
549
+ return undefined;
550
+ held.closed = true;
551
+ try {
552
+ ctx.ops.closeSync(held.fd);
553
+ }
554
+ catch (err) {
555
+ return err;
556
+ }
557
+ return undefined;
558
+ }
559
+ /** Keep the original failure; mention a failed release of the descriptor. */
560
+ function withCloseNote(err, closeErr) {
561
+ if (closeErr === undefined || !(err instanceof SafeWriteError))
562
+ return err;
563
+ return new SafeWriteError(`${err.message} The identity descriptor also failed to close: ${errMsg(closeErr)}.`, {
564
+ code: err.code,
565
+ phase: err.phase,
566
+ publication: err.publication,
567
+ path: err.path,
568
+ tmp: err.tmp,
569
+ cause: err.cause,
570
+ });
571
+ }
513
572
  function tmpInfo(res, name) {
514
573
  switch (res) {
515
574
  case 'removed':
@@ -587,22 +646,70 @@ function inspectTarget(parent, name, ctx, mode) {
587
646
  return { existing: st };
588
647
  }
589
648
  /**
590
- * Steps 1-5b of the durability order: create, write, fchmod, verify the
591
- * descriptor, fsync the file, close. Any failure leaves the destination
592
- * untouched and removes the temp file only if ownership still verifies.
649
+ * Flags of the identity descriptor: `O_RDONLY|O_NOFOLLOW|O_NONBLOCK`. Without
650
+ * `O_NOFOLLOW` (Windows, best-effort per ADR-012) the flag is simply absent and
651
+ * `stage` compensates with an `lstat` check; that is NOT equivalent protection.
652
+ * `O_NONBLOCK` keeps a swapped-in FIFO from blocking the open.
653
+ */
654
+ function identityFlags(ctx) {
655
+ const c = fs.constants;
656
+ const nonBlock = typeof c.O_NONBLOCK === 'number' ? c.O_NONBLOCK : 0;
657
+ // On POSIX a missing O_NONBLOCK would allow a blocking open of a swapped FIFO: refuse.
658
+ if (nonBlock === 0 && ctx.caps.posix)
659
+ return null;
660
+ return c.O_RDONLY | (ctx.caps.noFollow ? c.O_NOFOLLOW : 0) | nonBlock;
661
+ }
662
+ /**
663
+ * Steps of the durability order (ADR-012) with TWO descriptors:
664
+ *
665
+ * - WRITING fd (from `createExclusive`): write, fchmod, verify, fsync, then
666
+ * CLOSED before publication (a deferred write error surfaces at `close` and
667
+ * must make the write NOT_PUBLISHED);
668
+ * - IDENTITY fd (separate read-only `open`, not `dup`): opened right after the
669
+ * create while the writing fd is open, proven to be the same inode, and
670
+ * returned STILL OPEN. Its owner from here on is the caller, which releases
671
+ * it with `releaseHeld` after publication and cleanup. It pins the inode
672
+ * (no ABA) for verification and cleanup.
673
+ *
674
+ * Any failure leaves the destination untouched, removes the temp file only if
675
+ * ownership still verifies against a live descriptor, and closes every
676
+ * descriptor still owned here exactly once (`releaseHeld` marks before closing,
677
+ * so a failed close is never repeated).
593
678
  */
594
679
  function stage(parent, finalName, data, finalMode, createMode, fsyncFile, ctx) {
595
680
  const dir = pdata(parent).path;
596
681
  const owned = createExclusive(parent, tempName(finalName), createMode, { ops: ctx.ops, caps: ctx.caps });
597
682
  const { fd, token } = owned;
598
683
  const tmpPath = join(dir, token.name);
599
- let closed = false;
684
+ const writing = { token, fd, closed: false };
685
+ let identity = null;
686
+ let identityProven = false; // until proven, only the writing descriptor vouches for ownership
687
+ // Owner-read elevation (ALPHA4-SEC-SAFEWRITE-008): the mode the file really had
688
+ // after creation, set ONLY while `0o400` was temporarily added so the read-only
689
+ // identity descriptor can be opened. `null` = no elevation outstanding.
690
+ let restoreMode = null;
600
691
  const abort = (code, phase, what, cause) => {
601
- if (!closed)
602
- safeClose(ctx, fd);
603
- closed = true;
604
- const info = tmpInfo(disposeOwned(parent, token, { ops: ctx.ops, caps: ctx.caps }), token.name);
605
- throw new SafeWriteError(`${what}: ${errMsg(cause)}.${residueNote(info, dir)}`, {
692
+ // Put the mode back first, while the writing descriptor is still open. A
693
+ // failure here never replaces the original error; it is reported in the message.
694
+ let modeNote = '';
695
+ let modeRestoreFailed = false;
696
+ if (restoreMode !== null && !writing.closed) {
697
+ try {
698
+ ctx.ops.fchmodSync(fd, restoreMode);
699
+ restoreMode = null;
700
+ }
701
+ catch (restoreErr) {
702
+ modeRestoreFailed = true;
703
+ modeNote = ` The original mode of the temporary file could not be restored after the temporary owner-read elevation: ${errMsg(restoreErr)}.`;
704
+ }
705
+ }
706
+ // Cleanup next, while a live descriptor still proves what the name must show.
707
+ const info = tmpInfo(disposeHeld(parent, identityProven && identity ? identity : writing, ctx), token.name);
708
+ if (modeRestoreFailed && info.owned && !info.removed)
709
+ modeNote += ' The temporary file was left with the owner-read permission added.';
710
+ const closeErrs = [releaseHeld(ctx, writing), identity ? releaseHeld(ctx, identity) : undefined].filter((e) => e !== undefined);
711
+ const note = closeErrs.length === 0 ? '' : ` A descriptor also failed to close: ${closeErrs.map(errMsg).join('; ')}.`;
712
+ throw new SafeWriteError(`${what}: ${errMsg(cause)}.${residueNote(info, dir)}${modeNote}${note}`, {
606
713
  code,
607
714
  phase,
608
715
  publication: 'NOT_PUBLISHED',
@@ -611,6 +718,55 @@ function stage(parent, finalName, data, finalMode, createMode, fsyncFile, ctx) {
611
718
  cause,
612
719
  });
613
720
  };
721
+ // Owner-read elevation: the identity descriptor is read-only, so a staging
722
+ // file created without owner-read (e.g. `exactMode:false` with 0o200/0o000)
723
+ // could not be opened. Only the mode bit is added, only on the file THIS call
724
+ // created, and only when `0o400` is missing; the mode is restored at the
725
+ // `fchmod` step below, before fsync and close.
726
+ let createdMode;
727
+ try {
728
+ const st = ctx.ops.fstatSync(fd, { bigint: true });
729
+ if (!st.isFile())
730
+ throw new Error('not a regular file');
731
+ createdMode = Number(st.mode) & 0o7777;
732
+ }
733
+ catch (err) {
734
+ return abort('WRITE_VERIFY_FAILED', 'identity', `cannot inspect the mode of "${tmpPath}"`, err);
735
+ }
736
+ if ((createdMode & 0o400) === 0) {
737
+ try {
738
+ ctx.ops.fchmodSync(fd, createdMode | 0o400);
739
+ }
740
+ catch (err) {
741
+ return abort(codeForErrno(err), 'chmod', `cannot add owner-read to "${tmpPath}" for the identity descriptor`, err);
742
+ }
743
+ restoreMode = createdMode;
744
+ }
745
+ // Identity descriptor: opened before fchmod, while the writing one is open.
746
+ let idFd;
747
+ try {
748
+ const flags = identityFlags(ctx);
749
+ if (flags === null)
750
+ throw new Error('O_NONBLOCK is unavailable, so a swapped-in FIFO could block the open');
751
+ idFd = ctx.ops.openSync(tmpPath, flags);
752
+ identity = { token, fd: idFd, closed: false };
753
+ const w = ctx.ops.fstatSync(fd, { bigint: true });
754
+ const i = ctx.ops.fstatSync(idFd, { bigint: true });
755
+ if (!w.isFile() || !i.isFile())
756
+ throw new Error('not a regular file');
757
+ if (!sameId(i, w.dev, w.ino) || !sameId(i, token.dev, token.ino))
758
+ throw new Error('identity descriptor shows a different file');
759
+ if (!ctx.caps.noFollow) {
760
+ // No O_NOFOLLOW: the name must still be the very file both descriptors show.
761
+ const at = bigLstat(ctx, tmpPath);
762
+ if (at.isSymbolicLink() || !sameId(at, w.dev, w.ino))
763
+ throw new Error('path no longer names the created file');
764
+ }
765
+ identityProven = true;
766
+ }
767
+ catch (err) {
768
+ return abort('WRITE_VERIFY_FAILED', 'identity', `cannot establish the identity descriptor of "${tmpPath}"`, err);
769
+ }
614
770
  try {
615
771
  let off = 0;
616
772
  while (off < data.length) {
@@ -623,9 +779,13 @@ function stage(parent, finalName, data, finalMode, createMode, fsyncFile, ctx) {
623
779
  catch (err) {
624
780
  return abort(codeForErrno(err), 'write', `cannot write "${tmpPath}"`, err);
625
781
  }
626
- if (finalMode !== null) {
782
+ // The final mode, or (after an elevation) the original mode. Without either
783
+ // there is no `fchmod` at all: ordinary modes are never touched.
784
+ const targetMode = finalMode ?? restoreMode;
785
+ if (targetMode !== null) {
627
786
  try {
628
- ctx.ops.fchmodSync(fd, finalMode);
787
+ ctx.ops.fchmodSync(fd, targetMode);
788
+ restoreMode = null;
629
789
  }
630
790
  catch (err) {
631
791
  return abort(codeForErrno(err), 'chmod', `cannot set the mode of "${tmpPath}"`, err);
@@ -641,7 +801,7 @@ function stage(parent, finalName, data, finalMode, createMode, fsyncFile, ctx) {
641
801
  throw new Error(`size is ${st.size}, expected ${data.length}`);
642
802
  if (!sameId(st, token.dev, token.ino))
643
803
  throw new Error('identity changed');
644
- if (finalMode !== null && (Number(st.mode) & 0o777) !== finalMode)
804
+ if (targetMode !== null && (Number(st.mode) & 0o777) !== (finalMode !== null ? finalMode : targetMode & 0o777))
645
805
  throw new Error('mode does not match');
646
806
  if (ctx.caps.ownerCheck && Number(st.uid) !== process.geteuid())
647
807
  throw new Error('not owned by the effective user');
@@ -658,17 +818,35 @@ function stage(parent, finalName, data, finalMode, createMode, fsyncFile, ctx) {
658
818
  return abort('WRITE_FSYNC_FAILED', 'fsync', `cannot fsync "${tmpPath}"`, err);
659
819
  }
660
820
  }
821
+ // The writing descriptor is closed BEFORE publication (ADR-012): a close error
822
+ // forbids publishing. Whatever close reports, the fd number is spent and is
823
+ // never closed again.
824
+ const closeErr = releaseHeld(ctx, writing);
825
+ if (closeErr !== undefined) {
826
+ return abort(codeForErrno(closeErr), 'close', `cannot close "${tmpPath}"`, closeErr);
827
+ }
828
+ // The identity descriptor must still show the same, complete file.
661
829
  try {
662
- closed = true;
663
- ctx.ops.closeSync(fd);
830
+ const st = ctx.ops.fstatSync(idFd, { bigint: true });
831
+ if (!st.isFile() || !sameId(st, token.dev, token.ino))
832
+ throw new Error('identity changed after close');
833
+ if (st.nlink !== 1n)
834
+ throw new Error(`link count is ${st.nlink}`);
835
+ if (st.size !== BigInt(data.length))
836
+ throw new Error(`size is ${st.size}, expected ${data.length}`);
664
837
  }
665
838
  catch (err) {
666
- return abort(codeForErrno(err), 'close', `cannot close "${tmpPath}"`, err);
839
+ return abort('WRITE_VERIFY_FAILED', 'verify', `temporary file "${tmpPath}" failed verification after close`, err);
667
840
  }
668
- return { token, tmpPath };
841
+ return { token, fd: idFd, closed: false, tmpPath };
669
842
  }
670
- function isOurs(st, staged) {
671
- return st !== null && st.isFile() && sameId(st, staged.token.dev, staged.token.ino);
843
+ /**
844
+ * Is `st` the file this call created? Decided against the LIVE descriptor
845
+ * (`fstat(fd)`), not a remembered `dev/ino`. A throw means the proof could not
846
+ * be obtained and must be read as "unknown" by the caller.
847
+ */
848
+ function isOurs(st, staged, ctx) {
849
+ return st !== null && showsOwned(ctx, staged, st);
672
850
  }
673
851
  /**
674
852
  * One read-only look at the two names after a publish call failed. NO errno is
@@ -691,10 +869,10 @@ function isOurs(st, staged) {
691
869
  function verifyPublication(kind, errno, targetPath, staged, ctx) {
692
870
  try {
693
871
  const t = bigLstatOrNull(ctx, targetPath);
694
- if (isOurs(t, staged))
872
+ if (isOurs(t, staged, ctx))
695
873
  return 'published';
696
874
  const m = bigLstatOrNull(ctx, staged.tmpPath);
697
- if (isOurs(m, staged)) {
875
+ if (isOurs(m, staged, ctx)) {
698
876
  if (kind === 'link' && errno === 'EEXIST') {
699
877
  // Held by another object, or it vanished again while our temp name was
700
878
  // never linked (link count 1): either way nothing of ours was published.
@@ -714,7 +892,7 @@ function publishFailure(kind, err, parent, staged, targetPath, ctx, warnings) {
714
892
  const dir = pdata(parent).path;
715
893
  const errno = errnoOf(err);
716
894
  const notPublished = (code, what) => {
717
- const info = tmpInfo(disposeOwned(parent, staged.token, { ops: ctx.ops, caps: ctx.caps }), staged.token.name);
895
+ const info = tmpInfo(disposeHeld(parent, staged, ctx), staged.token.name);
718
896
  throw new SafeWriteError(`${what}: ${errMsg(err)}. The destination was not modified.${residueNote(info, dir)}`, {
719
897
  code,
720
898
  phase: 'publish',
@@ -763,7 +941,8 @@ export function publishLink(parent, token, finalName, opts = {}) {
763
941
  const ctx = ctxOf(opts);
764
942
  validName(finalName, parent);
765
943
  const dir = pdata(parent).path;
766
- const staged = { token, tmpPath: join(dir, token.name) };
944
+ // The caller already closed its descriptor: token-only (legacy) identity, see `Held`.
945
+ const staged = { token, fd: null, closed: true, tmpPath: join(dir, token.name) };
767
946
  const targetPath = join(dir, finalName);
768
947
  const warnings = [];
769
948
  assertParentUnchanged(parent, ctx);
@@ -858,22 +1037,46 @@ export function replaceFile(parent, name, data, opts) {
858
1037
  finalMode = p.finalExact ? opts.mode : null;
859
1038
  const warnings = platformWarnings(ctx);
860
1039
  const staged = stage(parent, name, buf, finalMode, p.finalExact || existing ? 0o600 : opts.mode, p.fsyncFile, ctx);
861
- assertParentUnchangedOrDispose(parent, staged, ctx, targetPath);
1040
+ // The staging descriptor stays open through publication and cleanup and is
1041
+ // closed exactly once, whatever happens (a failure keeps its own error).
862
1042
  try {
863
- ctx.ops.renameSync(staged.tmpPath, targetPath);
1043
+ assertParentUnchangedOrDispose(parent, staged, ctx, targetPath);
1044
+ try {
1045
+ ctx.ops.renameSync(staged.tmpPath, targetPath);
1046
+ }
1047
+ catch (err) {
1048
+ publishFailure('rename', err, parent, staged, targetPath, ctx, warnings);
1049
+ }
864
1050
  }
865
1051
  catch (err) {
866
- publishFailure('rename', err, parent, staged, targetPath, ctx, warnings);
1052
+ throw withCloseNote(err, releaseHeld(ctx, staged));
867
1053
  }
1054
+ noteIdentityCloseFailure(releaseHeld(ctx, staged), warnings, staged.tmpPath);
868
1055
  const dirReached = p.fsyncDir ? syncDirectory(parent, ctx, warnings) : 'file';
869
1056
  return { publication: 'PUBLISHED', durability: reached(p.requested, dirReached), warnings };
870
1057
  }
1058
+ /**
1059
+ * The write is already published: a failed release of the IDENTITY descriptor
1060
+ * never changes the result. It is reported as `STAGING_DESCRIPTOR_CLOSE_FAILED`
1061
+ * (appended to the existing warnings), NOT as `STAGING_RESIDUE`, which means "a
1062
+ * temporary file is left behind". The close is never retried. Only the errno
1063
+ * code is reported, not the raw message.
1064
+ */
1065
+ function noteIdentityCloseFailure(closeErr, warnings, tmpPath) {
1066
+ if (closeErr === undefined)
1067
+ return;
1068
+ warnings.push({
1069
+ code: 'STAGING_DESCRIPTOR_CLOSE_FAILED',
1070
+ message: `the staging descriptor failed to close after publication (${errnoOf(closeErr) ?? 'unknown error'}); the write is published and unaffected.`,
1071
+ path: tmpPath,
1072
+ });
1073
+ }
871
1074
  function assertParentUnchangedOrDispose(parent, staged, ctx, targetPath) {
872
1075
  try {
873
1076
  assertParentUnchanged(parent, ctx);
874
1077
  }
875
1078
  catch (err) {
876
- const info = tmpInfo(disposeOwned(parent, staged.token, { ops: ctx.ops, caps: ctx.caps }), staged.token.name);
1079
+ const info = tmpInfo(disposeHeld(parent, staged, ctx), staged.token.name);
877
1080
  if (err instanceof SafeWriteError) {
878
1081
  throw new SafeWriteError(`${err.message}${residueNote(info, pdata(parent).path)}`, {
879
1082
  code: err.code,
@@ -906,26 +1109,37 @@ export function createFile(parent, name, data, opts) {
906
1109
  inspectTarget(parent, name, ctx, 'create');
907
1110
  const warnings = platformWarnings(ctx);
908
1111
  const staged = stage(parent, name, buf, p.finalExact ? opts.mode : null, p.finalExact ? 0o600 : opts.mode, p.fsyncFile, ctx);
909
- assertParentUnchangedOrDispose(parent, staged, ctx, targetPath);
910
- let outcome;
1112
+ // One owner for the staging descriptor: it is held until the second name has
1113
+ // been disposed (the identity check needs it) and closed exactly once.
1114
+ let removed;
911
1115
  try {
912
- ctx.ops.linkSync(staged.tmpPath, targetPath);
913
- outcome = 'published';
1116
+ assertParentUnchangedOrDispose(parent, staged, ctx, targetPath);
1117
+ let outcome;
1118
+ try {
1119
+ ctx.ops.linkSync(staged.tmpPath, targetPath);
1120
+ outcome = 'published';
1121
+ }
1122
+ catch (err) {
1123
+ outcome = publishFailure('link', err, parent, staged, targetPath, ctx, warnings);
1124
+ }
1125
+ if (outcome === 'lost-race') {
1126
+ const info = tmpInfo(disposeHeld(parent, staged, ctx), staged.token.name);
1127
+ throw new SafeWriteError(`"${targetPath}" appeared while it was being created; it is never overwritten.${residueNote(info, dir)}`, {
1128
+ code: 'WRITE_TARGET_EXISTS',
1129
+ phase: 'publish',
1130
+ publication: 'NOT_PUBLISHED',
1131
+ path: targetPath,
1132
+ tmp: info,
1133
+ });
1134
+ }
1135
+ // Published: the temp name is now a second link (nlink 2) to OUR inode; the
1136
+ // live descriptor still identifies it, so only that name is removed.
1137
+ removed = disposeHeld(parent, staged, ctx);
914
1138
  }
915
1139
  catch (err) {
916
- outcome = publishFailure('link', err, parent, staged, targetPath, ctx, warnings);
917
- }
918
- if (outcome === 'lost-race') {
919
- const info = tmpInfo(disposeOwned(parent, staged.token, { ops: ctx.ops, caps: ctx.caps }), staged.token.name);
920
- throw new SafeWriteError(`"${targetPath}" appeared while it was being created; it is never overwritten.${residueNote(info, dir)}`, {
921
- code: 'WRITE_TARGET_EXISTS',
922
- phase: 'publish',
923
- publication: 'NOT_PUBLISHED',
924
- path: targetPath,
925
- tmp: info,
926
- });
1140
+ throw withCloseNote(err, releaseHeld(ctx, staged));
927
1141
  }
928
- const removed = disposeOwned(parent, staged.token, { ops: ctx.ops, caps: ctx.caps });
1142
+ noteIdentityCloseFailure(releaseHeld(ctx, staged), warnings, staged.tmpPath);
929
1143
  if (removed !== 'removed' && removed !== 'absent') {
930
1144
  warnings.push({
931
1145
  code: 'STAGING_RESIDUE',
@@ -0,0 +1 @@
1
+ .skin-switcher[data-v-570e4117]{color:var(--muted);font-size:var(--fs-secondary);display:flex;flex-direction:column;gap:4px}.skin-switcher select[data-v-570e4117]{background:var(--surface-2);color:var(--text);border:1px solid var(--control-border);border-radius:4px;padding:4px 6px;font:inherit}:root{--bg: #0e1116;--surface: #161b22;--surface-2: #1d242d;--border: #2b3440;--control-border: #627185;--text: #e6edf3;--muted: #a3afc0;--accent: #58a6ff;--ok: #3fb950;--warn: #d29922;--err: #f85149;--demo: #f0b429;color-scheme:dark;--fs-body: 16px;--fs-nav: 15px;--fs-project: 18px;--fs-h2: 16px;--fs-h3: 15px;--fs-secondary: 14px;--fs-meta: 13px;--fs-label: 12px;--fs-mono: 14px;--lh-body: 1.55;--lh-heading: 1.3}*{box-sizing:border-box}html{scrollbar-gutter:stable}body{margin:0;background:var(--bg);color:var(--text);font:var(--fs-body)/var(--lh-body) system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}code,pre{font-family:ui-monospace,SF Mono,Menlo,Consolas,monospace;font-size:var(--fs-mono);line-height:1.55}.shell{display:flex;align-items:flex-start;min-height:100vh}.shell-main{flex:1 1 auto;min-width:0}.sidebar{position:sticky;top:0;align-self:flex-start;flex:0 0 240px;width:240px;height:100vh;display:flex;flex-direction:column;background:var(--surface);border-right:1px solid var(--border)}.sidebar-head{padding:14px 16px}.sidebar-panel{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:16px;padding:4px 12px 16px;overflow-y:auto}.sidebar-panel .skin-switcher{padding:0 4px 12px;border-bottom:1px solid var(--border)}.nav-toggle{display:none}.topbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px 24px;padding:10px 20px;min-height:52px;background:var(--surface);border-bottom:1px solid var(--border)}.brand{display:flex;gap:10px;align-items:center;min-width:0}.brand-logo{flex:none;width:40px;height:40px;display:block}.brand-text{display:flex;flex-direction:column;min-width:0;line-height:1.25}.brand .muted{white-space:nowrap;font-size:var(--fs-meta)}.brand strong{letter-spacing:.12em;font-size:var(--fs-h2)}.project{font-weight:600;font-size:var(--fs-project);line-height:var(--lh-heading);min-width:0;overflow-wrap:anywhere}.topbar-right{margin-left:auto;display:flex;align-items:center;gap:14px}.scenario{color:var(--muted);font-size:var(--fs-secondary);display:flex;gap:6px;align-items:center}.scenario select{background:var(--surface-2);color:var(--text);border:1px solid var(--control-border);border-radius:4px;padding:4px 8px;font:inherit;min-height:32px}.demo-badge{background:var(--demo);color:#1a1300;font-weight:800;letter-spacing:.1em;padding:4px 12px;border-radius:4px}.nav{display:flex;flex-direction:column;gap:2px}.nav a{color:var(--muted);text-decoration:none;overflow-wrap:anywhere;padding:8px 12px;border-radius:4px;font-size:var(--fs-nav);line-height:1.35}.nav a:hover{color:var(--text);background:var(--surface-2)}.nav a[aria-current=location]{color:var(--text);background:var(--surface-2);font-weight:600;box-shadow:inset 3px 0 0 var(--accent)}.nav a:focus-visible,button:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.content{max-width:1100px;margin:0 auto;padding:20px;display:grid;grid-template-columns:minmax(0,1fr);gap:16px}.panel{min-width:0;overflow-wrap:anywhere;background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:16px 20px;scroll-margin-top:16px}.panel h2{margin:0 0 12px;font-size:var(--fs-h2);line-height:var(--lh-heading);text-transform:uppercase;letter-spacing:.06em;color:var(--accent)}.panel h3{margin:16px 0 6px;font-size:var(--fs-h3);line-height:var(--lh-heading)}.panel h4{margin:10px 0 4px;font-size:var(--fs-secondary);line-height:var(--lh-heading);color:var(--muted)}.facts{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:4px 16px;margin:0}.facts dt{color:var(--muted)}.facts dt,.facts dd{min-width:0}.facts dd{margin:0;overflow-wrap:anywhere}.facts.compact{font-size:var(--fs-secondary);margin-top:8px}.cards{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:minmax(0,1fr);gap:10px}.card{min-width:0;overflow-wrap:anywhere;background:var(--surface-2);border:1px solid var(--border);border-radius:4px;padding:10px 14px}.card h3{margin:6px 0 2px;font-size:var(--fs-h3);line-height:var(--lh-heading)}.card p{margin:4px 0}.card-head{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}.status{font-size:var(--fs-meta);padding:1px 8px;border-radius:10px;border:1px solid var(--ok);color:var(--ok)}.tag.tag-neutral{color:var(--muted);border-color:var(--muted)}.steps ul{margin:0;padding-left:20px}.tag{font-weight:400;font-size:var(--fs-label);color:var(--warn);border:1px solid var(--warn);border-radius:3px;padding:0 6px;margin-left:6px}.roadmap{margin:0;padding:12px;background:var(--surface-2);border-radius:4px;white-space:pre-wrap;overflow-wrap:anywhere;font-size:var(--fs-mono);line-height:1.55}.phases{list-style:none;margin:12px 0;padding:0;display:grid;gap:10px}.phase{min-width:0;overflow-wrap:anywhere;background:var(--surface-2);border:1px solid var(--border);border-radius:4px;padding:10px 14px}.phase-active{border-color:var(--accent);border-left-width:4px}.panel .phase-title{margin:0 0 4px;font-size:var(--fs-h3);line-height:var(--lh-heading)}.phase-meta{margin:0 0 6px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}.status-badge{font-size:var(--fs-meta);padding:1px 8px;border-radius:10px;border:1px solid var(--control-border);color:var(--text)}.status-badge.status-unrecognized{border-color:var(--warn);color:var(--warn)}.phase-meta .tag{margin-left:0;border-color:var(--accent);color:var(--accent)}.phase-goal{margin:4px 0}.phase-details,.raw{margin-top:8px}.phase-details summary,.raw summary{cursor:pointer;color:var(--accent)}.phase-details summary:focus-visible,.raw summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.phase-details .roadmap,.raw .roadmap{margin-top:8px;overflow-x:auto}.roadmap-diag{margin-top:12px}.diag li{min-width:0}.diag{list-style:none;margin:0;padding:0;display:grid;gap:4px}.sev{font-size:var(--fs-label);text-transform:uppercase;font-weight:700;margin-right:6px}.sev-info .sev{color:var(--accent)}.sev-warning .sev{color:var(--warn)}.sev-error .sev{color:var(--err)}.muted{color:var(--muted)}.empty{color:var(--muted);font-style:italic;margin:0}.unknown{color:var(--warn);font-weight:600;font-style:normal;letter-spacing:.04em}@media(max-width:899px){.shell{display:block}.sidebar{position:sticky;top:0;z-index:2;width:auto;height:auto;max-height:100vh;border-right:0;border-bottom:1px solid var(--border)}.sidebar-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 12px}.brand-logo{width:36px;height:36px}.sidebar-panel{display:none;padding-top:4px;max-height:calc(100vh - 56px)}.sidebar.open .sidebar-panel{display:flex}.nav-toggle{display:inline-block}.panel{scroll-margin-top:64px}}@media(max-width:600px){.topbar{padding-left:12px;padding-right:12px}.topbar-right{margin-left:0;width:100%;flex-wrap:wrap;justify-content:space-between}.content{padding:12px}.facts{grid-template-columns:1fr}.facts dd{margin-bottom:6px}}.live-badge{background:#1f6f43;color:#eafff3;font-weight:800;letter-spacing:.1em;padding:4px 12px;border-radius:4px}.live-badge-off{background:var(--surface-2);color:var(--muted, #9aa4b2);border:1px solid var(--border)}.story{white-space:pre-wrap;margin:0;font:inherit}button{background:var(--surface-2);color:var(--text);border:1px solid var(--control-border);border-radius:4px;padding:4px 12px;font:inherit;cursor:pointer}button:disabled{opacity:.5;cursor:default}.section-fade-enter-active{transition:opacity .22s ease-out,transform .22s ease-out}.section-fade-leave-active{transition:opacity .15s ease-in,transform .15s ease-in}.section-fade-enter-from{opacity:0;transform:translateY(8px)}.section-fade-leave-to{opacity:0;transform:translateY(-8px)}@media(prefers-reduced-motion:reduce){.section-fade-enter-active,.section-fade-leave-active{transition:none}.section-fade-enter-from,.section-fade-leave-to{opacity:1;transform:none}}.ov-block+.ov-block{margin-top:16px;padding-top:4px;border-top:1px solid var(--border)}.ov-block h3{margin-top:8px;text-transform:uppercase;letter-spacing:.06em;font-size:var(--fs-secondary);color:var(--muted)}:root[data-skin=cosmic-glass]{--bg: #080a1e;--surface: rgba(12, 16, 40, .74);--surface-2: rgba(30, 34, 76, .62);--border: rgba(190, 200, 255, .2);--control-border: rgba(204, 212, 255, .52);--text: #f1f4ff;--muted: #c6d0e8;--accent: #b4a9ff;--ok: #5fd68a;--warn: #f0c040;--err: #ff8a85;--demo: #f0b429;--glass-dense: rgba(9, 12, 32, .86);--glass-edge: inset 0 1px 0 rgba(255, 255, 255, .07);--glass-shadow: 0 8px 32px rgba(2, 3, 14, .45);--glass-blur: blur(14px) saturate(1.15);background:var(--bg)}:root[data-skin=cosmic-glass] body{background:transparent;isolation:isolate}:root[data-skin=cosmic-glass] body:before{content:"";position:fixed;top:0;bottom:0;right:0;left:-14vw;z-index:-1;pointer-events:none;background:linear-gradient(180deg,#080a1e00,#080a1e33 60%,#080a1e99),url(./cosmic-glass-bg-Dwx47zPu.webp) 24% 0 / cover no-repeat,var(--bg)}:root[data-skin=cosmic-glass] .sidebar{background:#080b2080;-webkit-backdrop-filter:blur(4px) saturate(1.1);backdrop-filter:blur(4px) saturate(1.1);border-right-color:var(--border);box-shadow:1px 0 #968cff14,6px 0 28px #02030e59}:root[data-skin=cosmic-glass] .brand strong{color:var(--text);letter-spacing:.16em}:root[data-skin=cosmic-glass] .brand .muted{font-size:var(--fs-meta)}:root[data-skin=cosmic-glass] .brand-logo{border-radius:9px;box-shadow:0 0 0 1px #bec8ff47,0 2px 12px #02030e80}:root[data-skin=cosmic-glass] .nav a{color:var(--muted);border-left:2px solid transparent;border-radius:0 6px 6px 0}:root[data-skin=cosmic-glass] .nav a:hover{color:var(--text);background:#968cff29;border-left-color:var(--accent)}:root[data-skin=cosmic-glass] .nav a[aria-current=location]{color:var(--text);background:linear-gradient(90deg,#968cff42,#968cff14);border-left-color:#d3ccff;box-shadow:none}:root[data-skin=cosmic-glass] .sidebar-panel .skin-switcher{border-bottom-color:var(--border)}:root[data-skin=cosmic-glass] .topbar{background:#080b20a8;-webkit-backdrop-filter:blur(12px) saturate(1.1);backdrop-filter:blur(12px) saturate(1.1);border-bottom-color:var(--border);box-shadow:0 6px 24px #02030e4d}:root[data-skin=cosmic-glass] .project{font-size:var(--fs-project);letter-spacing:.01em}:root[data-skin=cosmic-glass] .live-badge{background:#14532f;color:#eafff3;border:1px solid #5fd68a}:root[data-skin=cosmic-glass] .live-badge-off{background:var(--glass-dense);color:var(--muted);border-color:var(--border)}:root[data-skin=cosmic-glass] .demo-badge{border:1px solid #ffd978}:root[data-skin=cosmic-glass] .panel{background:var(--surface);-webkit-backdrop-filter:var(--glass-blur);backdrop-filter:var(--glass-blur);border-radius:10px;box-shadow:var(--glass-edge),var(--glass-shadow);padding:18px 22px}:root[data-skin=cosmic-glass] .panel h2{font-size:var(--fs-h2);letter-spacing:.08em;font-weight:700;padding-bottom:8px;margin-bottom:14px;border-bottom:1px solid var(--border)}:root[data-skin=cosmic-glass] .panel h3{font-size:var(--fs-h3);font-weight:650}:root[data-skin=cosmic-glass] .card{background:var(--surface-2);border-radius:8px;box-shadow:var(--glass-edge);line-height:1.55}:root[data-skin=cosmic-glass] .phase{background:var(--surface-2);border-radius:8px;box-shadow:var(--glass-edge);line-height:1.55}:root[data-skin=cosmic-glass] .phase-active{border-color:var(--accent)}:root[data-skin=cosmic-glass] .status-badge{background:#ffffff0f}:root[data-skin=cosmic-glass] .status-badge.status-unrecognized{background:#f0c0401a}:root[data-skin=cosmic-glass] .phase-meta .tag{background:#58a6ff1f}:root[data-skin=cosmic-glass] .facts dt{font-size:var(--fs-secondary)}:root[data-skin=cosmic-glass] .roadmap,:root[data-skin=cosmic-glass] .story{background:var(--glass-dense);border-radius:8px;line-height:1.6}:root[data-skin=cosmic-glass] .roadmap{border:1px solid var(--border)}:root[data-skin=cosmic-glass] .status{background:#5fd68a1f}:root[data-skin=cosmic-glass] .tag{background:#f0c0401a}:root[data-skin=cosmic-glass] button,:root[data-skin=cosmic-glass] select{background:var(--glass-dense);border-color:var(--control-border)}:root[data-skin=cosmic-glass] button:hover:not(:disabled){border-color:var(--accent);background:#3c3882b3}:root[data-skin=cosmic-glass] button:disabled{opacity:1;color:#8f9abb;border-style:dashed;cursor:not-allowed}:root[data-skin=cosmic-glass] a:focus-visible,:root[data-skin=cosmic-glass] button:focus-visible,:root[data-skin=cosmic-glass] select:focus-visible{outline:2px solid #d3ccff;outline-offset:2px;box-shadow:0 0 0 4px #9a8cff59}@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){:root[data-skin=cosmic-glass]{--surface: rgba(12, 16, 40, .92);--surface-2: rgba(26, 30, 70, .92)}:root[data-skin=cosmic-glass] .sidebar,:root[data-skin=cosmic-glass] .topbar{background:#080b20f2}}@media(prefers-reduced-motion:no-preference){:root[data-skin=cosmic-glass] .nav a,:root[data-skin=cosmic-glass] button{transition:background-color .15s,border-color .15s,color .15s}}@media(max-width:899px){:root[data-skin=cosmic-glass] body:before{left:0;background:linear-gradient(180deg,#080a1e00,#080a1e33 55%,#080a1e8c),url(./cosmic-glass-bg-Dwx47zPu.webp) 16% 0 / cover no-repeat,var(--bg)}:root[data-skin=cosmic-glass] .sidebar{background:#080b20d1;border-bottom-color:var(--border)}:root[data-skin=cosmic-glass] .panel{padding:14px 16px;background:#0c102899}}