create-principles-disciple 1.144.22 → 1.144.25

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/_release/product-identity.json +1 -1
  2. package/codex-adapter/dist/pd-hook.js +84 -9
  3. package/codex-adapter/package.json +1 -1
  4. package/console/dist/server/routes/update-transaction.js +54 -8
  5. package/console/dist/ui/i18n/en.json +2 -0
  6. package/console/dist/ui/i18n/zh-CN.json +2 -0
  7. package/console/dist/ui/pages/principles/PrincipleDetailPage.js +9 -5
  8. package/console/dist/ui/utils/validators.d.ts +16 -0
  9. package/console/dist/ui/utils/validators.js +14 -1
  10. package/console/dist/web/assets/app.js +22 -22
  11. package/core/dist/principle-tree-ledger.d.ts +20 -0
  12. package/core/dist/principle-tree-ledger.d.ts.map +1 -1
  13. package/core/dist/principle-tree-ledger.js +13 -9
  14. package/core/dist/principle-tree-ledger.js.map +1 -1
  15. package/core/dist/runtime-v2/activation/activation-dispatcher.d.ts.map +1 -1
  16. package/core/dist/runtime-v2/activation/activation-dispatcher.js +10 -3
  17. package/core/dist/runtime-v2/activation/activation-dispatcher.js.map +1 -1
  18. package/core/dist/runtime-v2/activation/ledger-identity.d.ts +12 -3
  19. package/core/dist/runtime-v2/activation/ledger-identity.d.ts.map +1 -1
  20. package/core/dist/runtime-v2/activation/ledger-identity.js +18 -3
  21. package/core/dist/runtime-v2/activation/ledger-identity.js.map +1 -1
  22. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.d.ts +8 -0
  23. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.d.ts.map +1 -1
  24. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.js +11 -1
  25. package/core/dist/runtime-v2/adapter/principle-tree-ledger-adapter.js.map +1 -1
  26. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts +14 -3
  27. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  28. package/core/dist/runtime-v2/internalization/evaluator-runner.js +10 -2
  29. package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  30. package/core/dist/runtime-v2/internalization/scribe-runner.d.ts +4 -0
  31. package/core/dist/runtime-v2/internalization/scribe-runner.d.ts.map +1 -1
  32. package/core/dist/runtime-v2/internalization/scribe-runner.js +13 -1
  33. package/core/dist/runtime-v2/internalization/scribe-runner.js.map +1 -1
  34. package/core/dist/telemetry-event.d.ts.map +1 -1
  35. package/core/dist/telemetry-event.js +3 -2
  36. package/core/dist/telemetry-event.js.map +1 -1
  37. package/core/package.json +1 -1
  38. package/dist/update/release-manager.d.ts +1 -1
  39. package/dist/update/release-manager.d.ts.map +1 -1
  40. package/dist/update/release-manager.js +32 -1
  41. package/dist/update/release-manager.js.map +1 -1
  42. package/dist/update/transaction-journal.d.ts +12 -1
  43. package/dist/update/transaction-journal.d.ts.map +1 -1
  44. package/dist/update/transaction-journal.js +2 -2
  45. package/dist/update/transaction-journal.js.map +1 -1
  46. package/host-runtime/dist/internalization-consumer-cycle.js +6 -0
  47. package/host-runtime/package.json +1 -1
  48. package/package.json +1 -1
  49. package/pd-cli/dist/commands/pain-retry.d.ts.map +1 -1
  50. package/pd-cli/dist/commands/pain-retry.js +9 -3
  51. package/pd-cli/dist/commands/pain-retry.js.map +1 -1
  52. package/pd-cli/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  53. package/pd-cli/dist/commands/runtime-internalization-run-once.js +3 -0
  54. package/pd-cli/dist/commands/runtime-internalization-run-once.js.map +1 -1
  55. package/pd-cli/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
  56. package/pd-cli/dist/services/rulehost-pipeline-runner.js +6 -0
  57. package/pd-cli/dist/services/rulehost-pipeline-runner.js.map +1 -1
  58. package/pd-cli/package.json +1 -1
  59. package/plugin/dist/bundle.js +322 -322
  60. package/release-manager/dist/update/release-manager.d.ts +1 -1
  61. package/release-manager/dist/update/release-manager.d.ts.map +1 -1
  62. package/release-manager/dist/update/release-manager.js +32 -1
  63. package/release-manager/dist/update/release-manager.js.map +1 -1
  64. package/release-manager/dist/update/transaction-journal.d.ts +12 -1
  65. package/release-manager/dist/update/transaction-journal.d.ts.map +1 -1
  66. package/release-manager/dist/update/transaction-journal.js +2 -2
  67. package/release-manager/dist/update/transaction-journal.js.map +1 -1
  68. package/release-manager/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "productVersion": "2.3.0",
4
- "sourceCommit": "693e91ab726949844986dcbc2adaa27a5ef53a31"
4
+ "sourceCommit": "7e8635af81a5b43f1bd8c0a2305276805b6b22ab"
5
5
  }
@@ -365,14 +365,75 @@ export async function processHookInvocation(rawStdin, _env = process.env, cwd =
365
365
  return { stdout: {}, exitCode: 0, stderr: [diagnostic(reason, nextAction)] };
366
366
  }
367
367
  }
368
+ // PRI-943: process-boundary fail-open belt. Every resolved path sets exitCode 0,
369
+ // so nothing may let an unwritable stream, an unserializable result, or an
370
+ // escaping rejection reach node's default handling, which terminates the hook
371
+ // with exit 1. Three distinct failure modes, each reproduced from a spawned
372
+ // process in tests/pd-hook-entry-belt.test.ts:
373
+ // * a write that THROWS synchronously — caught per write below;
374
+ // * a host closing the pipe, which raises an ASYNC 'error' event on the
375
+ // Socket. try/catch cannot reach that one, so a listener is required
376
+ // (armStreamBelts) or node aborts with "Unhandled 'error' event".
377
+ // * the stdout JSON object failing to SERIALIZE — guarded separately because
378
+ // the object is the contract (serializeStdout).
379
+ // stdout must stay exactly one JSON object, so a belt diagnostic goes to stderr
380
+ // only; when stderr itself is the dead stream nothing is reported rather than
381
+ // written into the machine channel.
382
+ let beltReported = false;
383
+ let stdoutJsonDelivered = false;
384
+ function writeBeltDiagnostic(reason, nextAction) {
385
+ if (beltReported)
386
+ return;
387
+ beltReported = true;
388
+ try {
389
+ process.stderr.write(`${diagnostic(reason, nextAction)}\n`);
390
+ }
391
+ catch {
392
+ // Both streams are gone: there is no channel left inside the fail-open
393
+ // contract, and adding I/O here would widen the belt itself.
394
+ }
395
+ }
396
+ function reportStreamFailure(error) {
397
+ writeBeltDiagnostic(`hook_stream_failed:${errorMessage(error)}`, 'The host closed the pipe; retry the tool call — the hook failed open.');
398
+ }
399
+ function writeStream(stream, line) {
400
+ try {
401
+ process[stream].write(`${line}\n`);
402
+ // Tracked here rather than at each call site so no future stdout write can
403
+ // make the entry belt believe nothing was delivered yet.
404
+ if (stream === 'stdout')
405
+ stdoutJsonDelivered = true;
406
+ return true;
407
+ }
408
+ catch (error) {
409
+ reportStreamFailure(error);
410
+ return false;
411
+ }
412
+ }
413
+ function armStreamBelts() {
414
+ process.stdout.on('error', reportStreamFailure);
415
+ process.stderr.on('error', reportStreamFailure);
416
+ }
417
+ /** rc-8: the stdout JSON object is the contract itself, so serializing it is a
418
+ * failure path of the belt rather than of the encoder — degrade to `{}` and
419
+ * report, never let the throw escape into the entry belt's last-resort branch. */
420
+ function serializeStdout(value) {
421
+ try {
422
+ return JSON.stringify(value) ?? '{}';
423
+ }
424
+ catch (error) {
425
+ writeBeltDiagnostic(`hook_stdout_serialize_failed:${errorMessage(error)}`, 'Retry the tool call; the hook failed open with an empty JSON object.');
426
+ return '{}';
427
+ }
428
+ }
368
429
  async function main() {
369
430
  let raw;
370
431
  try {
371
432
  raw = readFileSync(0, 'utf8');
372
433
  }
373
434
  catch (error) {
374
- process.stderr.write(`${diagnostic(`stdin_read_failed:${errorMessage(error)}`, 'Run the hook from Codex with JSON stdin.')}\n`);
375
- process.stdout.write('{}\n');
435
+ writeStream('stderr', diagnostic(`stdin_read_failed:${errorMessage(error)}`, 'Run the hook from Codex with JSON stdin.'));
436
+ writeStream('stdout', '{}');
376
437
  return;
377
438
  }
378
439
  let result;
@@ -383,12 +444,16 @@ async function main() {
383
444
  // Fail-open belt for an unexpected pre-dispatch throw (e.g. a workspace
384
445
  // resolution race): Codex must still receive exactly one JSON object on
385
446
  // stdout and a bounded diagnostic on stderr — never a bare crash.
386
- process.stderr.write(`${diagnostic(`hook_pipeline_unexpected:${errorMessage(error)}`, 'Retry the tool call; if it repeats, inspect PD stderr and the Workspace .pd/config.yaml state.')}\n`);
447
+ writeStream('stderr', diagnostic(`hook_pipeline_unexpected:${errorMessage(error)}`, 'Retry the tool call; if it repeats, inspect PD stderr and the Workspace .pd/config.yaml state.'));
387
448
  result = { stdout: {}, exitCode: 0, stderr: [] };
388
449
  }
389
- for (const line of result.stderr)
390
- process.stderr.write(`${line}\n`);
391
- process.stdout.write(`${JSON.stringify(result.stdout)}\n`);
450
+ // Each write is guarded on its own: with stderr dead the stdout JSON object
451
+ // is still what Codex needs to fail open, so one stream must not abort the other.
452
+ for (const line of result.stderr) {
453
+ if (!writeStream('stderr', line))
454
+ break;
455
+ }
456
+ writeStream('stdout', serializeStdout(result.stdout));
392
457
  process.exitCode = result.exitCode;
393
458
  }
394
459
  // PRI-892: Node resolves the main module through symlinks/junctions while
@@ -414,9 +479,19 @@ function isMainModuleEntry() {
414
479
  // satisfies it, because its argv[1] is the runner, not pd-hook.
415
480
  if (path.basename(entry) !== path.basename(fileURLToPath(import.meta.url)))
416
481
  return false;
417
- process.stderr.write(`${diagnostic(`entry_identity_unverified:${errorMessage(error)}`, 'Reinstall the Codex adapter; if it repeats, report this line together with the command that invoked the hook.')}\n`);
482
+ writeStream('stderr', diagnostic(`entry_identity_unverified:${errorMessage(error)}`, 'Reinstall the Codex adapter; if it repeats, report this line together with the command that invoked the hook.'));
418
483
  return true;
419
484
  }
420
485
  }
421
- if (isMainModuleEntry())
422
- void main();
486
+ if (isMainModuleEntry()) {
487
+ armStreamBelts();
488
+ void main().catch((error) => {
489
+ // Last-resort belt: a rejection escaping main's own guards must still leave
490
+ // Codex with exit 0 and, when nothing reached stdout yet, an empty JSON
491
+ // object instead of a bare crash.
492
+ writeBeltDiagnostic(`hook_fatal:${errorMessage(error)}`, 'Retry the tool call; if it repeats, report this line together with the command that invoked the hook.');
493
+ if (!stdoutJsonDelivered)
494
+ writeStream('stdout', '{}');
495
+ process.exitCode = 0;
496
+ });
497
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/codex-adapter",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Codex CLI host adapter for Principles Disciple — implements HostAdapter interface for OpenAI Codex CLI's stdin/stdout JSON hook model (ADR-0020).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -5,8 +5,8 @@ import { sendSuccess, sendMethodNotAllowed, sendNotFound } from '../utils/respon
5
5
  /** Transaction ids are installer/console-generated (`<word>-<ts>-<hex>`); this
6
6
  * pattern doubles as the path-traversal guard for the journal file name (rc-1). */
7
7
  const TRANSACTION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
8
- function readJournalFact(journal, journalPath, transactionId) {
9
- const { transitions, tornTailDetected } = journal.readTransactionJournalForRecovery(journalPath);
8
+ function factFromTransitions(journal, transactionId, read) {
9
+ const { transitions } = read;
10
10
  const last = transitions[transitions.length - 1];
11
11
  const [first] = transitions;
12
12
  return {
@@ -14,7 +14,7 @@ function readJournalFact(journal, journalPath, transactionId) {
14
14
  exists: true,
15
15
  lastState: last?.to ?? null,
16
16
  terminal: last !== undefined && journal.isTerminalTransactionState(last.to),
17
- tornTailDetected,
17
+ tornTailDetected: read.tornTailDetected,
18
18
  startedAt: first?.at ?? null,
19
19
  lastAt: last?.at ?? null,
20
20
  productVersion: last?.productVersion ?? first?.productVersion ?? null,
@@ -26,6 +26,9 @@ function readJournalFact(journal, journalPath, transactionId) {
26
26
  })),
27
27
  };
28
28
  }
29
+ function readJournalFact(journal, journalPath, transactionId) {
30
+ return factFromTransitions(journal, transactionId, journal.readTransactionJournalForRecovery(journalPath));
31
+ }
29
32
  async function transactionStatus(res, transactionId) {
30
33
  if (!TRANSACTION_ID_PATTERN.test(transactionId)) {
31
34
  sendSuccess(res, { transactionId, exists: false, reason: 'invalid_transaction_id' });
@@ -78,22 +81,57 @@ async function recoveryStatus(res) {
78
81
  });
79
82
  }
80
83
  catch {
81
- sendSuccess(res, { needsRecovery: false, unfinished: [], broken: [] });
84
+ sendSuccess(res, { needsRecovery: false, unfinished: [], superseded: [], broken: [] });
82
85
  return null;
83
86
  }
84
87
  })();
85
88
  if (entries === null)
86
89
  return;
87
90
  const unfinished = [];
91
+ const superseded = [];
88
92
  const broken = [];
93
+ // PRI-896: a non-terminal journal is only an ALARM if the product's own
94
+ // recovery decision cannot explain it away. The live release record is the
95
+ // evidence for that, so a missing/corrupt active record means NO evidence —
96
+ // the alarm then stays, which is the conservative direction.
97
+ //
98
+ // rc-9 (no silent fallback): "file absent" (readActiveRecord returns null) and
99
+ // "file present but unreadable/corrupt" are different facts. The conservative
100
+ // outcome is the same (null → keep the alarm), but collapsing them hides WHY
101
+ // the alarm persists. Track unreadability explicitly so the response can say
102
+ // the alarm is retained for lack of a readable active record.
103
+ let activeRecordUnreadable = false;
104
+ const activeRecord = (() => {
105
+ try {
106
+ return journal.readActiveRecord(paths.activeRecordPath);
107
+ }
108
+ catch {
109
+ activeRecordUnreadable = true;
110
+ return null;
111
+ }
112
+ })();
89
113
  for (const entry of entries) {
90
114
  const transactionId = entry.name.slice(0, -'.jsonl'.length);
91
115
  if (!TRANSACTION_ID_PATTERN.test(transactionId))
92
116
  continue;
93
117
  try {
94
- const fact = readJournalFact(journal, path.join(paths.transactionsDir, entry.name), transactionId);
95
- if (!fact.terminal)
96
- unfinished.push(fact);
118
+ const read = journal.readTransactionJournalForRecovery(path.join(paths.transactionsDir, entry.name));
119
+ const fact = factFromTransitions(journal, transactionId, read);
120
+ if (fact.terminal)
121
+ continue;
122
+ const outcome = journal.recoverUnfinishedTransaction({
123
+ transitions: read.transitions,
124
+ activeRecord,
125
+ transactionId,
126
+ });
127
+ if (activeRecord !== null && outcome.kind === 'old_confirmed') {
128
+ // Benign residue a killed run left behind, already replaced by the
129
+ // live generation. Reported separately (rc-9: never a silent drop),
130
+ // but it must not hold the Owner's attention forever.
131
+ superseded.push({ transactionId, reason: outcome.reason });
132
+ continue;
133
+ }
134
+ unfinished.push(fact);
97
135
  }
98
136
  catch (error) {
99
137
  broken.push({ transactionId, reason: error instanceof Error ? error.message : String(error) });
@@ -103,10 +141,18 @@ async function recoveryStatus(res) {
103
141
  sendSuccess(res, {
104
142
  needsRecovery: unfinished.length > 0 || broken.length > 0,
105
143
  unfinished,
144
+ superseded,
106
145
  broken,
146
+ // rc-9: when the active record could not be read, the recovery decision had
147
+ // no live-release evidence, so every non-terminal journal is conservatively
148
+ // treated as an alarm. Surfacing this explains WHY the alarm persists and
149
+ // points the Owner at repairing active.json rather than the journals.
150
+ activeRecordUnreadable,
107
151
  // SPEC §12.1: exact next step, never a vague failure.
108
152
  nextAction: unfinished.length > 0 || broken.length > 0
109
- ? 'An update transaction did not reach a terminal state. Review its phase, then continue, roll back, or repair through the official installer before starting a new update.'
153
+ ? activeRecordUnreadable
154
+ ? 'The active release record (active.json) could not be read, so no transaction can be proven superseded and every unfinished one is reported. Repair active.json (re-run the official installer recovery), then re-check recovery before starting a new update.'
155
+ : 'An update transaction did not reach a terminal state. Review its phase, then continue, roll back, or repair through the official installer before starting a new update.'
110
156
  : undefined,
111
157
  });
112
158
  }
@@ -745,6 +745,8 @@
745
745
  "confirm": "Confirm",
746
746
  "cancel": "Cancel",
747
747
  "rejectReasonPlaceholder": "Please explain why you reject (required)",
748
+ "approveNoteLabel": "Approval note (optional)",
749
+ "approveNotePlaceholder": "Why you approve — recorded as the decision note (may be left blank)",
748
750
  "editReasonLabel": "Edit reason (required)",
749
751
  "editReasonPlaceholder": "Explain why the wording or scope needs to change",
750
752
  "editNewArtifactPlaceholder": "Enter validated revision artifact ID",
@@ -745,6 +745,8 @@
745
745
  "confirm": "确认",
746
746
  "cancel": "取消",
747
747
  "rejectReasonPlaceholder": "请说明拒绝原因(必填)",
748
+ "approveNoteLabel": "批准备注(可选)",
749
+ "approveNotePlaceholder": "写下批准的理由,将作为裁决记录保存(可留空)",
748
750
  "editReasonLabel": "编辑原因(必填)",
749
751
  "editReasonPlaceholder": "请说明为什么需要调整措辞或范围",
750
752
  "editNewArtifactPlaceholder": "输入新的已验证工件 ID",
@@ -212,6 +212,7 @@ export function PrincipleDetailPage() {
212
212
  const [pendingAction, setPendingAction] = useState(null);
213
213
  const [rejectReason, setRejectReason] = useState("");
214
214
  const [newArtifactId, setNewArtifactId] = useState("");
215
+ const [approveNote, setApproveNote] = useState("");
215
216
  const [actionLoading, setActionLoading] = useState(false);
216
217
  // Evidence ID copy state (Wave 6: stop showing raw UUIDs as primary content)
217
218
  const [evidenceCopied, setEvidenceCopied] = useState(false);
@@ -345,7 +346,7 @@ export function PrincipleDetailPage() {
345
346
  // The view's actions are advisory; every submission goes through the real
346
347
  // mutation service. On failure: show the service reason, invalidate stale
347
348
  // actions, re-GET the view. Never auto-retry a mutation.
348
- const submitOwnerAction = async (action, reason) => {
349
+ const submitOwnerAction = async (action, reason, note) => {
349
350
  if (actionLoading)
350
351
  return;
351
352
  setActionLoading(true);
@@ -364,7 +365,7 @@ export function PrincipleDetailPage() {
364
365
  toast.error(t("principles.detail.ownerDecision.invalidTarget", { defaultValue: "动作目标缺失,无法提交。" }));
365
366
  return;
366
367
  }
367
- const approveResult = await approveApproval(approvalId);
368
+ const approveResult = await approveApproval(approvalId, note);
368
369
  settle(approveResult);
369
370
  // PRI-890: non-fatal server warnings (e.g. prompt injection budget
370
371
  // exclusion) must reach the Owner on this path too, not vanish.
@@ -419,6 +420,7 @@ export function PrincipleDetailPage() {
419
420
  setPendingAction(null);
420
421
  setRejectReason("");
421
422
  setNewArtifactId("");
423
+ setApproveNote("");
422
424
  // Invalidate the stale action list and re-derive from a fresh GET.
423
425
  await loadData();
424
426
  }
@@ -460,13 +462,15 @@ export function PrincipleDetailPage() {
460
462
  return (_jsxs("li", { children: [t(stateKey, { defaultValue: subject.state }), decidedAt !== undefined ? ` · ${decidedAt.slice(0, 10)}` : '', ' · ', t(`principles.detail.ownerDecision.channel.${subject.channel}`, { defaultValue: subject.channel })] }, `hist-${subject.key}`));
461
463
  }) })] })), _jsxs("div", { className: "mt-5 border-t border-line pt-4", "data-testid": "owner-decision-actions", children: [_jsx("p", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4", children: t("principles.detail.ownerDecision.decision", { defaultValue: "你的决定" }) }), ownerDecision.availableActions.length === 0 && (_jsx("p", { className: "mt-1 text-ink-3 text-[13px]", children: t("principles.detail.ownerDecision.noActions", { defaultValue: "当前没有可以执行的操作。" }) })), ownerDecision.decisionSubjects.filter((subject) => subject.state === 'pending').length > 0 && (_jsx("div", { className: "mt-3 space-y-3", "data-testid": "owner-decision-subject-materials", children: ownerDecision.decisionSubjects.filter((subject) => subject.state === 'pending').map((subject) => (_jsxs("div", { className: "rounded-[var(--radius-sm)] border border-line p-3", "data-subject-key": subject.key, children: [_jsx("p", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4", children: t("principles.detail.ownerDecision.subjectMaterial", { defaultValue: "这项决定的材料" }) }), subject.decisionMaterial.learnedPrinciple.status === 'known' ? (subject.decisionMaterial.learnedPrinciple.value.map((item, index) => (_jsx("p", { className: "mt-1 text-ink-2 text-[13px] leading-relaxed", children: item.text }, `sm-${index}`)))) : (_jsx("p", { className: "mt-1 text-amber text-[13px]", children: subject.decisionMaterial.learnedPrinciple.reason.ownerText })), subject.decisionMaterial.consequence.status === 'known' && (_jsx("p", { className: "mt-1 text-ink-3 text-[12px] leading-relaxed", children: subject.decisionMaterial.consequence.value.map((item) => item.text).join(' ') }))] }, subject.key))) })), _jsx("div", { className: "mt-2 flex flex-wrap gap-3", children: ownerDecision.availableActions.map((action) => (_jsx(Button, { variant: action.semantic === 'reject' ? 'destructive' : action.semantic === 'disable' ? 'outline' : 'default', size: "sm", disabled: actionLoading, "data-action-semantic": action.semantic, onClick: () => {
462
464
  setRejectReason("");
465
+ setApproveNote("");
463
466
  setPendingAction(action);
464
- }, children: localizeActionLabel(action, t) }, action.key))) }), ownerDecision.blockers.length > 0 && (_jsx("ul", { className: "mt-3 space-y-1 text-[12px] text-ink-3", children: ownerDecision.blockers.map((blocker, index) => (_jsx("li", { "data-testid": "owner-decision-blocker", children: localizeAggregatedBlocker(blocker.reason, t) }, `blk-${index}`))) })), pendingAction !== null && (_jsxs("div", { className: "border-t border-line pt-6 mt-6", "data-testid": "owner-decision-confirm", children: [_jsx(SectionTitle, { children: pendingAction.label }), _jsxs("div", { className: "mt-2 p-3 bg-gov/5 border border-gov/20 rounded-[var(--radius-md)]", children: [pendingAction.expectedConsequence.status === 'known' && (_jsx("div", { className: "mb-3", children: pendingAction.expectedConsequence.value.map((item, index) => (_jsx("p", { className: "text-ink-2 text-[13px] leading-relaxed mb-1", children: item.text }, `exp-${index}`))) })), _jsx("p", { className: "text-ink-2 text-[13px] mb-3", children: pendingAction.confirmation.text }), pendingAction.requirements.some((req) => req.name === 'newArtifactId') && (_jsxs(_Fragment, { children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("principles.detail.editNewArtifactLabel", { defaultValue: "新的已验证工件 ID" }) }), _jsx("input", { type: "text", value: newArtifactId, onChange: (e) => setNewArtifactId(e.target.value), placeholder: t("principles.detail.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] mb-2 font-mono focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov", "aria-label": t("principles.detail.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }) })] })), pendingAction.requirements.some((req) => req.name === 'reason') && (_jsxs(_Fragment, { children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("principles.detail.rejectReasonLabel", { defaultValue: "拒绝原因" }) }), _jsx("textarea", { value: rejectReason, onChange: (e) => setRejectReason(e.target.value), placeholder: t("principles.detail.rejectReasonPlaceholder"), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov", "aria-label": t("principles.detail.rejectReasonPlaceholder") })] })), _jsxs("div", { className: "flex gap-2 mt-3", children: [_jsx(Button, { variant: pendingAction.semantic === 'reject' ? 'destructive' : 'default', size: "sm", disabled: actionLoading
467
+ }, children: localizeActionLabel(action, t) }, action.key))) }), ownerDecision.blockers.length > 0 && (_jsx("ul", { className: "mt-3 space-y-1 text-[12px] text-ink-3", children: ownerDecision.blockers.map((blocker, index) => (_jsx("li", { "data-testid": "owner-decision-blocker", children: localizeAggregatedBlocker(blocker.reason, t) }, `blk-${index}`))) })), pendingAction !== null && (_jsxs("div", { className: "border-t border-line pt-6 mt-6", "data-testid": "owner-decision-confirm", children: [_jsx(SectionTitle, { children: pendingAction.label }), _jsxs("div", { className: "mt-2 p-3 bg-gov/5 border border-gov/20 rounded-[var(--radius-md)]", children: [pendingAction.expectedConsequence.status === 'known' && (_jsx("div", { className: "mb-3", children: pendingAction.expectedConsequence.value.map((item, index) => (_jsx("p", { className: "text-ink-2 text-[13px] leading-relaxed mb-1", children: item.text }, `exp-${index}`))) })), _jsx("p", { className: "text-ink-2 text-[13px] mb-3", children: pendingAction.confirmation.text }), pendingAction.requirements.some((req) => req.name === 'newArtifactId') && (_jsxs(_Fragment, { children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("principles.detail.editNewArtifactLabel", { defaultValue: "新的已验证工件 ID" }) }), _jsx("input", { type: "text", value: newArtifactId, onChange: (e) => setNewArtifactId(e.target.value), placeholder: t("principles.detail.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] mb-2 font-mono focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov", "aria-label": t("principles.detail.editNewArtifactPlaceholder", { defaultValue: "输入新的已验证工件 ID" }) })] })), pendingAction.requirements.some((req) => req.name === 'reason') && (_jsxs(_Fragment, { children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("principles.detail.rejectReasonLabel", { defaultValue: "拒绝原因" }) }), _jsx("textarea", { value: rejectReason, onChange: (e) => setRejectReason(e.target.value), placeholder: t("principles.detail.rejectReasonPlaceholder"), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov", "aria-label": t("principles.detail.rejectReasonPlaceholder") })] })), pendingAction.requirements.some((req) => req.name === 'note') && (_jsxs(_Fragment, { children: [_jsx("label", { className: "block font-mono text-[11px] uppercase tracking-[0.08em] text-ink-3 mb-2", children: t("principles.detail.approveNoteLabel", { defaultValue: "批准备注(可选)" }) }), _jsx("textarea", { "data-testid": "owner-decision-note", value: approveNote, onChange: (e) => setApproveNote(e.target.value), placeholder: pendingAction.requirements.find((req) => req.name === 'note')?.ownerText
468
+ ?? t("principles.detail.approveNotePlaceholder", { defaultValue: "写下批准的理由(可留空)" }), className: "w-full border border-line rounded-[var(--radius-md)] bg-surface text-ink px-3 py-2 text-[13px] min-h-[80px] resize-y focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gov", "aria-label": t("principles.detail.approveNoteLabel", { defaultValue: "批准备注(可选)" }) })] })), _jsxs("div", { className: "flex gap-2 mt-3", children: [_jsx(Button, { variant: pendingAction.semantic === 'reject' ? 'destructive' : 'default', size: "sm", disabled: actionLoading
465
469
  || (pendingAction.requirements.some((req) => req.name === 'reason') && !rejectReason.trim())
466
470
  || (pendingAction.requirements.some((req) => req.name === 'newArtifactId') && !newArtifactId.trim()), onClick: () => {
467
471
  const action = pendingAction;
468
- void submitOwnerAction(action, rejectReason.trim() === '' ? undefined : rejectReason.trim());
469
- }, children: t("principles.detail.confirm", { defaultValue: "确认" }) }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => { setPendingAction(null); setRejectReason(""); setNewArtifactId(""); }, children: t("principles.detail.cancel") })] })] })] }))] })] })] })), ownerDecision === null && ownerDecisionUnavailable !== null && (_jsxs("section", { className: "mb-8", "data-testid": "owner-decision-unavailable", children: [_jsx(SectionTitle, { children: t("principles.detail.ownerDecision.title", { defaultValue: "这条原则的决策视图" }) }), _jsxs("div", { className: "rounded-[var(--radius-md)] border border-amber/30 bg-amber/5 p-4", role: "status", children: [_jsx("p", { className: "text-ink-2 text-[13px]", children: t("principles.detail.ownerDecision.unavailable", { defaultValue: "决策视图暂时不可用,无法据此做决定。" }) }), _jsx("p", { className: "mt-1 text-ink-4 text-[12px] font-mono", children: ownerDecisionUnavailable.reason })] })] })), (governance !== null || governanceUnavailable !== null) && (_jsxs("section", { className: "mb-8", "aria-labelledby": "governance-summary-title", children: [_jsx(SectionTitle, { children: t('principles.detail.governance.title') }), governance !== null ? (_jsxs("div", { "data-testid": "governance-summary", className: `rounded-[var(--radius-md)] border p-4 ${governance.dataQuality.degraded ? 'border-amber/30 bg-amber/5' : 'border-gov/25 bg-gov/5'}`, children: [_jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { children: [_jsx("h2", { id: "governance-summary-title", className: "text-[18px] font-semibold text-ink", children: t(`principles.detail.${governance.summary.headlineCode}`, { defaultValue: governance.summary.headlineCode }) }), _jsx("p", { className: "mt-1 text-[13px] leading-relaxed text-ink-2", children: t(`principles.detail.${governance.summary.reasonCode}`, { defaultValue: governance.summary.reasonCode }) })] }), _jsx("span", { className: "w-fit rounded-full border border-line px-2 py-1 font-mono text-[11px] text-ink-3", children: t(`principles.detail.governance.confidence.${governance.dataQuality.degraded ? 'degraded' : 'strong'}`) })] }), _jsxs("dl", { className: "mt-4 grid grid-cols-1 gap-3 sm:grid-cols-3", children: [_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-[11px] text-ink-4", children: t('principles.detail.governance.principleState') }), _jsx("dd", { className: "text-[13px] text-ink-2", children: t(`principles.detail.governance.state.${governance.principleState.value}`) })] }), _jsxs("div", { children: [_jsx("dt", { className: "font-mono text-[11px] text-ink-4", children: t('principles.detail.governance.process') }), _jsx("dd", { className: "text-[13px] text-ink-2", children: governance.process.stage === undefined ? t('principles.detail.governance.none') : t(`principles.detail.governance.stage.${governance.process.stage}`) })] }), _jsxs("div", { children: [_jsx("dt", { className: "font-mono text-[11px] text-ink-4", children: t('principles.detail.governance.automation') }), _jsx("dd", { className: "text-[13px] text-ink-2", children: t(`principles.detail.governance.automationState.${governance.automation.state}`) })] })] }), _jsxs("div", { "data-testid": "governance-next-action", className: "mt-4 border-l-2 border-l-gov pl-3", children: [_jsx("p", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4", children: t('principles.detail.governance.nextAction') }), _jsx("p", { className: "text-[13px] text-ink-2", children: t(`principles.detail.${governance.summary.nextActionCode}`, { defaultValue: governance.summary.nextActionCode }) })] }), governance.attention.items.length > 0 && (_jsxs("div", { className: "mt-4", children: [_jsx("p", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4", children: t('principles.detail.governance.blockers') }), _jsx("ul", { className: "mt-1 space-y-1 text-[13px] text-ink-2", children: groupByReasonCode(governance.attention.items).map(group => _jsx("li", { children: formatGroupedReasonLine(t, 'principles.detail.governance.attention', group) }, group.reasonCode)) })] })), governance.dataQuality.degraded && (_jsxs("div", { "data-testid": "governance-data-quality", className: "mt-4 rounded-[var(--radius-sm)] border border-amber/20 p-3", children: [_jsx("p", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-amber", children: t('principles.detail.governance.uncertainty') }), _jsx("ul", { className: "mt-1 space-y-1 text-[12px] text-ink-3", children: groupByReasonCode(governance.dataQuality.issues).map(group => _jsx("li", { children: formatGroupedReasonLine(t, 'principles.detail.governance.issue', group) }, group.reasonCode)) })] }))] })) : (_jsxs("div", { "data-testid": "governance-data-quality", className: "rounded-[var(--radius-md)] border border-amber/30 bg-amber/5 p-4", role: "status", children: [_jsx("h2", { id: "governance-summary-title", className: "text-[16px] font-semibold text-ink", children: t('principles.detail.governance.unavailable') }), _jsx("p", { className: "mt-1 text-[13px] text-ink-2", children: t('principles.detail.governance.unavailableReason') }), _jsx("p", { className: "mt-2 text-[12px] text-ink-3", children: t('principles.detail.governance.unavailableNextAction') })] }))] })), receipts !== null && (_jsxs("section", { className: "mb-8", "aria-labelledby": "receipt-history-title", children: [_jsx(SectionTitle, { children: t('principles.detail.receipts.title') }), receipts.status === 'ok' ? (_jsxs("div", { "data-testid": "receipt-history", className: "rounded-[var(--radius-md)] border border-line p-4", children: [_jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-2", children: [_jsx("h2", { id: "receipt-history-title", className: "text-[15px] font-semibold text-ink", children: receiptPresentation?.headlineKey === 'principles.detail.receipts.headline'
472
+ void submitOwnerAction(action, rejectReason.trim() === '' ? undefined : rejectReason.trim(), approveNote.trim() === '' ? undefined : approveNote.trim());
473
+ }, children: t("principles.detail.confirm", { defaultValue: "确认" }) }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => { setPendingAction(null); setRejectReason(""); setNewArtifactId(""); setApproveNote(""); }, children: t("principles.detail.cancel") })] })] })] }))] })] })] })), ownerDecision === null && ownerDecisionUnavailable !== null && (_jsxs("section", { className: "mb-8", "data-testid": "owner-decision-unavailable", children: [_jsx(SectionTitle, { children: t("principles.detail.ownerDecision.title", { defaultValue: "这条原则的决策视图" }) }), _jsxs("div", { className: "rounded-[var(--radius-md)] border border-amber/30 bg-amber/5 p-4", role: "status", children: [_jsx("p", { className: "text-ink-2 text-[13px]", children: t("principles.detail.ownerDecision.unavailable", { defaultValue: "决策视图暂时不可用,无法据此做决定。" }) }), _jsx("p", { className: "mt-1 text-ink-4 text-[12px] font-mono", children: ownerDecisionUnavailable.reason })] })] })), (governance !== null || governanceUnavailable !== null) && (_jsxs("section", { className: "mb-8", "aria-labelledby": "governance-summary-title", children: [_jsx(SectionTitle, { children: t('principles.detail.governance.title') }), governance !== null ? (_jsxs("div", { "data-testid": "governance-summary", className: `rounded-[var(--radius-md)] border p-4 ${governance.dataQuality.degraded ? 'border-amber/30 bg-amber/5' : 'border-gov/25 bg-gov/5'}`, children: [_jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { children: [_jsx("h2", { id: "governance-summary-title", className: "text-[18px] font-semibold text-ink", children: t(`principles.detail.${governance.summary.headlineCode}`, { defaultValue: governance.summary.headlineCode }) }), _jsx("p", { className: "mt-1 text-[13px] leading-relaxed text-ink-2", children: t(`principles.detail.${governance.summary.reasonCode}`, { defaultValue: governance.summary.reasonCode }) })] }), _jsx("span", { className: "w-fit rounded-full border border-line px-2 py-1 font-mono text-[11px] text-ink-3", children: t(`principles.detail.governance.confidence.${governance.dataQuality.degraded ? 'degraded' : 'strong'}`) })] }), _jsxs("dl", { className: "mt-4 grid grid-cols-1 gap-3 sm:grid-cols-3", children: [_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-[11px] text-ink-4", children: t('principles.detail.governance.principleState') }), _jsx("dd", { className: "text-[13px] text-ink-2", children: t(`principles.detail.governance.state.${governance.principleState.value}`) })] }), _jsxs("div", { children: [_jsx("dt", { className: "font-mono text-[11px] text-ink-4", children: t('principles.detail.governance.process') }), _jsx("dd", { className: "text-[13px] text-ink-2", children: governance.process.stage === undefined ? t('principles.detail.governance.none') : t(`principles.detail.governance.stage.${governance.process.stage}`) })] }), _jsxs("div", { children: [_jsx("dt", { className: "font-mono text-[11px] text-ink-4", children: t('principles.detail.governance.automation') }), _jsx("dd", { className: "text-[13px] text-ink-2", children: t(`principles.detail.governance.automationState.${governance.automation.state}`) })] })] }), _jsxs("div", { "data-testid": "governance-next-action", className: "mt-4 border-l-2 border-l-gov pl-3", children: [_jsx("p", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4", children: t('principles.detail.governance.nextAction') }), _jsx("p", { className: "text-[13px] text-ink-2", children: t(`principles.detail.${governance.summary.nextActionCode}`, { defaultValue: governance.summary.nextActionCode }) })] }), governance.attention.items.length > 0 && (_jsxs("div", { className: "mt-4", children: [_jsx("p", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4", children: t('principles.detail.governance.blockers') }), _jsx("ul", { className: "mt-1 space-y-1 text-[13px] text-ink-2", children: groupByReasonCode(governance.attention.items).map(group => _jsx("li", { children: formatGroupedReasonLine(t, 'principles.detail.governance.attention', group) }, group.reasonCode)) })] })), governance.dataQuality.degraded && (_jsxs("div", { "data-testid": "governance-data-quality", className: "mt-4 rounded-[var(--radius-sm)] border border-amber/20 p-3", children: [_jsx("p", { className: "font-mono text-[11px] uppercase tracking-[0.08em] text-amber", children: t('principles.detail.governance.uncertainty') }), _jsx("ul", { className: "mt-1 space-y-1 text-[12px] text-ink-3", children: groupByReasonCode(governance.dataQuality.issues).map(group => _jsx("li", { children: formatGroupedReasonLine(t, 'principles.detail.governance.issue', group) }, group.reasonCode)) })] }))] })) : (_jsxs("div", { "data-testid": "governance-data-quality", className: "rounded-[var(--radius-md)] border border-amber/30 bg-amber/5 p-4", role: "status", children: [_jsx("h2", { id: "governance-summary-title", className: "text-[16px] font-semibold text-ink", children: t('principles.detail.governance.unavailable') }), _jsx("p", { className: "mt-1 text-[13px] text-ink-2", children: t('principles.detail.governance.unavailableReason') }), _jsx("p", { className: "mt-2 text-[12px] text-ink-3", children: t('principles.detail.governance.unavailableNextAction') })] }))] })), receipts !== null && (_jsxs("section", { className: "mb-8", "aria-labelledby": "receipt-history-title", children: [_jsx(SectionTitle, { children: t('principles.detail.receipts.title') }), receipts.status === 'ok' ? (_jsxs("div", { "data-testid": "receipt-history", className: "rounded-[var(--radius-md)] border border-line p-4", children: [_jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-2", children: [_jsx("h2", { id: "receipt-history-title", className: "text-[15px] font-semibold text-ink", children: receiptPresentation?.headlineKey === 'principles.detail.receipts.headline'
470
474
  ? t(receiptPresentation.headlineKey, {
471
475
  defaultValue: '',
472
476
  effectCount: receipts.effectCount,
@@ -874,6 +874,10 @@ export interface UpdateRecoveryItem {
874
874
  productVersion: string | null;
875
875
  tornTailDetected: boolean;
876
876
  }
877
+ export interface UpdateRecoverySuperseded {
878
+ transactionId: string;
879
+ reason: string;
880
+ }
877
881
  export interface UpdateRecoveryData {
878
882
  needsRecovery: boolean;
879
883
  unfinished: UpdateRecoveryItem[];
@@ -881,6 +885,18 @@ export interface UpdateRecoveryData {
881
885
  transactionId: string;
882
886
  reason: string;
883
887
  }[];
888
+ /**
889
+ * PRI-896: non-terminal journals the live release has already superseded.
890
+ * Reported separately (rc-9: never a silent drop) but never hold the alarm.
891
+ */
892
+ superseded: UpdateRecoverySuperseded[];
893
+ /**
894
+ * rc-9 (PRI-896/CR-2): active.json could not be read, so recovery had no
895
+ * live-release evidence and every non-terminal journal is conservatively an
896
+ * alarm. When true the alarm is retained for lack of a readable active
897
+ * record — repair active.json rather than the journals.
898
+ */
899
+ activeRecordUnreadable?: boolean;
884
900
  nextAction?: string;
885
901
  }
886
902
  export declare function validateUpdateRecovery(v: unknown): UpdateRecoveryData | null;
@@ -2414,7 +2414,20 @@ export function validateUpdateRecovery(v) {
2414
2414
  broken.push({ transactionId: item.transactionId, reason: item.reason });
2415
2415
  }
2416
2416
  }
2417
- const result = { needsRecovery: v.needsRecovery, unfinished, broken };
2417
+ const superseded = [];
2418
+ if (Object.hasOwn(v, 'superseded') && Array.isArray(v.superseded)) {
2419
+ for (const item of v.superseded) {
2420
+ if (!isObject(item) || !Object.hasOwn(item, 'transactionId') || !isString(item.transactionId))
2421
+ continue;
2422
+ if (!Object.hasOwn(item, 'reason') || !isString(item.reason))
2423
+ continue;
2424
+ superseded.push({ transactionId: item.transactionId, reason: item.reason });
2425
+ }
2426
+ }
2427
+ const result = { needsRecovery: v.needsRecovery, unfinished, broken, superseded };
2428
+ if (Object.hasOwn(v, 'activeRecordUnreadable') && typeof v.activeRecordUnreadable === 'boolean') {
2429
+ result.activeRecordUnreadable = v.activeRecordUnreadable;
2430
+ }
2418
2431
  if (Object.hasOwn(v, 'nextAction') && isString(v.nextAction))
2419
2432
  result.nextAction = v.nextAction;
2420
2433
  return result;