@quolu/lattice 0.36.0 → 0.37.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/src/todo-cli.mjs CHANGED
@@ -416,6 +416,26 @@ function mutationActor(env) {
416
416
  return { host: entries[0].value, session: entries[1].value, agent: entries[2].value };
417
417
  }
418
418
 
419
+ /**
420
+ * gate_readyの札が出た理由と次の一歩をtypedに言う(ADR 0148裁定8)。
421
+ *
422
+ * 0.36.0で入れた終端監査gate(ADR 0147)は、これから終わる工程だけでなく過去に完了した
423
+ * 工程まで監査待ちにしてしまっていた——原因を言わないgateは、なぜ止まっているかを
424
+ * 説明しないのと同じなので、ここでその経緯を明示する。次の一歩は2択で書く:
425
+ * 今から監査する(review→accept)か、監査せず歴史として閉じる(close-unaudited、
426
+ * 複数plan一括ならbaseline)か。
427
+ */
428
+ function auditGateGuidance(planKey, phaseId) {
429
+ return '全ToDoがdoneになり監査待ち(gate_ready)。0.36.0で入れた終端監査gateは、'
430
+ + 'これから終わる工程だけでなく過去に完了した工程まで監査待ちにしてしまっていた'
431
+ + '(ADR 0148で修正)。今から監査するなら: '
432
+ + `todo phase review --plan ${planKey} --phase ${phaseId} --reason <text> → `
433
+ + `todo phase accept --plan ${planKey} --phase ${phaseId} --input <file>。`
434
+ + '監査せず歴史として閉じるなら: '
435
+ + `todo phase close-unaudited --plan ${planKey} --phase ${phaseId} --reason <text>`
436
+ + '(複数planをまとめて畳むなら todo phase baseline --reason <text> [--except <plan_key>]...)。';
437
+ }
438
+
419
439
  /**
420
440
  * phase無しplanで、この変異の結果terminal-audit Phaseがgate_ready(全task done・未監査)に
421
441
  * なっていれば助言を返す(ADR 0147)。doneの結果だけを見て機械的に判定するので、既にreview
@@ -429,9 +449,7 @@ function terminalAuditDoneAdvisory(plan, phases) {
429
449
  if (phase?.status !== 'gate_ready') return null;
430
450
  return {
431
451
  terminal_audit_required: true, phase_id: TERMINAL_AUDIT_PHASE_ID, status: phase.status,
432
- guidance: '全taskがdoneになった。このplanはphaseを持たないため、終端の重監査'
433
- + '(todo phase review --plan <key> --phase terminal-audit → todo phase accept)を'
434
- + '経るまで「閉じた」ことにはならない。',
452
+ guidance: auditGateGuidance(plan.plan_key, TERMINAL_AUDIT_PHASE_ID),
435
453
  };
436
454
  }
437
455
 
@@ -705,11 +723,110 @@ async function phaseStatus({ repoRoot, planKey }) {
705
723
  // 無いのでsnapshot.phasesは直接読まない)。ここでPHASE_UNAVAILABLEへ拒否せず、その暗黙Phase
706
724
  // をそのまま返す——`implicit`で機械可読に「宣言されたPhaseではない」ことを示す。
707
725
  const implicit = isPhaselessTodoPlanSchema(member.plan.schema);
726
+ // ADR 0148裁定8: gate_readyのPhaseには、なぜ監査待ちになったかと次の一歩をここでも言う
727
+ // (todo doneのadvisoryと同じ文言)。implicit(暗黙のterminal-audit Phase)に限らず、
728
+ // v4/v5の実Phaseがgate_readyになった場合も同じ理由で同じ案内が要る。
729
+ const phases = member.phases.map((phase) => ({
730
+ ...phase,
731
+ guidance: phase.status === 'gate_ready'
732
+ ? auditGateGuidance(member.plan.plan_key, phase.phase_id) : null,
733
+ }));
708
734
  const result = {
709
735
  schema: 'lattice.phase_status_result.v1', project_id: store.project_id,
710
736
  plan_key: member.plan.plan_key, plan_version: member.plan.plan_version,
711
737
  journal_head_digest: member.journal.events.at(-1).event_digest,
712
- implicit, phases: member.phases, result_digest: '',
738
+ implicit, phases, result_digest: '',
739
+ };
740
+ result.result_digest = todoSelfDigest(result, 'result_digest');
741
+ return result;
742
+ }
743
+
744
+ /**
745
+ * `phase baseline --reason <text> [--except <plan_key>]...`の可変長`--except`を解析する。
746
+ * 他のargvはすべて固定位置(argv.length + 位置一致)で判定しているが、0回以上繰り返せる
747
+ * flagだけは固定位置で表現できないため、並び全体を見る専用ヘルパへ切り出す
748
+ * (bridge-cliの`parseOptions`と同じ発想)。不正な並びはnullを返し、呼び出し側の
749
+ * 分岐条件がそのままusageFailureへ落ちる。
750
+ */
751
+ function parseBaselineExceptFlags(rest) {
752
+ if (rest.length % 2 !== 0) return null;
753
+ const planKeys = [];
754
+ for (let index = 0; index < rest.length; index += 2) {
755
+ if (rest[index] !== '--except' || !isTodoIdentifier(rest[index + 1])) return null;
756
+ planKeys.push(rest[index + 1]);
757
+ }
758
+ return planKeys;
759
+ }
760
+
761
+ /**
762
+ * `phase baseline`——現在gate_readyかつphase eventを1つも持たないPhaseを一括で
763
+ * closed_unauditedへ宣言する(ADR 0148裁定6)。「phase eventを1つも持たない」は
764
+ * derivedStatusだけでは判定できない——一度reviewしてからreopenしたPhaseは、構造的には
765
+ * また`gate_ready`に戻り得るが、既に監査に触れている以上は対象外である。journalの実event
766
+ * 履歴(phase_id別に`phase_`で始まるkindがあるか)を直接見て区別する。
767
+ *
768
+ * `--except`はplan単位の除外(ADR 0148裁定7)。ServerManagerの26 ToDoのような
769
+ * 「最近の作業でコードも生きている」campaignを基準線で流さないための口であり、
770
+ * Phase単位では絞れない(そのplanのPhaseは丸ごと対象外になる)。
771
+ */
772
+ async function phaseBaseline({ repoRoot, env, reason, exceptPlanKeys }) {
773
+ const store = await readTodoStore({ repoRoot });
774
+ const knownPlanKeys = new Set(store.members.map(({ descriptor }) => descriptor.plan_key));
775
+ const unknownExcept = exceptPlanKeys.filter((planKey) => !knownPlanKeys.has(planKey));
776
+ if (unknownExcept.length > 0) {
777
+ throw new TodoStoreError('PHASE_BASELINE_INVALID', 'except_plan_key_unknown', undefined, {
778
+ unknown_plan_keys: unknownExcept,
779
+ });
780
+ }
781
+ const exceptSet = new Set(exceptPlanKeys);
782
+ const applied = [];
783
+ const excluded = [];
784
+ const notApplicable = [];
785
+ const failed = [];
786
+ for (const member of store.members) {
787
+ const planKey = member.descriptor.plan_key;
788
+ const touchedPhaseIds = new Set(member.journal.events
789
+ .filter((event) => event.kind.startsWith('phase_'))
790
+ .map((event) => event.phase_id));
791
+ for (const phase of member.phases) {
792
+ const entry = { plan_key: planKey, phase_id: phase.phase_id, status: phase.status };
793
+ // 「対象外だったもの(既にaccepted等)も区別して返す」——まだgate_readyに到達していない
794
+ // (locked/active)のと、既に監査の決着が付いている(accepted/rejected/closed_unaudited)のは
795
+ // 原因が違うので、causeを分けて機械可読にする。
796
+ if (['accepted', 'rejected', 'closed_unaudited'].includes(phase.status)) {
797
+ notApplicable.push({ ...entry, cause: `already_${phase.status}` });
798
+ } else if (phase.status !== 'gate_ready') {
799
+ notApplicable.push({ ...entry, cause: 'not_gate_ready' });
800
+ } else if (touchedPhaseIds.has(phase.phase_id)) {
801
+ notApplicable.push({ ...entry, cause: 'already_has_phase_event' });
802
+ } else if (exceptSet.has(planKey)) {
803
+ excluded.push({ ...entry, cause: 'excepted' });
804
+ } else {
805
+ // ADR 0148裁定6の非目標: journalはappend-onlyで、一度書いた宣言を後から取り消す
806
+ // 手段が無い。1件失敗しても既に書けた他件を無かったことにはできない以上、
807
+ // 全件一括のtransactionは新設せず、appendTodoEvent単位(1 event = 1排他書込み)の
808
+ // 独立実行を続ける。各書込みは呼ぶたびにstoreを読み直してgate_ready前提を
809
+ // 再検証するため、他項目の成否とは無関係に安全である——1件の失敗で残りを
810
+ // 止めず、続行して全件の結果をtypedで返す。
811
+ try {
812
+ const result = await phaseMutation({
813
+ repoRoot, env, planKey, phaseId: phase.phase_id,
814
+ kind: 'phase_close_unaudited', payload: { reason },
815
+ });
816
+ applied.push({ ...entry, event_digest: result.event_digest,
817
+ journal_head_digest: result.journal_head_digest });
818
+ } catch (error) {
819
+ failed.push({ ...entry, code: error?.code ?? 'INTERNAL_FAILURE',
820
+ message: typeof error?.message === 'string' ? error.message : null });
821
+ }
822
+ }
823
+ }
824
+ }
825
+ const result = {
826
+ schema: 'lattice.phase_baseline_result.v1',
827
+ reason, except_plan_keys: [...exceptSet].sort(),
828
+ applied, excluded, not_applicable: notApplicable, failed,
829
+ result_digest: '',
713
830
  };
714
831
  result.result_digest = todoSelfDigest(result, 'result_digest');
715
832
  return result;
@@ -2148,6 +2265,17 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
2148
2265
  && (argv.length === 8 || (argv[8] === '--override-reason' && argv[9].length > 0))) {
2149
2266
  action = (repoRoot) => phaseMutation({ repoRoot, env, planKey: argv[3], phaseId: argv[5],
2150
2267
  kind: 'phase_reopen', payload: { reason: argv[7], override_reason: argv[9] ?? null } });
2268
+ } else if (argv.length === 8 && argv[0] === 'phase' && argv[1] === 'close-unaudited'
2269
+ && argv[2] === '--plan' && isTodoIdentifier(argv[3])
2270
+ && argv[4] === '--phase' && isTodoIdentifier(argv[5])
2271
+ && argv[6] === '--reason' && argv[7].length > 0) {
2272
+ action = (repoRoot) => phaseMutation({ repoRoot, env, planKey: argv[3], phaseId: argv[5],
2273
+ kind: 'phase_close_unaudited', payload: { reason: argv[7] } });
2274
+ } else if (argv.length >= 4 && argv[0] === 'phase' && argv[1] === 'baseline'
2275
+ && argv[2] === '--reason' && argv[3].length > 0
2276
+ && parseBaselineExceptFlags(argv.slice(4)) !== null) {
2277
+ const exceptPlanKeys = parseBaselineExceptFlags(argv.slice(4));
2278
+ action = (repoRoot) => phaseBaseline({ repoRoot, env, reason: argv[3], exceptPlanKeys });
2151
2279
  } else if ((argv.length === 5 || argv.length === 6 || argv.length === 7 || argv.length === 8)
2152
2280
  && argv[0] === 'start'
2153
2281
  && argv[1] === '--plan' && isTodoIdentifier(argv[2])
@@ -4,6 +4,10 @@ import { isCanonicalUtcTimestamp } from './timestamp-contract.mjs';
4
4
  export const TODO_EVENT_KINDS = Object.freeze([
5
5
  'plan_genesis', 'start', 'block', 'unblock', 'done', 'reopen',
6
6
  'phase_review', 'phase_accept', 'phase_reject', 'phase_reopen',
7
+ // ADR 0148: 監査していない歴史を「監査なしで閉じた」として明示的に閉じるための専用kind。
8
+ // phase_review/accept/reject/reopenと同じv3 tail event shape(phase_id持ち)に収め、
9
+ // 新しいevent schema版は作らない。
10
+ 'phase_close_unaudited',
7
11
  ]);
8
12
  export const TODO_LIMITS = Object.freeze({
9
13
  tasksPerPlan: 512,
@@ -345,6 +349,10 @@ function validPayload(event) {
345
349
  'reason', 'target_decision_digest', 'override_reason',
346
350
  ]) && nullableText(payload.reason) && payload.reason !== null
347
351
  && isTodoDigest(payload.target_decision_digest) && nullableText(payload.override_reason);
352
+ // ADR 0148裁定1: 監査なしで閉じたことの理由は必須(payload.reason !== null)。証拠は無い
353
+ // ——監査していないというのが事実であり、evidenceを要求すると「監査した体」を装う経路になる。
354
+ if (event.kind === 'phase_close_unaudited') return exactRecord(payload, ['reason'])
355
+ && nullableText(payload.reason) && payload.reason !== null;
348
356
  return false;
349
357
  }
350
358
 
@@ -392,7 +400,8 @@ function validPhaseStateMigration(value) {
392
400
  && (entry.state_policy === 'reset' ? entry.state === null
393
401
  : exactRecord(entry.state, [
394
402
  'status', 'review_event_digest', 'decision_event_digest', 'decision_evidence',
395
- ]) && ['locked', 'active', 'gate_ready', 'reviewing', 'accepted', 'rejected'].includes(entry.state.status)
403
+ ]) && ['locked', 'active', 'gate_ready', 'reviewing', 'accepted', 'rejected', 'closed_unaudited']
404
+ .includes(entry.state.status)
396
405
  && nullableDigest(entry.state.review_event_digest)
397
406
  && nullableDigest(entry.state.decision_event_digest)
398
407
  && (entry.state.decision_evidence === null || evidence(entry.state.decision_evidence))))
@@ -421,7 +430,8 @@ export function validateTodoEvent(value) {
421
430
  ]) && value.kind === 'plan_genesis' && value.task_id === null && value.phase_id === null
422
431
  && isTodoDigest(value.revision_digest) && validStateMigration(value.state_migration)
423
432
  && validPhaseStateMigration(value.phase_state_migration);
424
- const phaseKind = ['phase_review', 'phase_accept', 'phase_reject', 'phase_reopen'].includes(value?.kind);
433
+ const phaseKind = ['phase_review', 'phase_accept', 'phase_reject', 'phase_reopen', 'phase_close_unaudited']
434
+ .includes(value?.kind);
425
435
  return (v1 || v2 || v3 || v4) && isTodoIdentifier(value.project_id)
426
436
  && isTodoIdentifier(value.plan_key) && isTodoIdentifier(value.plan_version)
427
437
  && isNonNegativeSafeInteger(value.sequence) && nullableDigest(value.previous_digest)
@@ -491,7 +501,8 @@ export function validateTodoSnapshot(value) {
491
501
  && value.phases.every((entry) => exactRecord(entry, [
492
502
  'phase_id', 'status', 'review_event_digest', 'decision_event_digest', 'decision_evidence',
493
503
  ]) && isTodoIdentifier(entry.phase_id)
494
- && ['locked', 'active', 'gate_ready', 'reviewing', 'accepted', 'rejected'].includes(entry.status)
504
+ && ['locked', 'active', 'gate_ready', 'reviewing', 'accepted', 'rejected', 'closed_unaudited']
505
+ .includes(entry.status)
495
506
  && nullableDigest(entry.review_event_digest) && nullableDigest(entry.decision_event_digest)
496
507
  && (entry.decision_evidence === null || evidence(entry.decision_evidence)))
497
508
  && value.phases.every((entry, index) => index === 0
@@ -217,12 +217,13 @@ async function readJournal(repoRoot, journalRef) {
217
217
  const phaseTail = (event) => event.schema === 'lattice.todo_event.v3';
218
218
  const legacyTail = (event) => event.schema === 'lattice.todo_event.v1';
219
219
  // ADR 0147: genesisがv1/v2(phase無しplan)でも、暗黙のterminal-audit Phaseへの
220
- // phase_review/phase_accept/phase_reject/phase_reopenだけはv3 tail eventとして
221
- // 混在を許す。task側のevent(start/done/block/unblock/reopen)は従来どおりv1のまま
220
+ // phase_review/phase_accept/phase_reject/phase_reopen/phase_close_unaudited(ADR 0148)だけは
221
+ // v3 tail eventとして混在を許す。task側のevent(start/done/block/unblock/reopen)は従来どおりv1のまま
222
222
  // ——既存planの既存event bytesは1つも変わらない。新しく増えるのは、これまで
223
223
  // phase無しplanには存在し得なかったphase_*event kindの受け皿だけである。
224
224
  const implicitTerminalAuditTail = (event) => phaseTail(event)
225
- && ['phase_review', 'phase_accept', 'phase_reject', 'phase_reopen'].includes(event.kind);
225
+ && ['phase_review', 'phase_accept', 'phase_reject', 'phase_reopen', 'phase_close_unaudited']
226
+ .includes(event.kind);
226
227
  const legacyOrImplicitPhaseTail = (event) => legacyTail(event) || implicitTerminalAuditTail(event);
227
228
  if ((phaseSchema && events.some(({ schema }, index) => index === 0
228
229
  ? !['lattice.todo_event.v3', 'lattice.todo_event.v4'].includes(schema) : !phaseTail(events[index])))
@@ -271,7 +272,11 @@ function phasesOf(plan) {
271
272
 
272
273
  function derivedPhaseStatus(plan, taskStates, phaseStates, phaseId) {
273
274
  const state = phaseStates.get(phaseId);
274
- if (['reviewing', 'accepted', 'rejected'].includes(state.status)) return state.status;
275
+ // ADR 0148: closed_unauditedも他の終端状態と同じく確定済みとして扱う。ここへ足さないと、
276
+ // 全taskがdone(=構造的にgate_ready)のままなので、以後の呼び出しで毎回gate_readyへ
277
+ // 再導出されてしまい、記録した「監査なしで閉じた」がgantt折り畳み・phase_accept_dependencies
278
+ // 判定の上で一瞬で消える。
279
+ if (['reviewing', 'accepted', 'rejected', 'closed_unaudited'].includes(state.status)) return state.status;
275
280
  const phase = phasesOf(plan).find((entry) => entry.phase_id === phaseId);
276
281
  if (!phase.predecessor_phase_ids.every((id) => phaseStates.get(id)?.status === 'accepted')) return 'locked';
277
282
  // 暗黙のterminal-audit Phaseはtask側にphase_idフィールドが無い(v1/v2/v3にはそもそも
@@ -303,6 +308,13 @@ function projectPhaseStates(plan, events, taskStates) {
303
308
  const state = states.get(event.phase_id);
304
309
  state.status = 'rejected'; state.decision_event_digest = event.event_digest;
305
310
  state.decision_evidence = event.payload.decision_evidence;
311
+ } else if (event.kind === 'phase_close_unaudited') {
312
+ // ADR 0148: 監査していないので証拠(decision_evidence)は残さない。理由はevent payload
313
+ // 自身(decision_event_digestで指せるこの event)に記録済みで、accept/rejectと違い
314
+ // ここに複製しない。
315
+ const state = states.get(event.phase_id);
316
+ state.status = 'closed_unaudited'; state.decision_event_digest = event.event_digest;
317
+ state.decision_evidence = null;
306
318
  } else if (event.kind === 'phase_reopen') {
307
319
  Object.assign(states.get(event.phase_id), emptyPhaseState(event.phase_id));
308
320
  }
@@ -428,8 +440,17 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
428
440
  if (verifyEvidence) verifyEvidence(event.payload.decision_evidence);
429
441
  state.status = 'rejected'; state.decision_event_digest = event.event_digest;
430
442
  state.decision_evidence = event.payload.decision_evidence;
443
+ } else if (event.kind === 'phase_close_unaudited') {
444
+ // ADR 0148裁定3: reviewと同じ前提(gate_ready)を課す——所属ToDoが全てdoneでない段階では
445
+ // まだ監査の地点に到達していないので、監査なしで閉じることもできない。
446
+ if (currentStatus !== 'gate_ready') fail('STORE_INCONSISTENT', 'phase_gate_not_ready');
447
+ state.status = 'closed_unaudited'; state.decision_event_digest = event.event_digest;
448
+ state.decision_evidence = null;
431
449
  } else {
432
- if (!['accepted', 'rejected'].includes(currentStatus)
450
+ // phase_reopen。ADR 0148裁定5: closed_unauditedもaccepted/rejectedと同じく
451
+ // reopenで初期状態へ戻せる——監査せずに閉じた工程を、後から本当に監査したくなった時に
452
+ // 永久に締め出さない。
453
+ if (!['accepted', 'rejected', 'closed_unaudited'].includes(currentStatus)
433
454
  || state.decision_event_digest !== event.payload.target_decision_digest) {
434
455
  fail('STORE_INCONSISTENT', 'phase_reopen_binding_invalid');
435
456
  }
@@ -516,14 +537,16 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
516
537
  fail('STORE_INCONSISTENT', 'invalid_reopen_binding');
517
538
  }
518
539
  {
519
- // 暗黙のterminal-audit Phaseがacceptedの後にtaskだけを無警告でreopenできると、
520
- // 監査済みのまま(gantt上も畳まれたまま)裏で作業が再開する抜け道になり、ADR 0147の
521
- // 「監査の記録なしに閉じたことにさせない」を潜脱する。v4/v5の既存Phaseと同じ規律を
522
- // 暗黙Phaseにも及ぼし、phase_reopenを先に通させる。
540
+ // 暗黙のterminal-audit Phaseがaccepted/closed_unauditedの後にtaskだけを無警告でreopen
541
+ // できると、監査済み(または監査なしで閉じた)まま(gantt上も畳まれたまま)裏で作業が
542
+ // 再開する抜け道になり、ADR 0147/0148の「監査の記録なしに閉じたことにさせない」を
543
+ // 潜脱する。v4/v5の既存Phaseと同じ規律を暗黙Phaseにも及ぼし、phase_reopenを先に
544
+ // 通させる。closed_unauditedをここへ足さないと、ADR 0148で新設した状態だけが
545
+ // このgateを素通りしてしまう。
523
546
  const phaseId = isPhaselessTodoPlanSchema(plan.schema)
524
547
  ? TERMINAL_AUDIT_PHASE_ID
525
548
  : plan.tasks.find(({ task_id }) => task_id === event.task_id).phase_id;
526
- if (derivedPhaseStatus(plan, states, phaseStates, phaseId) === 'accepted') {
549
+ if (['accepted', 'closed_unaudited'].includes(derivedPhaseStatus(plan, states, phaseStates, phaseId))) {
527
550
  fail('STORE_INCONSISTENT', 'task_reopen_requires_phase_reopen');
528
551
  }
529
552
  }
@@ -1174,7 +1197,8 @@ function nextEvent(input, storeMember) {
1174
1197
  ? { done_mode: 'authored', imported: false, evidence: input.payload.evidence }
1175
1198
  : input.payload;
1176
1199
  const phaseCapablePlan = ['lattice.todo_plan.v4', 'lattice.todo_plan.v5'].includes(storeMember.plan.schema);
1177
- const phaseKind = ['phase_review', 'phase_accept', 'phase_reject', 'phase_reopen'].includes(input.kind);
1200
+ const phaseKind = ['phase_review', 'phase_accept', 'phase_reject', 'phase_reopen', 'phase_close_unaudited']
1201
+ .includes(input.kind);
1178
1202
  // ADR 0147: phase無しplanでも、暗黙のterminal-audit Phaseへのphase_*eventだけは
1179
1203
  // v3 tail eventとして書く(readJournalのlegacyOrImplicitPhaseTailが同じ規則で受理する)。
1180
1204
  // task側のevent(start/done等)はphase無しplanのままv1で書き続ける——既存の
@@ -1246,8 +1270,12 @@ function resolveTargetedEvent(input, storeMember) {
1246
1270
  };
1247
1271
  }
1248
1272
  if (input.kind === 'phase_reopen' && exactRecord(input.payload, ['reason', 'override_reason'])) {
1273
+ // ADR 0148裁定5: closed_unauditedもaccept/rejectと同じ「決定event」なので、reopenの
1274
+ // 結びつけ先として同列に探す。ここへ足し忘れると、監査なしで閉じたPhaseをreopenする
1275
+ // 手段が無くなる(target_decision_digestを解決できずphase_reopen_binding_invalidで拒否される)。
1249
1276
  const target = [...storeMember.journal.events].reverse().find((event) => (
1250
- ['phase_accept', 'phase_reject'].includes(event.kind) && event.phase_id === input.phase_id
1277
+ ['phase_accept', 'phase_reject', 'phase_close_unaudited'].includes(event.kind)
1278
+ && event.phase_id === input.phase_id
1251
1279
  ));
1252
1280
  if (target === undefined) fail('STORE_INCONSISTENT', 'phase_reopen_binding_invalid');
1253
1281
  return { ...input, payload: { ...input.payload, target_decision_digest: target.event_digest } };