@quolu/lattice 0.18.0 → 0.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/todo-cli.mjs CHANGED
@@ -461,6 +461,7 @@ async function startAdvisory({ repoRoot, store, projection, planKey, taskId }) {
461
461
  contractSuperseded: isTodoIndependenceLegacyMarker(artifact),
462
462
  conflictWithActive: conflictsWithActive[0]?.severability ?? null,
463
463
  conflictBetweenReady: readyConflict?.severability ?? null,
464
+ verdictsAbsent: selfUnknowns.some(({ kind }) => kind === 'plan_verdicts_absent'),
464
465
  }),
465
466
  };
466
467
  }
@@ -836,6 +837,8 @@ async function independence({ repoRoot, requestedPlanKey }) {
836
837
  .some(({ unknowns }) => unknowns.some(({ kind }) => kind === 'record_stale')),
837
838
  conflictWithActive: projected.frontier.conflicts_with_active[0]?.severability ?? null,
838
839
  conflictBetweenReady: projected.frontier.serialize_pairs[0]?.severability ?? null,
840
+ verdictsAbsent: projected.frontier.unknown
841
+ .some(({ unknowns }) => unknowns.some(({ kind }) => kind === 'plan_verdicts_absent')),
839
842
  }),
840
843
  frontier: projected.frontier,
841
844
  result_digest: '',
@@ -17,6 +17,7 @@ export const TODO_INDEPENDENCE_GUIDANCE_CODES = Object.freeze([
17
17
  'independence_stale_for_task',
18
18
  'independence_conflict_with_active',
19
19
  'independence_conflict_between_ready',
20
+ 'independence_verdicts_absent',
20
21
  'independence_verified',
21
22
  ]);
22
23
 
@@ -90,6 +91,10 @@ const CATALOG = Object.freeze({
90
91
  message: '他のready工程と同じ資源を書く記録がある。同時に着手すると衝突する。',
91
92
  next_action: 'serialize_or_split_boundary',
92
93
  }),
94
+ independence_verdicts_absent: Object.freeze({
95
+ message: '判定が途中で止まっており、記録はどの組についてもverdictを持たない。自分にunknownが無いことは、他と干渉しない証拠にならない。',
96
+ next_action: 'resolve_unknowns_then_recompile',
97
+ }),
93
98
  independence_verified: Object.freeze({
94
99
  message: '記録時点の宣言境界では、他のready工程と干渉しない。',
95
100
  next_action: 'none',
@@ -164,7 +169,7 @@ export function todoIndependenceGuidance(code, { severability = null } = {}) {
164
169
  */
165
170
  export function selectIndependenceGuidance({
166
171
  coverage, taskDeclared, taskStale, conflictWithActive = null, conflictBetweenReady = null,
167
- contractSuperseded = false, readyCount = null,
172
+ contractSuperseded = false, readyCount = null, verdictsAbsent = false,
168
173
  }) {
169
174
  // 着手候補が無いなら述べる対象が無い。ここを通さないと、readyが空のとき
170
175
  // 「未検査taskが1件も無い」が空虚に真になり、記録が古くても検証済みへ倒れる。
@@ -186,6 +191,9 @@ export function selectIndependenceGuidance({
186
191
  if (coverage === 'superseded') return todoIndependenceGuidance('independence_superseded');
187
192
  if (!taskDeclared) return todoIndependenceGuidance('independence_task_undeclared');
188
193
  if (taskStale) return todoIndependenceGuidance('independence_stale_for_task');
194
+ // 記録は新しく、この工程自身にも問題が無い。それでもverdictが1つも無いなら、
195
+ // 述べられるのは「干渉しない」ではなく「まだ何も判定していない」である。
196
+ if (verdictsAbsent) return todoIndependenceGuidance('independence_verdicts_absent');
189
197
  return todoIndependenceGuidance('independence_verified');
190
198
  }
191
199
 
@@ -386,6 +386,18 @@ export function projectIndependenceFrontier({
386
386
  for (const entry of artifact.unknowns) {
387
387
  if (ready.includes(entry.task_id)) noteUnknown(entry.task_id, entry.kind, entry.ref);
388
388
  }
389
+ // 記録全体がverdictを持たない場合、どのpairについても判定は存在しない。
390
+ //
391
+ // compileがBOUNDARY_UNKNOWNで止まると、front endはpairwise verdictを1つも返さないので
392
+ // artifact.conflictsは空になる。この空をそのまま読むと、自分にunknownが無いreadyどうしが
393
+ // 「ぶつかる記録が無い=独立」として並列グループへ入る。実際には**誰についても判定していない**。
394
+ // 新規fileを1つ宣言したToDoが混ざるだけで、無関係なToDoの実competitionが消えて
395
+ // 検証済み並列に見える——不在を証拠に読み替える、最も危険な向きの誤りである。
396
+ if (artifact.outcome !== 'compiled') {
397
+ for (const taskId of ready) {
398
+ if (covered.has(taskId)) noteUnknown(taskId, 'plan_verdicts_absent', artifact.outcome);
399
+ }
400
+ }
389
401
  // planが進んだ記録はtask単位に救えない(topology自体が別物)。
390
402
  // HEADだけが進んだ場合は、宣言境界に触れたtaskだけを落とす。
391
403
  if (coverage === 'superseded') {