@quolu/lattice 0.65.0 → 0.66.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.65.0",
3
+ "version": "0.66.1",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
package/src/cli-help.mjs CHANGED
@@ -102,6 +102,7 @@ Write commands:
102
102
  # --serial-confirmedは互換のため受理する。いずれも門ではない
103
103
  block --plan <key> --task <id> --reason <text>
104
104
  unblock --plan <key> --task <id>
105
+ retire --plan <key> --task <id> --reason <text>
105
106
  done --plan <key> --task <id> (--evidence <file>|--message <text>)
106
107
  # taskを閉じる。--evidenceはdescriptor JSONでも証拠本文でもよい。repo内なら絶対path可。
107
108
  # 監査と構造finalizationは残作業であり、doneの門ではない
@@ -273,6 +274,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
273
274
  'todo retract': 'todo retract --plan <key> --task <id> --reason <text>',
274
275
  'todo block': 'todo block --plan <key> --task <id> --reason <text>',
275
276
  'todo unblock': 'todo unblock --plan <key> --task <id>',
277
+ 'todo retire': 'todo retire --plan <key> --task <id> --reason <text>',
276
278
  'todo done': 'todo done --plan <key> --task <id> (--evidence <file>|--message <text>) [--test-result <markdown-file>]',
277
279
  'todo reopen': 'todo reopen --plan <key> --task <id> --reason <text> [--override-reason <text>]',
278
280
  'todo evidence': 'todo evidence promote --plan <key> --task <id> --evidence <file>',
package/src/todo-cli.mjs CHANGED
@@ -256,7 +256,7 @@ function internalFailure(stderr, error) {
256
256
  const TODO_COMMAND_NAMES = Object.freeze([
257
257
  'status', 'show', 'note', 'bindings', 'independence', 'structure', 'seam-profile', 'seam-proposal',
258
258
  'verify', 'repair-eol', 'snapshot', 'gantt', 'dashboard', 'phase', 'migrate', 'start', 'block',
259
- 'unblock', 'done', 'reopen', 'evidence', 'split', 'revise', 'revise-phase', 'revise-set',
259
+ 'unblock', 'retire', 'done', 'reopen', 'evidence', 'split', 'revise', 'revise-phase', 'revise-set',
260
260
  ]);
261
261
 
262
262
  function typedArgumentFailure(stderr, code, message, detail) {
@@ -275,7 +275,7 @@ function supportsAtomicStoreCommit(argv) {
275
275
  if (command === 'snapshot') return argv[1] === '--rebuild';
276
276
  if (command === 'migrate') return !argv.includes('--dry-run') && !argv.includes('--schema');
277
277
  if (['revise', 'split', 'revise-set', 'revise-phase', 'start', 'retract', 'block',
278
- 'unblock', 'done', 'reopen'].includes(command)) return true;
278
+ 'unblock', 'retire', 'done', 'reopen'].includes(command)) return true;
279
279
  if (command === 'evidence') return argv[1] === 'promote';
280
280
  if (command === 'phase') return argv[1] !== 'status';
281
281
  return false;
@@ -3436,7 +3436,7 @@ function writesTodoStore(argv) {
3436
3436
  case 'evidence': return second === 'promote';
3437
3437
  case 'dependency': return second === 'connect';
3438
3438
  case 'phase': return second !== 'status';
3439
- case 'start': case 'retract': case 'block': case 'unblock': case 'done':
3439
+ case 'start': case 'retract': case 'block': case 'unblock': case 'retire': case 'done':
3440
3440
  case 'reopen': case 'split': case 'revise': case 'revise-phase': case 'revise-set':
3441
3441
  return true;
3442
3442
  default: return false;
@@ -3961,6 +3961,17 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
3961
3961
  kind: 'block', payload: { reason: flags.reason }, evidenceRef: null,
3962
3962
  });
3963
3963
  }
3964
+ } else if (argv[0] === 'retire') {
3965
+ // 恒久除去(2026-08-29オーナー裁定)。planの形は変えず、状態遷移として工程を閉じる。
3966
+ const flags = matchFlagCommand(argv, ['retire'], {
3967
+ known: ['plan', 'task', 'reason'], required: ['plan', 'task', 'reason'],
3968
+ });
3969
+ if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.task)) {
3970
+ action = (repoRoot) => mutate({
3971
+ repoRoot, env, planKey: flags.plan, taskId: flags.task,
3972
+ kind: 'retire', payload: { reason: flags.reason }, evidenceRef: null,
3973
+ });
3974
+ }
3964
3975
  } else if (argv[0] === 'unblock') {
3965
3976
  const flags = matchFlagCommand(argv, ['unblock'], {
3966
3977
  known: ['plan', 'task'], required: ['plan', 'task'],
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { isCanonicalUtcTimestamp } from './timestamp-contract.mjs';
3
3
 
4
4
  export const TODO_EVENT_KINDS = Object.freeze([
5
- 'plan_genesis', 'start', 'start_retracted', 'block', 'unblock', 'done', 'reopen',
5
+ 'plan_genesis', 'start', 'start_retracted', 'block', 'unblock', 'retire', 'done', 'reopen',
6
6
  'phase_review', 'phase_accept', 'phase_reject', 'phase_reopen',
7
7
  // ADR 0148: 監査していない歴史を「監査なしで閉じた」として明示的に閉じるための専用kind。
8
8
  // phase_review/accept/reject/reopenと同じv3 tail event shape(phase_id持ち)に収め、
@@ -530,6 +530,9 @@ function validPayload(event) {
530
530
  && isTodoDigest(payload.target_start_digest);
531
531
  if (event.kind === 'block') return exactRecord(payload, ['reason']) && nullableText(payload.reason) && payload.reason !== null;
532
532
  if (event.kind === 'unblock') return exactRecord(payload, []);
533
+ // retire: 消費計画が消えた工程の恒久除去(ADR 0147系の状態遷移に足す軽量kind・2026-08-29オーナー裁定)。
534
+ // 理由必須。blocked_reasonのfieldを理由の置き場として再利用し、state recordの形を変えない。
535
+ if (event.kind === 'retire') return exactRecord(payload, ['reason']) && nullableText(payload.reason) && payload.reason !== null;
533
536
  if (event.kind === 'done' && payload?.done_mode === 'authored') {
534
537
  return (exactRecord(payload, ['done_mode', 'imported', 'evidence'])
535
538
  || exactRecord(payload, ['done_mode', 'imported', 'evidence', 'test_result']))
@@ -582,7 +585,7 @@ function validCarriedState(value) {
582
585
  const resultAware = exactRecord(value, [
583
586
  'status', 'started_at', 'done_at', 'blocked_reason', 'evidence', 'imported', 'test_result',
584
587
  ]);
585
- if ((!legacy && !resultAware) || !['pending', 'in-progress', 'blocked', 'done'].includes(value.status)
588
+ if ((!legacy && !resultAware) || !['pending', 'in-progress', 'blocked', 'retired', 'done'].includes(value.status)
586
589
  || (value.started_at !== null && !isStrictTodoTimestamp(value.started_at))
587
590
  || (value.done_at !== null && !isStrictTodoTimestamp(value.done_at))
588
591
  || (value.blocked_reason !== null && !nullableText(value.blocked_reason))
@@ -599,6 +602,9 @@ function validCarriedState(value) {
599
602
  && activeEvidenceValid && testResult === null;
600
603
  if (value.status === 'blocked') return value.done_at === null && value.blocked_reason !== null
601
604
  && activeEvidenceValid && testResult === null;
605
+ // retired: 実行されないまま恒久に閉じた工程。理由はblocked_reasonが持ち、証拠・試験結果は持たない。
606
+ if (value.status === 'retired') return value.done_at === null && value.blocked_reason !== null
607
+ && value.evidence === null && testResult === null;
602
608
  return value.blocked_reason === null && value.evidence !== null
603
609
  // importedは完了状態の来歴を表す。evidence_promotion後もtrueを維持するため、
604
610
  // imported doneの現在証拠はimport sourceまたは通常evidence descriptorのどちらも有効。
@@ -737,7 +743,7 @@ export function validateTodoSnapshot(value) {
737
743
  && value.tasks.every((entry) => exactRecord(entry, [
738
744
  'task_id', 'status', 'started_at', 'done_at', 'blocked_reason', 'evidence', 'evidence_unverified', 'imported',
739
745
  ...(resultAware ? ['test_result'] : []),
740
- ]) && isTodoIdentifier(entry.task_id) && ['pending', 'in-progress', 'blocked', 'done'].includes(entry.status)
746
+ ]) && isTodoIdentifier(entry.task_id) && ['pending', 'in-progress', 'blocked', 'retired', 'done'].includes(entry.status)
741
747
  && (entry.started_at === null || isStrictTodoTimestamp(entry.started_at))
742
748
  && (entry.done_at === null || isStrictTodoTimestamp(entry.done_at)) && nullableText(entry.blocked_reason)
743
749
  && (entry.evidence === null || evidence(entry.evidence) || validateTodoImportSource(entry.evidence))
@@ -81,6 +81,8 @@ function normalizeSections(readModel, narratives, anchorOutcomes, noteContexts)
81
81
  for (const member of readModel.members) {
82
82
  const states = new Map(member.tasks.map((state) => [state.task_id, state]));
83
83
  for (const task of member.plan.tasks) {
84
+ // retired(恒久除去)は図にも詳細パネルにも出さない。履歴と理由はsnapshot/journalが持つ。
85
+ if (states.get(task.task_id)?.status === 'retired') continue;
84
86
  const ref = { project_id: member.plan.project_id, plan_key: member.plan.plan_key, task_id: task.task_id };
85
87
  const narrative = supplied.get(refKey(ref));
86
88
  const markdown = narrative?.markdown ?? '';
@@ -140,6 +140,7 @@ function normalizeInput(readModel, chainProjection) {
140
140
  }
141
141
 
142
142
  const nodesByKey = new Map();
143
+ const retiredKeys = new Set(); // 撤去済みtask。端点に持つ辺は図から一緒に消す
143
144
  const members = [...readModel.members].sort((left, right) => {
144
145
  const leftPlan = left?.plan;
145
146
  const rightPlan = right?.plan;
@@ -164,9 +165,12 @@ function normalizeInput(readModel, chainProjection) {
164
165
  fail('TODO_LAYOUT_INVALID_INPUT', 'plan task has an invalid shape');
165
166
  }
166
167
  const state = statusByTask.get(task.task_id);
167
- if (!plain(state) || !['pending', 'in-progress', 'blocked', 'done'].includes(state.status)) {
168
+ if (!plain(state) || !['pending', 'in-progress', 'blocked', 'retired', 'done'].includes(state.status)) {
168
169
  fail('TODO_LAYOUT_INVALID_INPUT', `missing task state for ${task.task_id}`);
169
170
  }
171
+ // retiredは「実行されないまま恒久に閉じた」工程。工程図は今とこれからの仕事を表す面なので
172
+ // 描画から除く(理由と履歴はsnapshot/journalが持つ。2026-08-29オーナー裁定「消せ」)。
173
+ if (state.status === 'retired') { retiredKeys.add(refKey({ project_id: plan.project_id, plan_key: plan.plan_key, task_id: task.task_id })); continue; }
170
174
  const ref = { project_id: plan.project_id, plan_key: plan.plan_key, task_id: task.task_id };
171
175
  const key = refKey(ref);
172
176
  if (nodesByKey.has(key)) fail('TODO_LAYOUT_INVALID_INPUT', `duplicate task: ${key}`);
@@ -203,6 +207,7 @@ function normalizeInput(readModel, chainProjection) {
203
207
  const toRef = refOf(toValue, 'edge.to');
204
208
  const from = refKey(fromRef);
205
209
  const to = refKey(toRef);
210
+ if (retiredKeys.has(from) || retiredKeys.has(to)) return; // 撤去済み端点の辺は描かない
206
211
  if (!nodesByKey.has(from) || !nodesByKey.has(to) || from === to) {
207
212
  fail('TODO_LAYOUT_INVALID_INPUT', 'edge must connect two distinct read-model tasks');
208
213
  }
@@ -441,7 +441,7 @@ function buildTodoGraph(readModel) {
441
441
  }
442
442
  for (const task of member.plan.tasks) {
443
443
  const state = states.get(task.task_id);
444
- if (!plain(state) || !['pending', 'in-progress', 'blocked', 'done'].includes(state.status)) {
444
+ if (!plain(state) || !['pending', 'in-progress', 'blocked', 'retired', 'done'].includes(state.status)) {
445
445
  fail('TODO_STATUS_INVALID_INPUT', 'todo_status_task_state_missing', {
446
446
  plan_key: member.plan.plan_key,
447
447
  task_id: task.task_id,
@@ -439,7 +439,8 @@ function derivedPhaseStatus(plan, taskStates, phaseStates, phaseId) {
439
439
  // 存在しない)ため、所属taskをフィルタで絞らずplan全taskとして扱う。
440
440
  const tasks = phaseId === TERMINAL_AUDIT_PHASE_ID && isPhaselessTodoPlanSchema(plan.schema)
441
441
  ? plan.tasks : plan.tasks.filter((entry) => entry.phase_id === phaseId);
442
- return tasks.every((entry) => taskStates.get(entry.task_id)?.status === 'done') ? 'gate_ready' : 'active';
442
+ // retiredは「実行されないまま閉じた」工程であり、Phase完了の妨げにしない(全taskがretiredのPhaseも閉じられる)
443
+ return tasks.every((entry) => ['done', 'retired'].includes(taskStates.get(entry.task_id)?.status)) ? 'gate_ready' : 'active';
443
444
  }
444
445
 
445
446
  function projectPhaseStates(plan, events, taskStates) {
@@ -726,6 +727,10 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
726
727
  } else if (event.kind === 'unblock') {
727
728
  if (state.status !== 'blocked') fail('STORE_INCONSISTENT', 'invalid_unblock_transition');
728
729
  state.status = 'in-progress'; state.blocked_reason = null;
730
+ } else if (event.kind === 'retire') {
731
+ // 恒久除去はpending/blockedからだけ。in-progressは保持者がretractしてから、doneは撤去対象でない。
732
+ if (!['pending', 'blocked'].includes(state.status)) fail('STORE_INCONSISTENT', 'invalid_retire_transition');
733
+ state.status = 'retired'; state.blocked_reason = event.payload.reason;
729
734
  } else if (event.kind === 'done') {
730
735
  if (event.payload.done_mode === 'authored') {
731
736
  if (state.status !== 'in-progress' || !dependenciesDone) fail('STORE_INCONSISTENT', 'invalid_done_transition');
@@ -61,7 +61,7 @@ function normalizeTaskStates(structureSet, taskStates) {
61
61
  for (const entry of taskStates) {
62
62
  if (!isPlain(entry) || Object.keys(entry).length !== 2
63
63
  || typeof entry.task_id !== 'string'
64
- || !['pending', 'in-progress', 'blocked', 'done'].includes(entry.status)
64
+ || !['pending', 'in-progress', 'blocked', 'retired', 'done'].includes(entry.status)
65
65
  || states.has(entry.task_id)) {
66
66
  fail('STRUCTURE_OVERLAY_INPUT_INVALID', 'task_state_entry_invalid');
67
67
  }