create-principles-disciple 1.133.13 → 1.134.0
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/console/dist/server/routes/update.js +38 -7
- package/console/dist/server/update/legacy-mutation-journal.d.ts +118 -0
- package/console/dist/server/update/legacy-mutation-journal.js +220 -0
- package/console/dist/web/assets/app.js +81 -11
- package/console/package.json +2 -2
- package/core/dist/runtime-v2/__tests__/recovery-sweep-service.test.js +130 -0
- package/core/dist/runtime-v2/__tests__/recovery-sweep-service.test.js.map +1 -1
- package/core/dist/runtime-v2/recovery-sweep-service.d.ts +9 -0
- package/core/dist/runtime-v2/recovery-sweep-service.d.ts.map +1 -1
- package/core/dist/runtime-v2/recovery-sweep-service.js +7 -2
- package/core/dist/runtime-v2/recovery-sweep-service.js.map +1 -1
- package/core/package.json +1 -1
- package/dist/installer.d.ts +48 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +115 -13
- package/dist/installer.js.map +1 -1
- package/dist/uninstaller.d.ts.map +1 -1
- package/dist/uninstaller.js +7 -3
- package/dist/uninstaller.js.map +1 -1
- package/dist/update/install-layout.d.ts +39 -2
- package/dist/update/install-layout.d.ts.map +1 -1
- package/dist/update/install-layout.js +69 -3
- package/dist/update/install-layout.js.map +1 -1
- package/dist/update/release-manager-authority.d.ts +8 -0
- package/dist/update/release-manager-authority.d.ts.map +1 -1
- package/dist/update/release-manager-authority.js +26 -4
- package/dist/update/release-manager-authority.js.map +1 -1
- package/dist/update/release-metadata-source.d.ts +85 -0
- package/dist/update/release-metadata-source.d.ts.map +1 -0
- package/dist/update/release-metadata-source.js +102 -0
- package/dist/update/release-metadata-source.js.map +1 -0
- package/package.json +2 -2
- package/pd-cli/dist/commands/runtime-recovery-failed-tasks.d.ts.map +1 -1
- package/pd-cli/dist/commands/runtime-recovery-failed-tasks.js +82 -6
- package/pd-cli/dist/commands/runtime-recovery-failed-tasks.js.map +1 -1
- package/pd-cli/dist/index.js +6 -3
- package/pd-cli/dist/index.js.map +1 -1
- package/pd-cli/package.json +1 -1
- package/plugin/dist/bundle.js +3 -3
- package/plugin/dist/governance-audit.js +139 -139
- package/plugin/dist/rulehost-evidence.js +144 -144
- package/release-manager/dist/installer.d.ts +48 -0
- package/release-manager/dist/installer.d.ts.map +1 -1
- package/release-manager/dist/installer.js +115 -13
- package/release-manager/dist/installer.js.map +1 -1
- package/release-manager/dist/uninstaller.d.ts.map +1 -1
- package/release-manager/dist/uninstaller.js +7 -3
- package/release-manager/dist/uninstaller.js.map +1 -1
- package/release-manager/dist/update/install-layout.d.ts +39 -2
- package/release-manager/dist/update/install-layout.d.ts.map +1 -1
- package/release-manager/dist/update/install-layout.js +69 -3
- package/release-manager/dist/update/install-layout.js.map +1 -1
- package/release-manager/dist/update/release-manager-authority.d.ts +8 -0
- package/release-manager/dist/update/release-manager-authority.d.ts.map +1 -1
- package/release-manager/dist/update/release-manager-authority.js +26 -4
- package/release-manager/dist/update/release-manager-authority.js.map +1 -1
- package/release-manager/dist/update/release-metadata-source.d.ts +85 -0
- package/release-manager/dist/update/release-metadata-source.d.ts.map +1 -0
- package/release-manager/dist/update/release-metadata-source.js +102 -0
- package/release-manager/dist/update/release-metadata-source.js.map +1 -0
- package/release-manager/package.json +2 -2
|
@@ -11,6 +11,7 @@ import { resolveExtensionsDir, resolveUpdateLayout, resolvePluginDir, resolveCan
|
|
|
11
11
|
import { ActivationCompatibilityReadModel, isFeatureEnabled } from '@principles/core/runtime-v2';
|
|
12
12
|
import { collectFileDepLinkSpecs } from '../utils/update-links.js';
|
|
13
13
|
import { updateMutationController, LEGACY_MUTATION_AUTHORITY, RELEASE_MANAGER_AUTHORITY, MUTATION_KINDS, } from '../update/mutation-controller.js';
|
|
14
|
+
import { runLegacyJournaledMutation } from '../update/legacy-mutation-journal.js';
|
|
14
15
|
import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
|
|
15
16
|
/**
|
|
16
17
|
* Legacy rule contract preflight (2026-08-19): refuse to swap the runtime
|
|
@@ -1540,7 +1541,26 @@ function legacyCheckMutation(req, res, ctx) {
|
|
|
1540
1541
|
}
|
|
1541
1542
|
})();
|
|
1542
1543
|
}
|
|
1544
|
+
/**
|
|
1545
|
+
* PRI-709 P0-3 (ADR-0024 D-2): the shared installation root that owns the
|
|
1546
|
+
* transaction journal. Legacy console mutations journal to the SAME place as
|
|
1547
|
+
* the installer and the ReleaseManager — never a console-private ledger.
|
|
1548
|
+
*/
|
|
1549
|
+
function resolvePdHome() {
|
|
1550
|
+
return path.join(os.homedir(), '.pd');
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* A runtime mutation that ran without journal evidence is a governance gap,
|
|
1554
|
+
* not a mutation failure: it is reported loud (rc-9) and the mutation result
|
|
1555
|
+
* is still served. Blocking the Owner's update here would be worse than an
|
|
1556
|
+
* unaudited one — but the gap must never be silent.
|
|
1557
|
+
*/
|
|
1558
|
+
function logLegacyJournalGap(kind, reason) {
|
|
1559
|
+
console.warn(`[update] Runtime mutation "${kind}" completed WITHOUT journal evidence (${reason}). `
|
|
1560
|
+
+ 'ADR-0024 D-2 requires every runtime mutation to be auditable; this is usually a missing create-principles-disciple install surface.');
|
|
1561
|
+
}
|
|
1543
1562
|
function legacyApplyMutation(req, res, ctx) {
|
|
1563
|
+
const pdHome = resolvePdHome();
|
|
1544
1564
|
return (async () => {
|
|
1545
1565
|
const pluginDir = resolvePluginDir(ctx.workspaceDir);
|
|
1546
1566
|
if (req.method !== 'POST') {
|
|
@@ -1578,11 +1598,12 @@ function legacyApplyMutation(req, res, ctx) {
|
|
|
1578
1598
|
sendBadRequest(res, 'targetDir must be within workspace or extensions directory');
|
|
1579
1599
|
return;
|
|
1580
1600
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1601
|
+
// ADR-0024 D-2 (PRI-709 P0-3): every runtime mutation is journaled.
|
|
1602
|
+
// `planned` lands after all request validation, so a rejected request
|
|
1603
|
+
// leaves no transaction behind.
|
|
1604
|
+
const { result, journal } = await runLegacyJournaledMutation({ kind: 'apply', pdHome, pluginDir }, () => doApplyUpdate({ targetDir, mergeStrategy, createBackup }, ctx.workspaceDir));
|
|
1605
|
+
if (!journal.journaled)
|
|
1606
|
+
logLegacyJournalGap('apply', journal.reason);
|
|
1586
1607
|
sendSuccess(res, result);
|
|
1587
1608
|
}
|
|
1588
1609
|
catch (err) {
|
|
@@ -1595,6 +1616,7 @@ function legacyApplyMutation(req, res, ctx) {
|
|
|
1595
1616
|
})();
|
|
1596
1617
|
}
|
|
1597
1618
|
function legacyRollbackMutation(req, res, ctx) {
|
|
1619
|
+
const pdHome = resolvePdHome();
|
|
1598
1620
|
return (async () => {
|
|
1599
1621
|
const pluginDir = resolvePluginDir(ctx.workspaceDir);
|
|
1600
1622
|
if (req.method !== 'POST') {
|
|
@@ -1626,7 +1648,12 @@ function legacyRollbackMutation(req, res, ctx) {
|
|
|
1626
1648
|
sendBadRequest(res, 'backupDir must be within the workspace, extensions directory, or PD backups directory');
|
|
1627
1649
|
return;
|
|
1628
1650
|
}
|
|
1629
|
-
|
|
1651
|
+
// ADR-0024 D-2 (PRI-709 P0-3): a rollback is a forward transition to a
|
|
1652
|
+
// known-good deployment, not an undo of this transaction — that is why
|
|
1653
|
+
// the terminal state is `confirmed`, not `rolled_back`.
|
|
1654
|
+
const { result, journal } = await runLegacyJournaledMutation({ kind: 'rollback', pdHome, pluginDir }, () => doRollbackUpdate({ targetDir, backupDir }, ctx.workspaceDir));
|
|
1655
|
+
if (!journal.journaled)
|
|
1656
|
+
logLegacyJournalGap('rollback', journal.reason);
|
|
1630
1657
|
sendSuccess(res, result);
|
|
1631
1658
|
}
|
|
1632
1659
|
catch (err) {
|
|
@@ -1639,13 +1666,17 @@ function legacyRollbackMutation(req, res, ctx) {
|
|
|
1639
1666
|
})();
|
|
1640
1667
|
}
|
|
1641
1668
|
function legacyApplyFullMutation(req, res, ctx) {
|
|
1669
|
+
const pdHome = resolvePdHome();
|
|
1642
1670
|
return (async () => {
|
|
1643
1671
|
if (req.method !== 'POST') {
|
|
1644
1672
|
sendMethodNotAllowed(res);
|
|
1645
1673
|
return;
|
|
1646
1674
|
}
|
|
1675
|
+
const pluginDir = resolvePluginDir(ctx.workspaceDir);
|
|
1647
1676
|
try {
|
|
1648
|
-
const result = await doInlineFullUpdate(ctx.workspaceDir);
|
|
1677
|
+
const { result, journal } = await runLegacyJournaledMutation({ kind: 'apply-full', pdHome, pluginDir }, () => doInlineFullUpdate(ctx.workspaceDir));
|
|
1678
|
+
if (!journal.journaled)
|
|
1679
|
+
logLegacyJournalGap('apply-full', journal.reason);
|
|
1649
1680
|
sendSuccess(res, result);
|
|
1650
1681
|
}
|
|
1651
1682
|
catch (err) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-709 P0-3 — journal coverage for the legacy console updater (ADR-0024 D-2).
|
|
3
|
+
*
|
|
4
|
+
* PRI-698 Phase 0 Audit finding F-3: the console updater performed runtime
|
|
5
|
+
* mutations with **zero** journal writes. Under ADR-0024 D-2 an unjournaled
|
|
6
|
+
* runtime mutation is the one thing that must never happen, so the legacy
|
|
7
|
+
* updater had to be brought under the SAME journal as the installer and the
|
|
8
|
+
* ReleaseManager before the legacy path can be retired.
|
|
9
|
+
*
|
|
10
|
+
* Design rules (all load-bearing):
|
|
11
|
+
*
|
|
12
|
+
* - **No new journal implementation.** This module reuses
|
|
13
|
+
* `create-principles-disciple`'s `transaction-journal` — one JSONL file per
|
|
14
|
+
* transaction under `~/.pd/transactions/`, append + fsync, strict reader.
|
|
15
|
+
* Nothing here parses, formats or rotates journals.
|
|
16
|
+
* - **The ReleaseManager path is never double-journaled.** ReleaseManager
|
|
17
|
+
* `apply-full` orchestrates the installer, which journals the whole
|
|
18
|
+
* lifecycle; the console dispatch for that path never reaches the legacy
|
|
19
|
+
* handlers this module wraps. When the ReleaseManager explicitly falls back
|
|
20
|
+
* to legacy, the legacy handler is the ONLY writer — one transaction per
|
|
21
|
+
* mutation, whichever authority served it.
|
|
22
|
+
* - **Explicit degradation, never a blocked mutation.** The journal module is
|
|
23
|
+
* loaded dynamically: the console runs in installations where the
|
|
24
|
+
* create-principles-disciple dist may be absent (the same delivery-surface
|
|
25
|
+
* gap the ReleaseManager authority loader already handles). A missing module
|
|
26
|
+
* is reported and the mutation proceeds unjournaled — refusing the Owner's
|
|
27
|
+
* update would be worse than an unaudited one, and the gap is observable.
|
|
28
|
+
* - **No fabricated digests.** The legacy updater does not verify signed
|
|
29
|
+
* release metadata, so its digest provenance is `fallback`: a synthetic
|
|
30
|
+
* sha256 over a literal marker, readable but explicitly NOT verifiable. It
|
|
31
|
+
* never claims `manifest` / `signed_channel` it cannot back.
|
|
32
|
+
*
|
|
33
|
+
* Transition strategy for legacy kinds:
|
|
34
|
+
*
|
|
35
|
+
* apply / apply-full / rollback: `planned` → `confirmed` | `failed`
|
|
36
|
+
*
|
|
37
|
+
* `planned` is appended immediately before the mutation (after all request
|
|
38
|
+
* validation, so rejected requests leave no transaction); `confirmed` or
|
|
39
|
+
* `failed` after it. `rolled_back` is NOT used for the rollback kind — a
|
|
40
|
+
* rollback restores a previous deployment and is itself a forward transition
|
|
41
|
+
* to a known-good state, whereas `rolled_back` means "this transaction was
|
|
42
|
+
* undone".
|
|
43
|
+
*
|
|
44
|
+
* Two documented trade-offs (PRI-709 review, deliberate):
|
|
45
|
+
*
|
|
46
|
+
* - **`generation` stays at the standalone default `1`.** Legacy console
|
|
47
|
+
* transactions do not participate in the dual-slot generation lineage — they
|
|
48
|
+
* never move the active record — so recording a real `active.json.generation
|
|
49
|
+
* + 1` would CLAIM a lineage the console does not actually advance. Recovery
|
|
50
|
+
* is unaffected either way: `recoverUnfinishedTransaction` keys on
|
|
51
|
+
* `activeRecord.transactionId`, which a console transaction never matches, so
|
|
52
|
+
* it resolves to "the previously confirmed release stands".
|
|
53
|
+
* - **The legacy path leaves `active.json` untouched.** The legacy updater
|
|
54
|
+
* replaces the runtime without the installer's dual-slot swap, so after a
|
|
55
|
+
* legacy `apply-full` the active record describes the PREVIOUS deployment.
|
|
56
|
+
* Making the console a second writer of the deployment identity would be a
|
|
57
|
+
* worse violation (one source of truth); the honest fix is retiring the
|
|
58
|
+
* legacy path (ADR-0024 D-1), which is exactly what this journal coverage
|
|
59
|
+
* unblocks.
|
|
60
|
+
*/
|
|
61
|
+
import type { ReleaseMetadataDigestSource, TransactionState } from 'create-principles-disciple/dist/update/transaction-journal.js';
|
|
62
|
+
/** Journal kinds the legacy console updater performs. */
|
|
63
|
+
export type LegacyMutationKind = 'apply' | 'apply-full' | 'rollback';
|
|
64
|
+
export interface LegacyMutationJournal {
|
|
65
|
+
readonly transactionId: string;
|
|
66
|
+
readonly journalPath: string;
|
|
67
|
+
readonly releaseId: string;
|
|
68
|
+
readonly productVersion: string;
|
|
69
|
+
readonly releaseMetadataDigest: string;
|
|
70
|
+
}
|
|
71
|
+
export type LegacyJournalStatus = {
|
|
72
|
+
readonly journaled: true;
|
|
73
|
+
readonly journal: LegacyMutationJournal;
|
|
74
|
+
} | {
|
|
75
|
+
readonly journaled: false;
|
|
76
|
+
readonly reason: string;
|
|
77
|
+
};
|
|
78
|
+
/** Journal port — the real implementation is the shared transaction journal. */
|
|
79
|
+
export interface LegacyJournalPort {
|
|
80
|
+
appendJournalTransition(journalPath: string, transition: {
|
|
81
|
+
readonly at: string;
|
|
82
|
+
readonly from: TransactionState | null;
|
|
83
|
+
readonly to: TransactionState;
|
|
84
|
+
readonly transactionId: string;
|
|
85
|
+
readonly releaseId: string;
|
|
86
|
+
readonly productVersion: string;
|
|
87
|
+
readonly releaseMetadataDigest: string;
|
|
88
|
+
readonly releaseMetadataDigestSource: ReleaseMetadataDigestSource;
|
|
89
|
+
readonly generation: number;
|
|
90
|
+
readonly detail?: string;
|
|
91
|
+
}): void;
|
|
92
|
+
}
|
|
93
|
+
export interface LegacyJournalOptions {
|
|
94
|
+
/** `~/.pd` — the shared installation root (ADR-0023). */
|
|
95
|
+
readonly pdHome: string;
|
|
96
|
+
/** Installed runtime plugin dir; identity source of what is deployed. */
|
|
97
|
+
readonly pluginDir: string;
|
|
98
|
+
readonly kind: LegacyMutationKind;
|
|
99
|
+
readonly now?: () => Date;
|
|
100
|
+
/** Test seam; defaults to the dynamically imported shared journal. */
|
|
101
|
+
readonly journal?: LegacyJournalPort;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Open one legacy transaction and append `planned` before the mutation runs.
|
|
105
|
+
*/
|
|
106
|
+
export declare function openLegacyMutationJournal(options: LegacyJournalOptions): Promise<LegacyJournalStatus>;
|
|
107
|
+
/**
|
|
108
|
+
* Runs a legacy mutation under one journal transaction.
|
|
109
|
+
*
|
|
110
|
+
* `planned` is written before `run()`; `confirmed` / `failed` after — decided
|
|
111
|
+
* by BOTH the thrown/not-thrown boundary and the updater's `success` result
|
|
112
|
+
* shape (see `toLegacyMutationOutcome`). A journal failure never changes the
|
|
113
|
+
* mutation's outcome — it is reported alongside it.
|
|
114
|
+
*/
|
|
115
|
+
export declare function runLegacyJournaledMutation<T>(options: LegacyJournalOptions, run: () => Promise<T>): Promise<{
|
|
116
|
+
readonly result: T;
|
|
117
|
+
readonly journal: LegacyJournalStatus;
|
|
118
|
+
}>;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-709 P0-3 — journal coverage for the legacy console updater (ADR-0024 D-2).
|
|
3
|
+
*
|
|
4
|
+
* PRI-698 Phase 0 Audit finding F-3: the console updater performed runtime
|
|
5
|
+
* mutations with **zero** journal writes. Under ADR-0024 D-2 an unjournaled
|
|
6
|
+
* runtime mutation is the one thing that must never happen, so the legacy
|
|
7
|
+
* updater had to be brought under the SAME journal as the installer and the
|
|
8
|
+
* ReleaseManager before the legacy path can be retired.
|
|
9
|
+
*
|
|
10
|
+
* Design rules (all load-bearing):
|
|
11
|
+
*
|
|
12
|
+
* - **No new journal implementation.** This module reuses
|
|
13
|
+
* `create-principles-disciple`'s `transaction-journal` — one JSONL file per
|
|
14
|
+
* transaction under `~/.pd/transactions/`, append + fsync, strict reader.
|
|
15
|
+
* Nothing here parses, formats or rotates journals.
|
|
16
|
+
* - **The ReleaseManager path is never double-journaled.** ReleaseManager
|
|
17
|
+
* `apply-full` orchestrates the installer, which journals the whole
|
|
18
|
+
* lifecycle; the console dispatch for that path never reaches the legacy
|
|
19
|
+
* handlers this module wraps. When the ReleaseManager explicitly falls back
|
|
20
|
+
* to legacy, the legacy handler is the ONLY writer — one transaction per
|
|
21
|
+
* mutation, whichever authority served it.
|
|
22
|
+
* - **Explicit degradation, never a blocked mutation.** The journal module is
|
|
23
|
+
* loaded dynamically: the console runs in installations where the
|
|
24
|
+
* create-principles-disciple dist may be absent (the same delivery-surface
|
|
25
|
+
* gap the ReleaseManager authority loader already handles). A missing module
|
|
26
|
+
* is reported and the mutation proceeds unjournaled — refusing the Owner's
|
|
27
|
+
* update would be worse than an unaudited one, and the gap is observable.
|
|
28
|
+
* - **No fabricated digests.** The legacy updater does not verify signed
|
|
29
|
+
* release metadata, so its digest provenance is `fallback`: a synthetic
|
|
30
|
+
* sha256 over a literal marker, readable but explicitly NOT verifiable. It
|
|
31
|
+
* never claims `manifest` / `signed_channel` it cannot back.
|
|
32
|
+
*
|
|
33
|
+
* Transition strategy for legacy kinds:
|
|
34
|
+
*
|
|
35
|
+
* apply / apply-full / rollback: `planned` → `confirmed` | `failed`
|
|
36
|
+
*
|
|
37
|
+
* `planned` is appended immediately before the mutation (after all request
|
|
38
|
+
* validation, so rejected requests leave no transaction); `confirmed` or
|
|
39
|
+
* `failed` after it. `rolled_back` is NOT used for the rollback kind — a
|
|
40
|
+
* rollback restores a previous deployment and is itself a forward transition
|
|
41
|
+
* to a known-good state, whereas `rolled_back` means "this transaction was
|
|
42
|
+
* undone".
|
|
43
|
+
*
|
|
44
|
+
* Two documented trade-offs (PRI-709 review, deliberate):
|
|
45
|
+
*
|
|
46
|
+
* - **`generation` stays at the standalone default `1`.** Legacy console
|
|
47
|
+
* transactions do not participate in the dual-slot generation lineage — they
|
|
48
|
+
* never move the active record — so recording a real `active.json.generation
|
|
49
|
+
* + 1` would CLAIM a lineage the console does not actually advance. Recovery
|
|
50
|
+
* is unaffected either way: `recoverUnfinishedTransaction` keys on
|
|
51
|
+
* `activeRecord.transactionId`, which a console transaction never matches, so
|
|
52
|
+
* it resolves to "the previously confirmed release stands".
|
|
53
|
+
* - **The legacy path leaves `active.json` untouched.** The legacy updater
|
|
54
|
+
* replaces the runtime without the installer's dual-slot swap, so after a
|
|
55
|
+
* legacy `apply-full` the active record describes the PREVIOUS deployment.
|
|
56
|
+
* Making the console a second writer of the deployment identity would be a
|
|
57
|
+
* worse violation (one source of truth); the honest fix is retiring the
|
|
58
|
+
* legacy path (ADR-0024 D-1), which is exactly what this journal coverage
|
|
59
|
+
* unblocks.
|
|
60
|
+
*/
|
|
61
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
62
|
+
import * as fs from 'node:fs';
|
|
63
|
+
import * as path from 'node:path';
|
|
64
|
+
/** Load the shared journal; `null` when the delivery surface lacks the module. */
|
|
65
|
+
async function loadJournalPort() {
|
|
66
|
+
try {
|
|
67
|
+
const module = await import('create-principles-disciple/dist/update/transaction-journal.js');
|
|
68
|
+
return { appendJournalTransition: module.appendJournalTransition };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Product version of the currently installed runtime.
|
|
76
|
+
*
|
|
77
|
+
* Same rule as the installer (PRI-709 P0-2): the PRODUCT manifest is
|
|
78
|
+
* `pluginDir/package.json`; `pd-cli` versions independently and is only a
|
|
79
|
+
* fallback.
|
|
80
|
+
*/
|
|
81
|
+
function resolveInstalledProductVersion(pluginDir) {
|
|
82
|
+
for (const candidate of [path.join(pluginDir, 'package.json'), path.join(pluginDir, 'pd-cli', 'package.json')]) {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
85
|
+
if (typeof parsed.version === 'string' && parsed.version.length > 0)
|
|
86
|
+
return parsed.version;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Identity falls back; an unjournaled-able version must not block a mutation.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return 'unknown';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Open one legacy transaction and append `planned` before the mutation runs.
|
|
96
|
+
*/
|
|
97
|
+
export async function openLegacyMutationJournal(options) {
|
|
98
|
+
const port = options.journal ?? await loadJournalPort();
|
|
99
|
+
if (port === null) {
|
|
100
|
+
return { journaled: false, reason: 'journal_module_unavailable' };
|
|
101
|
+
}
|
|
102
|
+
const productVersion = resolveInstalledProductVersion(options.pluginDir);
|
|
103
|
+
// Unverifiable by construction: the legacy updater never sees signed release
|
|
104
|
+
// metadata, so the digest is a marker hash labelled `fallback`.
|
|
105
|
+
const releaseMetadataDigest = createHash('sha256')
|
|
106
|
+
.update(`legacy-console-updater-unverified:${options.kind}`)
|
|
107
|
+
.digest('hex');
|
|
108
|
+
const transactionId = `console-${options.kind}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
109
|
+
const releaseId = `console-${options.kind}-${productVersion}-${releaseMetadataDigest.slice(0, 12)}`;
|
|
110
|
+
const journal = {
|
|
111
|
+
transactionId,
|
|
112
|
+
journalPath: path.join(options.pdHome, 'transactions', `${transactionId}.jsonl`),
|
|
113
|
+
releaseId,
|
|
114
|
+
productVersion,
|
|
115
|
+
releaseMetadataDigest,
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
port.appendJournalTransition(journal.journalPath, {
|
|
119
|
+
at: (options.now ?? (() => new Date))().toISOString(),
|
|
120
|
+
from: null,
|
|
121
|
+
to: 'planned',
|
|
122
|
+
transactionId,
|
|
123
|
+
releaseId,
|
|
124
|
+
productVersion,
|
|
125
|
+
releaseMetadataDigest,
|
|
126
|
+
releaseMetadataDigestSource: 'fallback',
|
|
127
|
+
generation: 1,
|
|
128
|
+
detail: `actor=console-updater kind=${options.kind}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return {
|
|
133
|
+
journaled: false,
|
|
134
|
+
reason: `journal_write_failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { journaled: true, journal };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Maps a mutation's RESULT to its journal terminal state.
|
|
141
|
+
*
|
|
142
|
+
* PRI-709 review: the legacy updater reports failure as a VALUE —
|
|
143
|
+
* `doApplyUpdate` / `doRollbackUpdate` / `doInlineFullUpdate` return
|
|
144
|
+
* `{ success: false, message }` for ~30 distinct failure modes and only throw
|
|
145
|
+
* for infrastructure errors. Terminal state decided on "did it throw" alone
|
|
146
|
+
* would journal confirmed for most real failures. So: `success === true` is
|
|
147
|
+
* `confirmed`, `success === false` is `failed` (with the reported message as
|
|
148
|
+
* detail), and a non-object result (no contract) is treated as completed.
|
|
149
|
+
* The HTTP response contract is untouched either way — this only decides what
|
|
150
|
+
* the journal records.
|
|
151
|
+
*/
|
|
152
|
+
function toLegacyMutationOutcome(result) {
|
|
153
|
+
if (typeof result !== 'object' || result === null) {
|
|
154
|
+
return { to: 'confirmed', detail: 'completed' };
|
|
155
|
+
}
|
|
156
|
+
const record = result;
|
|
157
|
+
if (!Object.hasOwn(record, 'success')) {
|
|
158
|
+
return { to: 'confirmed', detail: 'completed' };
|
|
159
|
+
}
|
|
160
|
+
const detailParts = [record.message, record.reason]
|
|
161
|
+
.filter((part) => typeof part === 'string' && part.length > 0);
|
|
162
|
+
const detail = detailParts.length > 0 ? detailParts.join('; ') : 'no detail reported';
|
|
163
|
+
return { to: record.success === true ? 'confirmed' : 'failed', detail };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Writes the terminal transition for an opened legacy transaction and reports
|
|
167
|
+
* loud when it cannot land: the journal would then hold an unfinished
|
|
168
|
+
* transaction, which is a governance gap (rc-9) even though the mutation's
|
|
169
|
+
* real outcome must not be masked.
|
|
170
|
+
*/
|
|
171
|
+
async function appendLegacyOutcome(options, status, outcome) {
|
|
172
|
+
if (!status.journaled)
|
|
173
|
+
return;
|
|
174
|
+
const port = options.journal ?? await loadJournalPort();
|
|
175
|
+
if (port === null)
|
|
176
|
+
return;
|
|
177
|
+
try {
|
|
178
|
+
port.appendJournalTransition(status.journal.journalPath, {
|
|
179
|
+
at: (options.now ?? (() => new Date))().toISOString(),
|
|
180
|
+
from: 'planned',
|
|
181
|
+
to: outcome.to,
|
|
182
|
+
transactionId: status.journal.transactionId,
|
|
183
|
+
releaseId: status.journal.releaseId,
|
|
184
|
+
productVersion: status.journal.productVersion,
|
|
185
|
+
releaseMetadataDigest: status.journal.releaseMetadataDigest,
|
|
186
|
+
releaseMetadataDigestSource: 'fallback',
|
|
187
|
+
generation: 1,
|
|
188
|
+
detail: `actor=console-updater kind=${options.kind} ${outcome.detail}`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
console.warn(`[update] Legacy mutation "${options.kind}" finished as "${outcome.to}" but its terminal journal transition `
|
|
193
|
+
+ `could not be written (${error instanceof Error ? error.message : String(error)}). `
|
|
194
|
+
+ `Transaction ${status.journal.transactionId} stays unfinished and must be reconciled.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Runs a legacy mutation under one journal transaction.
|
|
199
|
+
*
|
|
200
|
+
* `planned` is written before `run()`; `confirmed` / `failed` after — decided
|
|
201
|
+
* by BOTH the thrown/not-thrown boundary and the updater's `success` result
|
|
202
|
+
* shape (see `toLegacyMutationOutcome`). A journal failure never changes the
|
|
203
|
+
* mutation's outcome — it is reported alongside it.
|
|
204
|
+
*/
|
|
205
|
+
export async function runLegacyJournaledMutation(options, run) {
|
|
206
|
+
const opened = await openLegacyMutationJournal(options);
|
|
207
|
+
try {
|
|
208
|
+
const result = await run();
|
|
209
|
+
const outcome = toLegacyMutationOutcome(result);
|
|
210
|
+
await appendLegacyOutcome(options, opened, { to: outcome.to, detail: `${options.kind}: ${outcome.detail}` });
|
|
211
|
+
return { result, journal: opened };
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
await appendLegacyOutcome(options, opened, {
|
|
215
|
+
to: 'failed',
|
|
216
|
+
detail: `${options.kind}: ${error instanceof Error ? error.message : String(error)}`,
|
|
217
|
+
});
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|