@quolu/lattice 0.12.8 → 0.12.10

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/bin/lattice.mjs CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { installPipeCloseGuard } from '../src/cli-stdio.mjs';
3
4
  import { runRuntimeCli } from '../src/runtime-cli.mjs';
4
5
  import { renderCliHelp } from '../src/cli-help.mjs';
5
6
  import packageJson from '../package.json' with { type: 'json' };
6
7
 
8
+ installPipeCloseGuard();
9
+
7
10
  const args = process.argv.slice(2);
8
11
  const help = renderCliHelp(args);
9
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.12.8",
3
+ "version": "0.12.10",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,10 +49,12 @@
49
49
  },
50
50
  "scripts": {
51
51
  "prepack": "npm --prefix sensor run build",
52
+ "verify:release-commit": "node scripts/verify-release-commit.mjs",
53
+ "prepublishOnly": "npm run verify:release-commit",
52
54
  "test": "node scripts/run-product-tests.mjs",
53
55
  "test:sensor": "npm --prefix sensor test",
54
56
  "precheck": "node --check src/bridge-launch-agent.mjs",
55
- "check": "node --check bin/lattice.mjs && node --check bin/lattice-mcp.mjs && node --check bin/lattice-dashboard.mjs && node --check bin/lattice-bridge.mjs && node --check src/bridge-config.mjs && node --check src/bridge-server.mjs && node --check src/bridge-daemon.mjs && node --check src/bridge-cli.mjs && node --check src/project-cli.mjs && node --check src/sensor-cli.mjs && node --check src/sensor-runtime.mjs && node --check src/sensor-adapter.mjs && node --check src/factory-diagnostics.mjs && node --check src/runtime-errors.mjs && node --check src/runtime-contracts.mjs && node --check src/runtime-event-store.mjs && node --check src/hash-chain.mjs && node --check src/dag-chain.mjs && node --check src/todo-contracts.mjs && node --check src/todo-chain.mjs && node --check src/todo-store.mjs && node --check src/todo-migration.mjs && node --check src/todo-revision.mjs && node --check src/todo-status.mjs && node --check src/todo-cli.mjs && node --check src/todo-dashboard-registry.mjs && node --check src/todo-gantt-presentation.mjs && node --check src/todo-gantt-live.mjs && node --check src/bounded-seam.mjs && node --check src/todo-narrative-anchor.mjs && node --check src/todo-markdown-renderer.mjs && node --check src/todo-gantt-svg.mjs && node --check src/todo-gantt-html.mjs && node --check src/runtime-projection.mjs && node --check src/runtime-decision-verifier.mjs && node --check src/runtime-front-end.mjs && node --check src/runtime-cli.mjs && node --check src/rc3-dogfood-scaffold.mjs && node --check src/runtime-engine.mjs && node --check src/runtime-scripted-executor.mjs && node --check src/runtime-diff-observer.mjs && node --check src/runtime-worktree-executor.mjs && node --check src/runtime-hold-recompile.mjs && node --check src/rc3-scripted-campaign.mjs && node --check src/rc3-actual-dogfood.mjs && node --check src/rc4-stage1-dogfood.mjs && node --check research/fixtures/dispatch-record/src/dispatch-record.mjs && node --check test/research-dispatch-record.test.mjs",
57
+ "check": "node --check bin/lattice.mjs && node --check bin/lattice-mcp.mjs && node --check bin/lattice-dashboard.mjs && node --check bin/lattice-bridge.mjs && node --check src/cli-stdio.mjs && node --check src/bridge-config.mjs && node --check src/bridge-server.mjs && node --check src/bridge-daemon.mjs && node --check src/bridge-cli.mjs && node --check src/project-cli.mjs && node --check src/sensor-cli.mjs && node --check src/sensor-runtime.mjs && node --check src/sensor-adapter.mjs && node --check src/factory-diagnostics.mjs && node --check src/runtime-errors.mjs && node --check src/runtime-contracts.mjs && node --check src/runtime-event-store.mjs && node --check src/hash-chain.mjs && node --check src/dag-chain.mjs && node --check src/todo-contracts.mjs && node --check src/todo-chain.mjs && node --check src/todo-store.mjs && node --check src/todo-migration.mjs && node --check src/todo-revision.mjs && node --check src/todo-status.mjs && node --check src/todo-cli.mjs && node --check src/todo-dashboard-registry.mjs && node --check src/todo-gantt-presentation.mjs && node --check src/todo-gantt-live.mjs && node --check src/bounded-seam.mjs && node --check src/todo-narrative-anchor.mjs && node --check src/todo-markdown-renderer.mjs && node --check src/todo-gantt-svg.mjs && node --check src/todo-gantt-html.mjs && node --check src/runtime-projection.mjs && node --check src/runtime-decision-verifier.mjs && node --check src/runtime-front-end.mjs && node --check src/runtime-cli.mjs && node --check src/rc3-dogfood-scaffold.mjs && node --check src/runtime-engine.mjs && node --check src/runtime-scripted-executor.mjs && node --check src/runtime-diff-observer.mjs && node --check src/runtime-worktree-executor.mjs && node --check src/runtime-hold-recompile.mjs && node --check src/rc3-scripted-campaign.mjs && node --check src/rc3-actual-dogfood.mjs && node --check src/rc4-stage1-dogfood.mjs && node --check research/fixtures/dispatch-record/src/dispatch-record.mjs && node --check test/research-dispatch-record.test.mjs",
56
58
  "check:project-identity": "node --check src/project-identity.mjs",
57
59
  "ci": "npm run test && npm run test:sensor && npm run check && npm run check:project-identity"
58
60
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CLI stdio guards.
3
+ *
4
+ * Reading part of a result is a legitimate use: `lattice todo status --json |
5
+ * head` stops the consumer as soon as it has what it wants. When that consumer
6
+ * exits, the next write to stdout raises EPIPE, and Node's default handling
7
+ * turns it into an unhandled 'error' event — the CLI dies with a stack trace
8
+ * and a non-zero exit, so a working pipeline reports a failure that never
9
+ * happened.
10
+ *
11
+ * EPIPE from a closed consumer is the POSIX contract, not a fault, so the
12
+ * process stops quietly. This is deliberately narrow: only EPIPE, only on the
13
+ * output streams. Every other stream error keeps its previous behaviour and
14
+ * still crashes the CLI, because those are real faults.
15
+ */
16
+
17
+ /**
18
+ * @param {object} [options]
19
+ * @param {Array<NodeJS.WriteStream>} [options.streams] streams to guard.
20
+ * @param {(code: number) => void} [options.exit] process exit, injectable for tests.
21
+ * @returns {() => void} removes the guards again (used by tests).
22
+ */
23
+ export function installPipeCloseGuard({
24
+ streams = [process.stdout, process.stderr],
25
+ exit = (code) => process.exit(code),
26
+ } = {}) {
27
+ const installed = [];
28
+ for (const stream of streams) {
29
+ if (stream === undefined || stream === null || typeof stream.on !== 'function') continue;
30
+ const onError = (error) => {
31
+ if (error?.code !== 'EPIPE') throw error;
32
+ exit(0);
33
+ };
34
+ stream.on('error', onError);
35
+ installed.push(() => stream.off('error', onError));
36
+ }
37
+ return () => {
38
+ for (const remove of installed) remove();
39
+ };
40
+ }
@@ -1082,17 +1082,41 @@ function nextEvent(input, storeMember) {
1082
1082
  return event;
1083
1083
  }
1084
1084
 
1085
+ /**
1086
+ * The digest replay binds a task's completion to.
1087
+ *
1088
+ * A ToDo completed inside this plan version binds to its own `done` event. A
1089
+ * ToDo carried across a revision has no `done` event in the successor journal —
1090
+ * its completion arrives with the `plan_genesis` state migration, and replay
1091
+ * binds it to the genesis digest (see the carry branch in `replay`). Resolving
1092
+ * only the first case is what made `reopen` unusable on carried work.
1093
+ *
1094
+ * Returns null when the task has no completion to bind to at all.
1095
+ */
1096
+ function resolveDoneBindingDigest(storeMember, taskId) {
1097
+ const events = storeMember.journal.events;
1098
+ const authored = [...events].reverse().find((event) => (
1099
+ event.kind === 'done' && event.task_id === taskId
1100
+ ));
1101
+ if (authored !== undefined) return authored.event_digest;
1102
+ const genesis = events[0];
1103
+ if (genesis?.kind !== 'plan_genesis' || !Array.isArray(genesis.state_migration)) return null;
1104
+ const carried = genesis.state_migration.some((migration) => (
1105
+ ['carry', 'carry_reconciled_metadata'].includes(migration.state_policy)
1106
+ && migration.to_task_id === taskId && migration.state?.status === 'done'
1107
+ ));
1108
+ return carried ? genesis.event_digest : null;
1109
+ }
1110
+
1085
1111
  function resolveTargetedEvent(input, storeMember) {
1086
1112
  if (input.kind === 'reopen' && exactRecord(input.payload, [
1087
1113
  'reason', 'override_reason',
1088
1114
  ])) {
1089
- const target = [...storeMember.journal.events].reverse().find((event) => (
1090
- event.kind === 'done' && event.task_id === input.task_id
1091
- ));
1092
- if (target === undefined) fail('STORE_INCONSISTENT', 'invalid_reopen_binding');
1115
+ const targetDigest = resolveDoneBindingDigest(storeMember, input.task_id);
1116
+ if (targetDigest === null) fail('STORE_INCONSISTENT', 'invalid_reopen_binding');
1093
1117
  return {
1094
1118
  ...input,
1095
- payload: { ...input.payload, target_done_digest: target.event_digest },
1119
+ payload: { ...input.payload, target_done_digest: targetDigest },
1096
1120
  };
1097
1121
  }
1098
1122
  if (input.kind === 'done' && exactRecord(input.payload, [
@@ -1764,13 +1788,17 @@ function phaseV3CarrySemantics(plan, taskId, taskIdMap, phaseIdMap,
1764
1788
  ? { ...ref, task_id: taskIdMap.get(ref.task_id) ?? ref.task_id } : ref;
1765
1789
  const mapPhaseRef = (ref) => ref.project_id === plan.project_id && ref.plan_key === plan.plan_key
1766
1790
  ? { ...ref, phase_id: phaseIdMap.get(ref.phase_id) ?? ref.phase_id } : ref;
1791
+ // Phaseを持たない世代(todo_plan.v3)のtaskはphase_idを持たない。undefinedのまま
1792
+ // canonicalizeするとJSON treeでないとしてTypeErrorになり、typedな拒否理由が失われる。
1793
+ // nullへ正規化してcarry比較へ渡し、Phase割当ての獲得をcarry_semantics_changedとして拒否する。
1794
+ const mappedPhaseId = phaseIdMap.get(task.phase_id) ?? task.phase_id ?? null;
1767
1795
  const mappedTask = reconciliationMetadata ? {
1768
1796
  task_id: taskIdMap.get(task.task_id) ?? task.task_id, title: task.title, lane: task.lane,
1769
1797
  compile_binding: task.compile_binding,
1770
- phase_id: phaseIdMap.get(task.phase_id) ?? task.phase_id,
1798
+ phase_id: mappedPhaseId,
1771
1799
  } : { ...task,
1772
1800
  task_id: taskIdMap.get(task.task_id) ?? task.task_id,
1773
- phase_id: phaseIdMap.get(task.phase_id) ?? task.phase_id,
1801
+ phase_id: mappedPhaseId,
1774
1802
  parent_task_id: task.parent_task_id === null ? null
1775
1803
  : taskIdMap.get(task.parent_task_id) ?? task.parent_task_id,
1776
1804
  };
@@ -2933,7 +2961,12 @@ export async function applyPhaseTodoRevision(options = {}) {
2933
2961
  const descriptor = currentManifest.members.find(({ plan_key }) => plan_key === revision.plan_key);
2934
2962
  Object.assign(descriptor, { active_plan_version: revision.desired_plan.plan_version,
2935
2963
  plan_ref: planRef, journal_ref: journalRef, snapshot_ref: snapshotRef,
2936
- topology_digest: revision.desired_plan.topology_digest, journal_head_digest: genesis.event_digest });
2964
+ topology_digest: revision.desired_plan.topology_digest, journal_head_digest: genesis.event_digest,
2965
+ // manifest v2はactive_revision_digestがgenesisのrevision_digestと一致することを読み取り時に
2966
+ // 要求する。v3昇格後のstoreでここを据え置くと、書込みは成功したように見えるのに以後の
2967
+ // readがmanifest_revision_binding_mismatchで落ちる。v1 memberはこのkeyを持てない。
2968
+ ...(currentManifest.schema === 'lattice.todo_manifest.v2'
2969
+ ? { active_revision_digest: revision.revision_digest } : {}) });
2937
2970
  currentManifest.manifest_digest = todoSelfDigest(currentManifest, 'manifest_digest');
2938
2971
  await atomicWrite(path.resolve(repoRoot, MANIFEST_REF), canonicalLine(currentManifest));
2939
2972
  await protocolStage(options, 'phase_revision_manifest_activated');