create-principles-disciple 1.144.22 → 1.144.24

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "productVersion": "2.3.0",
4
- "sourceCommit": "693e91ab726949844986dcbc2adaa27a5ef53a31"
4
+ "sourceCommit": "041cdc0a268119dbfba5819e7648a2016f9189b0"
5
5
  }
@@ -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;