@quolu/lattice 0.58.0 → 0.58.2

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.58.0",
3
+ "version": "0.58.2",
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/todo-cli.mjs CHANGED
@@ -77,8 +77,7 @@ import {
77
77
  explainTodoStructureRealization,
78
78
  explainTodoStructureSet,
79
79
  } from './todo-structure-contracts.mjs';
80
- import { collectTodoStructureSourceEvidence } from './todo-structure-source-adapter.mjs';
81
- import { collectTodoStructureGitProvenance } from './todo-structure-git-adapter.mjs';
80
+ import { collectTodoStructureAuthoritativeObservation } from './todo-structure-authoritative-observation.mjs';
82
81
  import { compileTodoStructureOverlay } from './todo-structure-overlay.mjs';
83
82
  import {
84
83
  buildTodoStructureCompileArtifact,
@@ -2024,8 +2023,9 @@ async function structureCompile({ repoRoot, env, planKey, inputRef }) {
2024
2023
  });
2025
2024
  }
2026
2025
  const actor = mutationActor(env);
2027
- const sourceEvidence = await collectTodoStructureSourceEvidence({ cwd: repoRoot, structureSet });
2028
- const gitProvenance = collectTodoStructureGitProvenance({ repoRoot, structureSet });
2026
+ const observation = await collectTodoStructureAuthoritativeObservation({ repoRoot, structureSet });
2027
+ const sourceEvidence = observation.source_evidence;
2028
+ const gitProvenance = observation.git_provenance;
2029
2029
  const realizations = [];
2030
2030
  for (const task of structureSet.tasks.filter(({ applicability }) => applicability === 'graph')) {
2031
2031
  realizations.push(...await readTodoStructureRealizationChain({
@@ -2035,10 +2035,7 @@ async function structureCompile({ repoRoot, env, planKey, inputRef }) {
2035
2035
  const overlay = compileTodoStructureOverlay({
2036
2036
  structureSet,
2037
2037
  topology: mergedTopology(store),
2038
- taskStates: structureSet.tasks.map(({ task_id: taskId }) => ({
2039
- task_id: taskId,
2040
- status: member.tasks.find(({ task_id: id }) => id === taskId)?.status ?? 'pending',
2041
- })),
2038
+ taskStates: member.tasks.map(({ task_id: taskId, status }) => ({ task_id: taskId, status })),
2042
2039
  sourceProjection: sourceEvidence.projection,
2043
2040
  gitProvenance,
2044
2041
  realizations,
@@ -2188,14 +2185,15 @@ async function structureFinalize({ repoRoot, env, planKey }) {
2188
2185
  effectiveTransforms.set(task.task_id, latest.realized);
2189
2186
  }
2190
2187
  const actor = mutationActor(env);
2191
- const sourceEvidence = await collectTodoStructureSourceEvidence({
2192
- cwd: repoRoot, structureSet: source, effectiveTransforms,
2188
+ const observation = await collectTodoStructureAuthoritativeObservation({
2189
+ repoRoot, structureSet: source, effectiveTransforms,
2193
2190
  });
2194
- const gitProvenance = collectTodoStructureGitProvenance({ repoRoot, structureSet: source });
2191
+ const sourceEvidence = observation.source_evidence;
2192
+ const gitProvenance = observation.git_provenance;
2195
2193
  const overlay = compileTodoStructureOverlay({
2196
2194
  structureSet: source,
2197
2195
  topology: mergedTopology(store),
2198
- taskStates: source.tasks.map(({ task_id: taskId }) => ({ task_id: taskId, status: 'done' })),
2196
+ taskStates: member.tasks.map(({ task_id: taskId, status }) => ({ task_id: taskId, status })),
2199
2197
  sourceProjection: sourceEvidence.projection,
2200
2198
  gitProvenance,
2201
2199
  realizations,
@@ -0,0 +1,162 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { gitSpawnSync } from './git-process.mjs';
6
+ import { runSensorCli } from './sensor-cli.mjs';
7
+ import { collectTodoStructureGitProvenance } from './todo-structure-git-adapter.mjs';
8
+ import { collectTodoStructureSourceEvidence } from './todo-structure-source-adapter.mjs';
9
+
10
+ const SHA = /^[0-9a-f]{40}$/u;
11
+
12
+ export class TodoStructureObservationError extends Error {
13
+ constructor(code, reason, detail = {}) {
14
+ super(reason);
15
+ this.name = 'TodoStructureObservationError';
16
+ this.code = code;
17
+ this.detail = { reason, ...detail };
18
+ }
19
+ }
20
+
21
+ function fail(code, reason, detail = {}) {
22
+ throw new TodoStructureObservationError(code, reason, detail);
23
+ }
24
+
25
+ function git({ cwd, args, operation }) {
26
+ const result = gitSpawnSync(args, {
27
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
28
+ });
29
+ if (result.error !== undefined || result.status !== 0 || result.signal !== null) {
30
+ fail('STRUCTURE_OBSERVATION_GIT_FAILED', 'observation_git_command_failed', {
31
+ operation, status: result.status ?? null, signal: result.signal ?? null,
32
+ cause: result.error?.message ?? null,
33
+ });
34
+ }
35
+ return result.stdout.trim();
36
+ }
37
+
38
+ function memoryStream() {
39
+ let value = '';
40
+ return {
41
+ stream: { write: (chunk) => { value += String(chunk); } },
42
+ read: () => value,
43
+ };
44
+ }
45
+
46
+ async function initializeObservationSensor(observationRoot) {
47
+ const stdout = memoryStream();
48
+ const stderr = memoryStream();
49
+ const status = await runSensorCli({
50
+ argv: ['init', observationRoot, '--json'],
51
+ stdout: stdout.stream,
52
+ stderr: stderr.stream,
53
+ });
54
+ if (status !== 0) {
55
+ let diagnostic = null;
56
+ try {
57
+ const parsed = JSON.parse(stderr.read().trim());
58
+ diagnostic = {
59
+ code: typeof parsed?.code === 'string' ? parsed.code : null,
60
+ detail: parsed?.detail ?? null,
61
+ };
62
+ } catch {
63
+ diagnostic = { code: null, detail: null };
64
+ }
65
+ fail('STRUCTURE_OBSERVATION_SENSOR_INIT_FAILED', 'observation_sensor_init_failed', {
66
+ sensor_error: diagnostic,
67
+ });
68
+ }
69
+ }
70
+
71
+ function errorSummary(error) {
72
+ return {
73
+ code: typeof error?.code === 'string' ? error.code : null,
74
+ message: error instanceof Error ? error.message : String(error),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * 管理repoのdirtyなindex/worktreeを一切観測せず、current HEADだけを一時detached worktreeへ
80
+ * 展開してGit来歴とsource graphを収集する。store/planned source/artifactの読書きは呼出側が
81
+ * 管理repoで行うため、ここへは持ち込まない。
82
+ */
83
+ export async function collectTodoStructureAuthoritativeObservation({
84
+ repoRoot, structureSet, effectiveTransforms = null,
85
+ initializeSensor = initializeObservationSensor,
86
+ } = {}) {
87
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0
88
+ || structureSet === null || typeof structureSet !== 'object'
89
+ || typeof initializeSensor !== 'function') {
90
+ fail('STRUCTURE_OBSERVATION_INPUT_INVALID', 'observation_input_invalid');
91
+ }
92
+
93
+ const headSha = git({
94
+ cwd: repoRoot, args: ['rev-parse', '--verify', 'HEAD^{commit}'], operation: 'resolve_head',
95
+ });
96
+ if (!SHA.test(headSha)) fail('STRUCTURE_OBSERVATION_HEAD_INVALID', 'observation_head_invalid');
97
+
98
+ const temporaryRoot = await mkdtemp(path.join(tmpdir(), 'lattice-structure-observation-'));
99
+ const observationRoot = path.join(temporaryRoot, 'worktree');
100
+ let registered = false;
101
+ let operationError = null;
102
+ let result;
103
+ try {
104
+ git({
105
+ cwd: repoRoot,
106
+ args: ['worktree', 'add', '--detach', '--quiet', observationRoot, headSha],
107
+ operation: 'worktree_add',
108
+ });
109
+ registered = true;
110
+ const observedHead = git({
111
+ cwd: observationRoot, args: ['rev-parse', '--verify', 'HEAD^{commit}'],
112
+ operation: 'verify_observation_head',
113
+ });
114
+ if (observedHead !== headSha) {
115
+ fail('STRUCTURE_OBSERVATION_HEAD_MISMATCH', 'observation_head_mismatch', {
116
+ expected_head_sha: headSha, actual_head_sha: observedHead,
117
+ });
118
+ }
119
+
120
+ // sensor DBを作る前にclean treeをGit provenanceへ束縛する。sensorの一時fileは証拠対象外で、
121
+ // source projection収集後にworktreeごと破棄する。
122
+ const gitProvenance = collectTodoStructureGitProvenance({
123
+ repoRoot: observationRoot, structureSet,
124
+ });
125
+ await initializeSensor(observationRoot);
126
+ const sourceEvidence = await collectTodoStructureSourceEvidence({
127
+ cwd: observationRoot, structureSet, effectiveTransforms,
128
+ observationStage: effectiveTransforms === null ? 'planned' : 'final',
129
+ });
130
+ result = {
131
+ head_sha: headSha,
132
+ git_provenance: gitProvenance,
133
+ source_evidence: sourceEvidence,
134
+ };
135
+ } catch (error) {
136
+ operationError = error;
137
+ }
138
+
139
+ let cleanupError = null;
140
+ try {
141
+ if (registered) {
142
+ git({
143
+ cwd: repoRoot,
144
+ args: ['worktree', 'remove', '--force', observationRoot],
145
+ operation: 'worktree_remove',
146
+ });
147
+ }
148
+ await rm(temporaryRoot, { recursive: true, force: true });
149
+ } catch (error) {
150
+ cleanupError = error;
151
+ }
152
+
153
+ if (cleanupError !== null) {
154
+ fail('STRUCTURE_OBSERVATION_CLEANUP_FAILED', 'observation_cleanup_failed', {
155
+ operation_error: operationError === null ? null : errorSummary(operationError),
156
+ cleanup_error: errorSummary(cleanupError),
157
+ observation_root: observationRoot,
158
+ });
159
+ }
160
+ if (operationError !== null) throw operationError;
161
+ return result;
162
+ }
@@ -62,12 +62,14 @@ function normalizeTaskStates(structureSet, taskStates) {
62
62
  if (!isPlain(entry) || Object.keys(entry).length !== 2
63
63
  || typeof entry.task_id !== 'string'
64
64
  || !['pending', 'in-progress', 'blocked', 'done'].includes(entry.status)
65
- || !expected.has(entry.task_id) || states.has(entry.task_id)) {
65
+ || states.has(entry.task_id)) {
66
66
  fail('STRUCTURE_OVERLAY_INPUT_INVALID', 'task_state_entry_invalid');
67
67
  }
68
68
  states.set(entry.task_id, entry.status);
69
69
  }
70
- if (states.size !== expected.size) fail('STRUCTURE_OVERLAY_INPUT_INVALID', 'task_state_coverage_invalid');
70
+ if ([...expected].some((taskId) => !states.has(taskId))) {
71
+ fail('STRUCTURE_OVERLAY_INPUT_INVALID', 'task_state_coverage_invalid');
72
+ }
71
73
  return states;
72
74
  }
73
75
 
@@ -302,7 +304,7 @@ function buildOverlayGraph(structureSet, states, latestRealizations, sourceProje
302
304
  return { nodes: uniqueNodes, edges, effective };
303
305
  }
304
306
 
305
- function connectionFindings(structureSet, effective, topologyDag) {
307
+ function connectionFindings(structureSet, effective, topologyDag, states) {
306
308
  const findings = [];
307
309
  const externalById = new Map(structureSet.external_contracts
308
310
  .map((external) => [external.contract_id, external]));
@@ -322,7 +324,8 @@ function connectionFindings(structureSet, effective, topologyDag) {
322
324
  const structureTaskIds = new Set(structureSet.tasks.map(({ task_id: id }) => id));
323
325
  const extraTopologyTasks = topologyDag.nodes
324
326
  .filter(({ ref }) => ref.project_id === structureSet.project_id
325
- && ref.plan_key === structureSet.plan_key && !structureTaskIds.has(ref.task_id))
327
+ && ref.plan_key === structureSet.plan_key && !structureTaskIds.has(ref.task_id)
328
+ && states.get(ref.task_id) !== 'done')
326
329
  .map(({ ref }) => ref.task_id).sort(compareText);
327
330
  if (extraTopologyTasks.length > 0) findings.push(finding({
328
331
  code: 'STRUCTURE_COVERAGE_MISSING', severity: 'error', taskIds: extraTopologyTasks,
@@ -466,12 +469,6 @@ function realizationFindings(structureSet, states, realizations, gitProvenance,
466
469
  observed: null, expected: 'fresh task realization',
467
470
  nextAction: 'record_the_task_realization_before_done',
468
471
  }));
469
- } else if (state === 'in-progress') {
470
- findings.push(finding({
471
- code: 'STRUCTURE_COMMIT_UNBOUND', severity: 'unknown', taskIds: [task.task_id],
472
- observed: { commits: [] }, expected: 'explicit realization commit OIDs when implementation exists',
473
- nextAction: 'record_a_realization_when_the_task_has_committed_implementation',
474
- }));
475
472
  }
476
473
  continue;
477
474
  }
@@ -563,7 +560,7 @@ export function compileTodoStructureOverlay({
563
560
  const rawFindings = [
564
561
  ...anchorFindings(structureSet, sourceProjection),
565
562
  ...missingEffectiveAnchors,
566
- ...connectionFindings(structureSet, graph.effective, topologyDag),
563
+ ...connectionFindings(structureSet, graph.effective, topologyDag, states),
567
564
  ...realizationFindings(
568
565
  structureSet, states, normalizedRealizations, gitProvenance, graph.effective,
569
566
  ),
@@ -234,13 +234,26 @@ function edgeObservation(outcome, expectedNode, direction, expectedPath) {
234
234
  };
235
235
  }
236
236
 
237
- function effectVerdict(anchor, existence) {
238
- if (anchor.expected_at !== 'current') {
237
+ function effectVerdict(anchor, existence, observationStage) {
238
+ if (anchor.expected_at === 'baseline') {
239
239
  return { verdict: 'unknown', reason: 'STRUCTURE_CODE_ANCHOR_TIME_DEFERRED' };
240
240
  }
241
+ if (anchor.expected_at === 'after_task' && observationStage === 'planned') {
242
+ return { verdict: 'consistent', reason: null, deferred: true };
243
+ }
241
244
  if (existence === 'unknown') {
242
245
  return { verdict: 'unknown', reason: 'STRUCTURE_CODE_ANCHOR_UNRESOLVED' };
243
246
  }
247
+ if (anchor.expected_at === 'after_task' && observationStage === 'final') {
248
+ const expected = anchor.effect === 'delete' ? 'absent' : 'present';
249
+ return existence === expected
250
+ ? { verdict: 'consistent', reason: null }
251
+ : { verdict: 'inconsistent', reason: expected === 'present'
252
+ ? 'STRUCTURE_CODE_ANCHOR_ABSENT' : 'STRUCTURE_DELETE_STILL_EXISTS' };
253
+ }
254
+ if (anchor.expected_at !== 'current') {
255
+ return { verdict: 'unknown', reason: 'STRUCTURE_CODE_ANCHOR_TIME_DEFERRED' };
256
+ }
244
257
  if (anchor.effect === 'create') {
245
258
  return existence === 'absent'
246
259
  ? { verdict: 'consistent', reason: null }
@@ -274,7 +287,21 @@ function unknownAnchor(anchor, evidence, reason) {
274
287
  };
275
288
  }
276
289
 
277
- function projectAnchor({ anchor, querySet, outcomes, statusReady }) {
290
+ function deferredAnchor(anchor, evidence) {
291
+ return {
292
+ ...unknownAnchor(anchor, evidence, null),
293
+ verdict: 'consistent',
294
+ reason: null,
295
+ coverage: 'deferred',
296
+ edges: {
297
+ state: 'not_applicable',
298
+ incoming: [], outgoing: [], incoming_omitted: 0, outgoing_omitted: 0,
299
+ incoming_source_limit_reached: false, outgoing_source_limit_reached: false,
300
+ },
301
+ };
302
+ }
303
+
304
+ function projectAnchor({ anchor, querySet, outcomes, statusReady, observationStage }) {
278
305
  const pathQuery = queryFor('affected', anchor.path);
279
306
  const pathOutcome = outcomes.get(pathQuery.id);
280
307
  const evidence = {
@@ -284,11 +311,14 @@ function projectAnchor({ anchor, querySet, outcomes, statusReady }) {
284
311
  callers: null,
285
312
  callees: null,
286
313
  };
314
+ if (anchor.expected_at === 'after_task' && observationStage === 'planned') {
315
+ return deferredAnchor(anchor, evidence);
316
+ }
287
317
  if (!statusReady) return unknownAnchor(anchor, evidence, 'STRUCTURE_SENSOR_NOT_READY');
288
318
 
289
319
  const pathResult = pathObservation(pathQuery, pathOutcome, anchor.path);
290
320
  if (anchor.symbol === null) {
291
- const decision = effectVerdict(anchor, pathResult.existence);
321
+ const decision = effectVerdict(anchor, pathResult.existence, observationStage);
292
322
  return {
293
323
  ...unknownAnchor(anchor, evidence, decision.reason),
294
324
  verdict: decision.verdict,
@@ -321,7 +351,7 @@ function projectAnchor({ anchor, querySet, outcomes, statusReady }) {
321
351
  candidates: symbolResult.candidates,
322
352
  };
323
353
 
324
- const decision = effectVerdict(anchor, symbolResult.existence);
354
+ const decision = effectVerdict(anchor, symbolResult.existence, observationStage);
325
355
  if (symbolResult.existence !== 'present') {
326
356
  return {
327
357
  ...unknownAnchor(anchor, evidence, decision.reason),
@@ -366,15 +396,18 @@ function projectAnchor({ anchor, querySet, outcomes, statusReady }) {
366
396
 
367
397
  /** 収集済みsensor outcomeから、anchor到達範囲だけのbounded projectionを作る。 */
368
398
  export function projectTodoStructureSourceEvidence({
369
- structureSet, collected, effectiveTransforms = null,
399
+ structureSet, collected, effectiveTransforms = null, observationStage = 'current',
370
400
  } = {}) {
371
401
  assertStructureSet(structureSet);
402
+ if (!['current', 'planned', 'final'].includes(observationStage)) {
403
+ throw new TypeError('observationStageが不正');
404
+ }
372
405
  const querySet = buildTodoStructureSensorQuerySet(structureSet, { effectiveTransforms });
373
406
  const outcomes = indexOutcomes(querySet, collected);
374
407
  const statusQuery = querySet.queries[0];
375
408
  const statusOutcome = outcomes.get(statusQuery.id);
376
409
  const anchors = graphAnchors(structureSet, effectiveTransforms).map((anchor) => projectAnchor({
377
- anchor, querySet, outcomes, statusReady: statusOutcome.outcome === 'ready',
410
+ anchor, querySet, outcomes, statusReady: statusOutcome.outcome === 'ready', observationStage,
378
411
  }));
379
412
  const projection = {
380
413
  schema: TODO_STRUCTURE_SOURCE_PROJECTION_SCHEMA,
@@ -400,7 +433,7 @@ export function projectTodoStructureSourceEvidence({
400
433
 
401
434
  /** 実sensor収集とpure projectionを同じ既存adapter経由で行う。 */
402
435
  export async function collectTodoStructureSourceEvidence({
403
- cwd, structureSet, effectiveTransforms = null,
436
+ cwd, structureSet, effectiveTransforms = null, observationStage = 'current',
404
437
  execute = undefined, inspectAffectedPath = undefined,
405
438
  } = {}) {
406
439
  const querySet = buildTodoStructureSensorQuerySet(structureSet, { effectiveTransforms });
@@ -412,6 +445,8 @@ export async function collectTodoStructureSourceEvidence({
412
445
  });
413
446
  return {
414
447
  query_set: querySet,
415
- projection: projectTodoStructureSourceEvidence({ structureSet, collected, effectiveTransforms }),
448
+ projection: projectTodoStructureSourceEvidence({
449
+ structureSet, collected, effectiveTransforms, observationStage,
450
+ }),
416
451
  };
417
452
  }