@quolu/lattice 0.57.2 → 0.58.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/src/cli-help.mjs CHANGED
@@ -77,11 +77,13 @@ Read commands:
77
77
  note list --plan <key> [--task <id>] --json # note全履歴の診断面(通常read/startでは不要)
78
78
  bindings [--plan <key>] [--json] # compile_binding付きTaskをTODO identityつきで投影する
79
79
  independence [--plan <key>] [--json] # readyを検証済み並列・要直列・未検査へ分けて投影する
80
+ structure --schema --json # plan定義後に入力する論理dataflow契約をstore非依存で返す
81
+ structure [--plan <key>] --json # 保存済み構造をsensor再実行なしでfreshness・finding付き投影する
80
82
  seam-profile --plan <key> --file <path> [--json] # 係争fileの切断コスト内訳を投影する(read-only)
81
83
  seam-proposal [--plan <key>] [--json] # 記録済みseam提案をsensor無しで投影する
82
84
  verify [--plan <key>] [--json]
83
85
  snapshot --rebuild --plan <key>
84
- gantt serve --port <port> [--scope live|all] # 動的表示。静的HTML生成とstatusは廃止
86
+ gantt serve --port <port> [--scope live|all] # 動的表示。opt-in済みplanは工程図と別の構造検査面を持つ
85
87
  phase status --plan <key>
86
88
 
87
89
  Write commands:
@@ -97,7 +99,7 @@ Write commands:
97
99
  # 既定は全ready同時dispatch。直列にするには理由の申告後、再考を経て --serial-confirmed が要る
98
100
  block --plan <key> --task <id> --reason <text>
99
101
  unblock --plan <key> --task <id>
100
- done --plan <key> --task <id> --evidence <file>
102
+ done --plan <key> --task <id> --evidence <file> # 構造対象taskはfresh realizationを要求する
101
103
  reopen --plan <key> --task <id> --reason <text> [--override-reason <text>]
102
104
  evidence promote --plan <key> --task <id> --evidence <file>
103
105
  dependency connect --from-plan <key> --from-task <id> --to-plan <key> --to-task <id> --reason <text>
@@ -105,6 +107,16 @@ Write commands:
105
107
  independence compile --plan <key> --input <file> # witness setとsensorから並列可否を記録する
106
108
  independence witness migrate --plan <key> # revision後の宣言をtask migrationで写す
107
109
  independence witness scaffold --plan <key> --input <draft> # 下書きとfresh観測から宣言を書き出す
110
+ structure input --plan <key> --input <file> --dry-run --json
111
+ # plan identity・topology・unfinished task coverage・baseline祖先を無変更で検査する
112
+ structure input --plan <key> --input <file>
113
+ # 検査済みplanned sourceをcanonical refへ保存する。compile成功までは有効化しない
114
+ structure compile --plan <key> --input <file>
115
+ # source graph・Git provenance・ToDo DAGを結合する。consistent時だけplanへimmutableに有効化する
116
+ structure realize --plan <key> --task <id> --input <file>
117
+ # 実装後の構造をappend-onlyで記録してからtodo doneへ進む
118
+ structure finalize --plan <key> --json
119
+ # 全対象task完了後、最終HEADと全realizationを再結合する。fresh consistentだけterminal受理へ進む
108
120
  seam-proposal compile --plan <key> # 並列可否記録と実sensorからseam提案を記録する
109
121
  seam-proposal apply --plan <key> # 記録済み提案を隔離worktreeで適用し五条件で採否を決める
110
122
  seam-proposal land --plan <key> --names <file> # 採用された変換を本ツリーへ着地させる
@@ -112,7 +124,7 @@ Write commands:
112
124
  revise --plan <key> --input <file>
113
125
  revise-phase --plan <key> --input <file>
114
126
  revise-set --input <file>
115
- <revise|revise-phase|revise-set|migrate> --schema --json
127
+ <revise|revise-phase|revise-set|migrate|structure> --schema --json
116
128
  # 実際に受理する最新契約のJSON Schemaを返す(storeを読まない)。
117
129
  # 入力が契約に合わないときは、違反フィールドのpathがerror detailへ載る
118
130
  phase review --plan <key> --phase <id> --reason <text>
@@ -224,6 +236,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
224
236
  'todo note list': 'todo note list --plan <key> [--task <id>] --json',
225
237
  'todo bindings': 'todo bindings [--plan <key>] [--json]',
226
238
  'todo independence': 'todo independence [--plan <key>] [--json] | compile --plan <key> --input <file> | witness migrate --plan <key>',
239
+ 'todo structure': 'todo structure --schema --json | [--plan <key>] --json | input --plan <key> --input <file> [--dry-run --json] | compile --plan <key> --input <file> | realize --plan <key> --task <id> --input <file> | finalize --plan <key> --json',
227
240
  'todo seam-profile': 'todo seam-profile --plan <key> --file <path> [--json]',
228
241
  'todo seam-proposal': 'todo seam-proposal [--plan <key>] [--json] | compile --plan <key>',
229
242
  'todo verify': 'todo verify [--plan <key>] [--json]',
package/src/dag-chain.mjs CHANGED
@@ -259,3 +259,40 @@ export function analyzeDagChains(
259
259
  topologicalOrder: order,
260
260
  };
261
261
  }
262
+
263
+ /**
264
+ * 既存DAG正本と同じnormalize/cycle判定を使い、要求されたpairだけの到達性を返す。
265
+ * structure overlayが独自のnode/edge検証やcycle algorithmを持たないための狭い公開面。
266
+ */
267
+ export function analyzeDagReachability(nodes, edges, pairs) {
268
+ if (!Array.isArray(pairs)) {
269
+ throw new DagChainError('DAG_INVALID_REACHABILITY_PAIRS', 'pairs must be an array');
270
+ }
271
+ const graph = normalizeGraph(nodes, edges);
272
+ topologicalOrder(nodes, graph.successors, graph.predecessors);
273
+ const nodeSet = new Set(nodes);
274
+ for (const pair of pairs) {
275
+ if (!Array.isArray(pair) || pair.length !== 2
276
+ || !nodeSet.has(pair[0]) || !nodeSet.has(pair[1])) {
277
+ throw new DagChainError(
278
+ 'DAG_INVALID_REACHABILITY_PAIRS',
279
+ 'each pair must be a [from, to] using graph nodes',
280
+ );
281
+ }
282
+ }
283
+ const reachedBySource = new Map();
284
+ for (const [from] of pairs) {
285
+ if (reachedBySource.has(from)) continue;
286
+ const reached = new Set([from]);
287
+ const pending = [from];
288
+ for (let cursor = 0; cursor < pending.length; cursor += 1) {
289
+ for (const successor of graph.successors.get(pending[cursor])) {
290
+ if (reached.has(successor)) continue;
291
+ reached.add(successor);
292
+ pending.push(successor);
293
+ }
294
+ }
295
+ reachedBySource.set(from, reached);
296
+ }
297
+ return pairs.map(([from, to]) => reachedBySource.get(from).has(to));
298
+ }
@@ -19,6 +19,7 @@ import { projectTodoStatus } from './todo-status.mjs';
19
19
  import { readTodoPlanNotesForStatus } from './todo-note-store.mjs';
20
20
  import { projectIndependenceFrontier } from './todo-independence.mjs';
21
21
  import { readTodoParallelCandidatesForStatus } from './todo-parallel-candidates.mjs';
22
+ import { readTodoStructureFinalizationsForStatus } from './todo-structure-store.mjs';
22
23
  import { isTodoIndependenceLegacyMarker } from './todo-independence-contracts.mjs';
23
24
  import { selectIndependenceGuidance } from './todo-independence-guidance.mjs';
24
25
  import { ensureTodoDashboardActivity } from './todo-dashboard-registry.mjs';
@@ -273,6 +274,7 @@ async function resolveProjectState({ cwd, cliVersion }) {
273
274
  const todo = projectTodoStatus(store, {
274
275
  planNotes: await readTodoPlanNotesForStatus({ repoRoot, store }),
275
276
  parallelCandidates: await readTodoParallelCandidatesForStatus({ repoRoot, store, gitHead }),
277
+ structureFinalizations: await readTodoStructureFinalizationsForStatus({ repoRoot, store }),
276
278
  });
277
279
  const activeRuns = todo.active_set.map((entry) => ({
278
280
  plan_key: entry.plan_key, task_id: entry.task_id, label: entry.label,
@@ -283,14 +285,18 @@ async function resolveProjectState({ cwd, cliVersion }) {
283
285
  : todo.next_ready.length > 0
284
286
  ? { command: `lattice todo start --plan ${todo.next_ready[0].plan_key} --task ${todo.next_ready[0].task_id}${todo.next_ready.length > 1 ? ' --parallel-frontier' : ''}`,
285
287
  reason: todo.next_ready.length > 1 ? 'parallel_frontier_present' : 'next_ready_present' }
286
- // 監査待ちが在る限り「残作業なし」と答えない。優先順位はactive_run > next_ready >
287
- // audit_pending > なしで、ready frontierが在る間はADR 0063の並列開始コマンドが勝つ
288
+ // 監査/構造finalization待ちが在る限り「残作業なし」と答えない。優先順位は
289
+ // active_run > next_ready > structure finalization > audit_pending > なし。
290
+ // ready frontierが在る間はADR 0063の並列開始コマンドが勝つ
288
291
  // ——ここを監査で上書きするとdispatchが再直列化する。監査待ちはtodo statusの
289
292
  // audit_pending欄に常在するので、順位を下げても消えない。
290
293
  // commandはverbatim実行可能で読み取り専用のものにする。`phase review`は
291
294
  // `--reason <text>`のplaceholderを含みjournalを書き換えるので置かない
292
295
  // (`todo phase status`の結果には既に両分岐のguidanceが載っている)。
293
- : todo.audit_pending.length > 0
296
+ : todo.structure_finalization_pending.length > 0
297
+ ? { command: todo.structure_finalization_pending[0].next_commands[0],
298
+ reason: 'structure_finalization_pending' }
299
+ : todo.audit_pending.length > 0
294
300
  ? { command: `lattice todo phase status --plan ${todo.audit_pending[0].plan_key}`,
295
301
  reason: 'audit_pending' }
296
302
  : { command: 'lattice todo status', reason: 'no_ready_task' };
@@ -681,8 +681,10 @@ export async function intakePullTask({ repoRoot, runDir, taskId, environment = p
681
681
  affected_tests: [], graph_evidence: [], witness_provenance: {}, manifest_digest: '',
682
682
  };
683
683
  manifest.manifest_digest = selfDigest(manifest, 'manifest_digest');
684
- verdict.state = 'hold'; verdict.reason = 'boundary_unverified';
685
- verdict.next_action = 'declare_and_recompile'; verdict.detail = { cause: error.code };
684
+ if (verdict.state !== 'hold') {
685
+ verdict.state = 'hold'; verdict.reason = 'boundary_unverified';
686
+ verdict.next_action = 'declare_and_recompile'; verdict.detail = { cause: error.code };
687
+ }
686
688
  }
687
689
  const constraint = verdict.state === 'none'
688
690
  ? planningConstraint(verdict.artifact, taskId, state, member) : null;
@@ -760,8 +762,10 @@ export async function intakePullTask({ repoRoot, runDir, taskId, environment = p
760
762
  affected_tests: [], graph_evidence: [], witness_provenance: {}, manifest_digest: '',
761
763
  };
762
764
  manifest.manifest_digest = selfDigest(manifest, 'manifest_digest');
763
- verdict.state = 'hold'; verdict.reason = 'boundary_unverified';
764
- verdict.next_action = 'declare_and_recompile'; verdict.detail = { cause: error.code };
765
+ if (verdict.state !== 'hold') {
766
+ verdict.state = 'hold'; verdict.reason = 'boundary_unverified';
767
+ verdict.next_action = 'declare_and_recompile'; verdict.detail = { cause: error.code };
768
+ }
765
769
  }
766
770
  const packet = packetFor({ meta: current.meta, taskId, baseSha, manifest });
767
771
  const worktreePath = await ensureScriptedWorktree({ repoRoot, runDir, packet });
@@ -277,7 +277,7 @@ async function resolveExactSymbol({ cwd, target, execute }) {
277
277
  }
278
278
 
279
279
  async function collectOne({ cwd, query, execute, inspectAffectedPath }) {
280
- const { id, operation, target } = query;
280
+ const { id, operation, target, exact_path: exactPath } = query;
281
281
  if (!OPERATIONS.has(operation)) {
282
282
  return { id, operation, outcome: 'unsupported' };
283
283
  }
@@ -332,6 +332,11 @@ async function collectOne({ cwd, query, execute, inspectAffectedPath }) {
332
332
  if (operation !== 'status' && (typeof target !== 'string' || target.length === 0)) {
333
333
  return { id, operation, outcome: 'unresolved' };
334
334
  }
335
+ if (Object.hasOwn(query, 'exact_path')
336
+ && (!['callers', 'callees'].includes(operation)
337
+ || typeof exactPath !== 'string' || exactPath.length === 0 || path.isAbsolute(exactPath))) {
338
+ return { id, operation, target, outcome: 'unresolved' };
339
+ }
335
340
 
336
341
  const command = {
337
342
  operation,
@@ -343,7 +348,8 @@ async function collectOne({ cwd, query, execute, inspectAffectedPath }) {
343
348
  args: operation === 'status'
344
349
  ? ['status', '.', '--json']
345
350
  : ['callers', 'callees'].includes(operation)
346
- ? [operation, target, '--path', '.', '--limit', '200', '--json']
351
+ ? [operation, target, '--path', '.', '--limit', '200',
352
+ ...(exactPath === undefined ? [] : ['--exact-path', exactPath]), '--json']
347
353
  : [operation, target, '--path', '.', '--json'],
348
354
  };
349
355
  const result = await executeCommand(execute, command);
@@ -90,11 +90,7 @@ function normalizeOptions(options) {
90
90
  return { countCap, representativeLimit };
91
91
  }
92
92
 
93
- export function projectTodoChainV1(
94
- mergedTodoTopology,
95
- options,
96
- ) {
97
- const { countCap, representativeLimit } = normalizeOptions(options);
93
+ function normalizeTodoTopology(mergedTodoTopology) {
98
94
  if (!isPlainObject(mergedTodoTopology)) {
99
95
  fail('TODO_CHAIN_INVALID_TOPOLOGY', 'mergedTodoTopology must be an object');
100
96
  }
@@ -145,6 +141,24 @@ export function projectTodoChainV1(
145
141
  );
146
142
  }
147
143
  }
144
+ return { refsByKey, edges };
145
+ }
146
+
147
+ /** structure等の消費側が同じToDo node/edge正本を使うためのlossless投影。 */
148
+ export function projectTodoTopologyDagV1(mergedTodoTopology) {
149
+ const { refsByKey, edges } = normalizeTodoTopology(mergedTodoTopology);
150
+ return {
151
+ nodes: [...refsByKey].map(([key, ref]) => ({ key, ref: { ...ref } })),
152
+ edges: edges.map(([from, to]) => ({ from, to })),
153
+ };
154
+ }
155
+
156
+ export function projectTodoChainV1(
157
+ mergedTodoTopology,
158
+ options,
159
+ ) {
160
+ const { countCap, representativeLimit } = normalizeOptions(options);
161
+ const { refsByKey, edges } = normalizeTodoTopology(mergedTodoTopology);
148
162
 
149
163
  let analysis;
150
164
  try {