@quolu/lattice 0.58.0 → 0.58.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.58.0",
3
+ "version": "0.58.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/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({
@@ -2188,10 +2188,11 @@ async function structureFinalize({ repoRoot, env, planKey }) {
2188
2188
  effectiveTransforms.set(task.task_id, latest.realized);
2189
2189
  }
2190
2190
  const actor = mutationActor(env);
2191
- const sourceEvidence = await collectTodoStructureSourceEvidence({
2192
- cwd: repoRoot, structureSet: source, effectiveTransforms,
2191
+ const observation = await collectTodoStructureAuthoritativeObservation({
2192
+ repoRoot, structureSet: source, effectiveTransforms,
2193
2193
  });
2194
- const gitProvenance = collectTodoStructureGitProvenance({ repoRoot, structureSet: source });
2194
+ const sourceEvidence = observation.source_evidence;
2195
+ const gitProvenance = observation.git_provenance;
2195
2196
  const overlay = compileTodoStructureOverlay({
2196
2197
  structureSet: source,
2197
2198
  topology: mergedTopology(store),
@@ -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
+ }
@@ -466,12 +466,6 @@ function realizationFindings(structureSet, states, realizations, gitProvenance,
466
466
  observed: null, expected: 'fresh task realization',
467
467
  nextAction: 'record_the_task_realization_before_done',
468
468
  }));
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
469
  }
476
470
  continue;
477
471
  }
@@ -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
  }