@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.
- package/README.md +1 -1
- package/dist/brief/cli.d.ts.map +1 -1
- package/dist/brief/cli.js +8 -2
- package/dist/brief/collect.d.ts.map +1 -1
- package/dist/brief/collect.js +11 -3
- package/dist/brief/freshness.d.ts +17 -2
- package/dist/brief/freshness.d.ts.map +1 -1
- package/dist/brief/freshness.js +22 -4
- package/dist/brief/git-signals.d.ts +32 -5
- package/dist/brief/git-signals.d.ts.map +1 -1
- package/dist/brief/git-signals.js +169 -39
- package/dist/brief/knowledge.d.ts +62 -0
- package/dist/brief/knowledge.d.ts.map +1 -0
- package/dist/brief/knowledge.js +123 -0
- package/dist/brief/reconcile.d.ts +7 -1
- package/dist/brief/reconcile.d.ts.map +1 -1
- package/dist/brief/reconcile.js +42 -2
- package/dist/cli/index.js +1 -1
- package/dist/cli/style.d.ts +26 -0
- package/dist/cli/style.d.ts.map +1 -0
- package/dist/cli/style.js +76 -0
- package/dist/cli/ui.d.ts.map +1 -1
- package/dist/cli/ui.js +33 -6
- package/dist/coordinatorium/cli.d.ts.map +1 -1
- package/dist/coordinatorium/cli.js +13 -0
- package/dist/finalize/cli.d.ts.map +1 -1
- package/dist/finalize/cli.js +30 -13
- package/dist/finalize/consistency.d.ts +51 -0
- package/dist/finalize/consistency.d.ts.map +1 -0
- package/dist/finalize/consistency.js +57 -0
- package/dist/finalize/core.d.ts +26 -0
- package/dist/finalize/core.d.ts.map +1 -1
- package/dist/finalize/core.js +140 -1
- package/dist/ideas/cli.d.ts +1 -0
- package/dist/ideas/cli.d.ts.map +1 -1
- package/dist/ideas/cli.js +14 -3
- package/dist/ideas/format.d.ts +31 -1
- package/dist/ideas/format.d.ts.map +1 -1
- package/dist/ideas/format.js +109 -1
- package/dist/ideas/store.d.ts +2 -0
- package/dist/ideas/store.d.ts.map +1 -1
- package/dist/ideas/store.js +4 -1
- package/dist/ideas/types.d.ts +10 -0
- package/dist/ideas/types.d.ts.map +1 -1
- package/dist/ideas/types.js +3 -0
- package/dist/readmodel/dashboard.d.ts.map +1 -1
- package/dist/readmodel/dashboard.js +19 -1
- package/dist/readmodel/index.d.ts +2 -1
- package/dist/readmodel/index.d.ts.map +1 -1
- package/dist/readmodel/index.js +1 -0
- package/dist/readmodel/roadmap.d.ts +34 -0
- package/dist/readmodel/roadmap.d.ts.map +1 -0
- package/dist/readmodel/roadmap.js +254 -0
- package/dist/readmodel/types.d.ts +88 -0
- package/dist/readmodel/types.d.ts.map +1 -1
- package/dist/root/safe-write.d.ts +15 -4
- package/dist/root/safe-write.d.ts.map +1 -1
- package/dist/root/safe-write.js +264 -50
- package/dist/ui/assets/index-BJKxUDPD.css +1 -0
- package/dist/ui/assets/index-ioC9DPHQ.js +28 -0
- package/dist/ui/index.html +2 -2
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +4 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/dist/ui/assets/index-5zXAgig8.css +0 -1
- package/dist/ui/assets/index-CQgoMYLM.js +0 -28
package/dist/root/safe-write.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
663
|
-
|
|
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(
|
|
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
|
-
|
|
671
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
910
|
-
|
|
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
|
-
|
|
913
|
-
outcome
|
|
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
|
-
|
|
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
|
-
|
|
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}}
|