@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 +3 -0
- package/package.json +4 -2
- package/src/cli-stdio.mjs +40 -0
- package/src/todo-store.mjs +41 -8
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.
|
|
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
|
+
}
|
package/src/todo-store.mjs
CHANGED
|
@@ -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
|
|
1090
|
-
|
|
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:
|
|
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:
|
|
1798
|
+
phase_id: mappedPhaseId,
|
|
1771
1799
|
} : { ...task,
|
|
1772
1800
|
task_id: taskIdMap.get(task.task_id) ?? task.task_id,
|
|
1773
|
-
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');
|