@quolu/lattice 0.69.5 → 0.70.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/README.ja.md CHANGED
@@ -309,9 +309,13 @@ required evidenceを束縛した`todo phase accept`で重監査の判断を記
309
309
  ```bash
310
310
  lattice todo phase status --plan <key> # phase無しplanでも暗黙Phaseを返す(implicit: true)
311
311
  lattice todo phase review --plan <key> --phase terminal-audit --reason <text>
312
- lattice todo phase accept --plan <key> --phase terminal-audit --input <file>
312
+ lattice todo phase accept --plan <key> --phase terminal-audit --evidence <監査file>
313
313
  ```
314
314
 
315
+ `--evidence`には、commit済みの監査本文をrepo相対pathで渡します。reviewのevent digest、証拠の記述子、
316
+ 必須evidence slotはLatticeが組み立てます。必須slotが複数あるPhaseや、記述子を自分で渡したい時は
317
+ `--input <file>`(`lattice.phase_accept_input.v1`、`input_digest`は空でよい)を使います。
318
+
315
319
  **過去の工程は監査できません。** 監査対象のコードが既に変化しているためです。そこで
316
320
  「監査なしで閉じた」という状態を別に持ちます——`accepted`(監査を通った)へは絶対に化けず、
317
321
  記録は永久に区別できます。工程図では畳まれるので監査待ちの札は外れます。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.69.5",
3
+ "version": "0.70.0",
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
@@ -141,6 +141,8 @@ Write commands:
141
141
  # 実際に受理する最新契約のJSON Schemaを返す(storeを読まない)。
142
142
  # 入力が契約に合わないときは、違反フィールドのpathがerror detailへ載る
143
143
  phase review --plan <key> --phase <id> --reason <text>
144
+ phase accept --plan <key> --phase <id> --evidence <file> # 監査fileから入力を機械が組む
145
+ phase reject --plan <key> --phase <id> --reason <text> --evidence <file>
144
146
  phase <accept|reject> --plan <key> --phase <id> --input <file>
145
147
  phase reopen --plan <key> --phase <id> --reason <text> [--override-reason <text>]
146
148
  phase close-unaudited --plan <key> --phase <id> --reason <text>
@@ -278,8 +280,8 @@ const SUBCOMMAND_USAGE = Object.freeze({
278
280
  + ' | baseline --reason <text> [--except <plan_key>]...',
279
281
  'todo phase status': 'todo phase status --plan <key>',
280
282
  'todo phase review': 'todo phase review --plan <key> --phase <id> --reason <text>',
281
- 'todo phase accept': 'todo phase accept --plan <key> --phase <id> --input <file>',
282
- 'todo phase reject': 'todo phase reject --plan <key> --phase <id> --input <file>',
283
+ 'todo phase accept': 'todo phase accept --plan <key> --phase <id> --evidence <file> | --input <file>',
284
+ 'todo phase reject': 'todo phase reject --plan <key> --phase <id> --reason <text> --evidence <file> | --input <file>',
283
285
  'todo phase reopen': 'todo phase reopen --plan <key> --phase <id> --reason <text> [--override-reason <text>]',
284
286
  'todo phase close-unaudited': 'todo phase close-unaudited --plan <key> --phase <id> --reason <text>',
285
287
  'todo phase baseline': 'todo phase baseline --reason <text> [--except <plan_key>]...',
package/src/todo-cli.mjs CHANGED
@@ -60,6 +60,7 @@ import {
60
60
  isPhaselessTodoPlanSchema,
61
61
  projectTodoCrossPlanDependencies,
62
62
  TERMINAL_AUDIT_PHASE_ID,
63
+ todoPhaseDefinitions,
63
64
  readTodoIndependenceArtifact,
64
65
  readTodoSeamProposalArtifact,
65
66
  readTodoStore,
@@ -974,13 +975,38 @@ function validatePhaseDecisionInput(value, outcome) {
974
975
  && isTodoDigest(value.input_digest) && value.input_digest === todoSelfDigest(value, 'input_digest');
975
976
  }
976
977
 
978
+ // `--input`の形が合わないとき、期待形をerrorへ同梱する(ADR 0130)。自己digestは入口が補うので
979
+ // 空でよい(ADR 0181)。証拠記述子を手で作らせないために、ふつうは`--evidence`を案内する。
980
+ const PHASE_DECISION_EXPECTED = Object.freeze({
981
+ accept: '{ "schema": "lattice.phase_accept_input.v1", "review_event_digest": "<phase reviewのevent_digest>",'
982
+ + ' "decision_evidence": <evidence記述子>, "evidence_slots": [{ "slot_id": "<必須slot>", "evidence": <evidence記述子> }],'
983
+ + ' "input_digest": "" }',
984
+ reject: '{ "schema": "lattice.phase_reject_input.v1", "review_event_digest": "<phase reviewのevent_digest>",'
985
+ + ' "reason": "<text>", "decision_evidence": <evidence記述子>, "input_digest": "" }',
986
+ note: 'input_digestは入口が計算する。証拠fileがcommit済みなら、`--evidence <file>`で'
987
+ + 'review_event_digest・記述子・必須slotを機械が埋める。evidence記述子の形はEVIDENCE_DESCRIPTOR_EXPECTEDを見る。',
988
+ });
989
+
977
990
  async function phaseDecision({ repoRoot, env, planKey, phaseId, outcome, inputRef }) {
978
- const input = await readRevisionInput(repoRoot, inputRef, {
979
- validate: (value) => validatePhaseDecisionInput(value, outcome),
980
- invalidCode: 'PHASE_DECISION_INVALID', invalidReason: 'phase_decision_schema_or_digest_invalid',
981
- // phase decision入力はrevision契約と別形状。既定のrevision explainを誤って当てない。
982
- explain: null,
983
- });
991
+ let input;
992
+ try {
993
+ input = await readRevisionInput(repoRoot, inputRef, {
994
+ validate: (value) => validatePhaseDecisionInput(value, outcome),
995
+ invalidCode: 'PHASE_DECISION_INVALID', invalidReason: 'phase_decision_schema_or_digest_invalid',
996
+ // phase decision入力はrevision契約と別形状。既定のrevision explainを誤って当てない。
997
+ explain: null,
998
+ });
999
+ } catch (error) {
1000
+ if (error instanceof TodoStoreError && error.code === 'PHASE_DECISION_INVALID') {
1001
+ throw new TodoStoreError('PHASE_DECISION_INVALID', 'phase_decision_schema_or_digest_invalid', undefined, {
1002
+ expected: PHASE_DECISION_EXPECTED[outcome], note: PHASE_DECISION_EXPECTED.note,
1003
+ evidence_descriptor: EVIDENCE_DESCRIPTOR_EXPECTED,
1004
+ next_action: `lattice todo phase ${outcome} --plan ${planKey} --phase ${phaseId}`
1005
+ + (outcome === 'reject' ? ' --reason <text>' : '') + ' --evidence <repo内の監査file>',
1006
+ });
1007
+ }
1008
+ throw error;
1009
+ }
984
1010
  const payload = outcome === 'accept'
985
1011
  ? { review_event_digest: input.review_event_digest, decision_evidence: input.decision_evidence,
986
1012
  evidence_slots: input.evidence_slots }
@@ -989,6 +1015,49 @@ async function phaseDecision({ repoRoot, env, planKey, phaseId, outcome, inputRe
989
1015
  return phaseMutation({ repoRoot, env, planKey, phaseId, kind: `phase_${outcome}`, payload });
990
1016
  }
991
1017
 
1018
+ /**
1019
+ * `--evidence <file>`から判断入力を組み立てる(ADR 0181の下書き受理をphase判断へ広げる)。
1020
+ * review_event_digestはstoreの現在のreviewから、記述子は`todo done`と同じ処理でcommit済み
1021
+ * fileから作る。必須slotが1つならその証拠で埋める。複数slotへ1つの証拠を流用すると
1022
+ * 「各slotを別々に確かめた」ことを偽るので、その場合は`--input`で明示させる。
1023
+ */
1024
+ async function phaseDecisionFromEvidence({ repoRoot, env, planKey, phaseId, outcome, evidenceRef, reason }) {
1025
+ const store = await readTodoStore({ repoRoot });
1026
+ const member = store.members.find(({ plan }) => plan.plan_key === planKey);
1027
+ if (member === undefined) {
1028
+ throw new TodoStoreError('PHASE_DECISION_INVALID', 'plan_not_active', undefined, { plan_key: planKey });
1029
+ }
1030
+ const state = member.phases.find(({ phase_id: current }) => current === phaseId);
1031
+ const definition = todoPhaseDefinitions(member.plan).find(({ phase_id: current }) => current === phaseId);
1032
+ if (state === undefined || definition === undefined) {
1033
+ throw new TodoStoreError('PHASE_DECISION_INVALID', 'phase_not_found', undefined, {
1034
+ plan_key: planKey, phase_id: phaseId,
1035
+ phases: todoPhaseDefinitions(member.plan).map(({ phase_id: id }) => id),
1036
+ });
1037
+ }
1038
+ if (state.status !== 'reviewing' || state.review_event_digest === null) {
1039
+ throw new TodoStoreError('PHASE_DECISION_INVALID', 'phase_not_reviewing', undefined, {
1040
+ plan_key: planKey, phase_id: phaseId, status: state.status,
1041
+ next_action: `lattice todo phase review --plan ${planKey} --phase ${phaseId} --reason <text>`,
1042
+ });
1043
+ }
1044
+ if (outcome === 'accept' && definition.required_evidence_slots.length !== 1) {
1045
+ throw new TodoStoreError('PHASE_DECISION_INVALID', 'multiple_evidence_slots_need_input', undefined, {
1046
+ required_evidence_slots: definition.required_evidence_slots,
1047
+ expected: PHASE_DECISION_EXPECTED.accept, evidence_descriptor: EVIDENCE_DESCRIPTOR_EXPECTED,
1048
+ next_action: `lattice todo phase accept --plan ${planKey} --phase ${phaseId} --input <file>`,
1049
+ });
1050
+ }
1051
+ const evidence = await resolveDoneEvidence({
1052
+ repoRoot, evidenceRef, evidenceMessage: null, planKey, taskId: `${phaseId}-${outcome}`,
1053
+ });
1054
+ const payload = outcome === 'accept'
1055
+ ? { review_event_digest: state.review_event_digest, decision_evidence: evidence,
1056
+ evidence_slots: definition.required_evidence_slots.map((slotId) => ({ slot_id: slotId, evidence })) }
1057
+ : { review_event_digest: state.review_event_digest, reason, decision_evidence: evidence };
1058
+ return phaseMutation({ repoRoot, env, planKey, phaseId, kind: `phase_${outcome}`, payload });
1059
+ }
1060
+
992
1061
  async function phaseMutation({ repoRoot, env, planKey, phaseId, kind, payload }) {
993
1062
  const { event, snapshot, phases } = await appendTodoEvent({
994
1063
  repoRoot, writer: createTodoStoreWriter({ caller: 'g5-authoring' }), planKey,
@@ -3989,14 +4058,22 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
3989
4058
  action = (repoRoot) => phaseMutation({ repoRoot, env, planKey: argv[3], phaseId: argv[5],
3990
4059
  kind: 'phase_review', payload: { reason: argv[7] } });
3991
4060
  } else if (argv[0] === 'phase' && ['accept', 'reject'].includes(argv[1])) {
3992
- const flags = matchFlagCommand(argv, ['phase', argv[1]], {
3993
- known: ['plan', 'phase', 'input'], required: ['plan', 'phase', 'input'],
4061
+ const outcome = argv[1];
4062
+ const flags = matchFlagCommand(argv, ['phase', outcome], {
4063
+ known: ['plan', 'phase', 'input', 'evidence', 'reason'], required: ['plan', 'phase'],
3994
4064
  });
3995
- if (flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.phase)
3996
- && isAuthoringPathToken(flags.input)) {
4065
+ const base = flags !== null && isTodoIdentifier(flags.plan) && isTodoIdentifier(flags.phase);
4066
+ if (base && isAuthoringPathToken(flags.input) && flags.evidence === undefined && flags.reason === undefined) {
3997
4067
  action = (repoRoot) => phaseDecision({
3998
4068
  repoRoot, env, planKey: flags.plan, phaseId: flags.phase,
3999
- outcome: argv[1], inputRef: flags.input,
4069
+ outcome, inputRef: flags.input,
4070
+ });
4071
+ } else if (base && flags.input === undefined && isAuthoringPathToken(flags.evidence)
4072
+ && (outcome === 'accept' ? flags.reason === undefined
4073
+ : typeof flags.reason === 'string' && flags.reason.length > 0)) {
4074
+ action = (repoRoot) => phaseDecisionFromEvidence({
4075
+ repoRoot, env, planKey: flags.plan, phaseId: flags.phase,
4076
+ outcome, evidenceRef: flags.evidence, reason: flags.reason ?? null,
4000
4077
  });
4001
4078
  }
4002
4079
  } else if ((argv.length === 8 || argv.length === 10) && argv[0] === 'phase'