@quolu/lattice 0.58.4 → 0.59.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/package.json +1 -1
- package/src/cli-help.mjs +1 -1
- package/src/todo-cli.mjs +22 -6
- package/src/todo-contracts.mjs +39 -11
- package/src/todo-store.mjs +25 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
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/cli-help.mjs
CHANGED
|
@@ -262,7 +262,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
|
|
|
262
262
|
'todo retract': 'todo retract --plan <key> --task <id> --reason <text>',
|
|
263
263
|
'todo block': 'todo block --plan <key> --task <id> --reason <text>',
|
|
264
264
|
'todo unblock': 'todo unblock --plan <key> --task <id>',
|
|
265
|
-
'todo done': 'todo done --plan <key> --task <id> --evidence <file>',
|
|
265
|
+
'todo done': 'todo done --plan <key> --task <id> --evidence <file> [--test-result <markdown-file>]',
|
|
266
266
|
'todo reopen': 'todo reopen --plan <key> --task <id> --reason <text> [--override-reason <text>]',
|
|
267
267
|
'todo evidence': 'todo evidence promote --plan <key> --task <id> --evidence <file>',
|
|
268
268
|
'todo evidence promote': 'todo evidence promote --plan <key> --task <id> --evidence <file>',
|
package/src/todo-cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { parseTree } from 'jsonc-parser';
|
|
|
10
10
|
import {
|
|
11
11
|
TODO_COORDINATION_MODES,
|
|
12
12
|
TODO_DESIGN_MEMO_PROMPT,
|
|
13
|
+
TODO_TEST_RESULT_CONTRACT_ID,
|
|
13
14
|
canonicalizeTodoArtifact,
|
|
14
15
|
digestTodoArtifact,
|
|
15
16
|
exactRecord,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
isTodoDesignMemo,
|
|
19
20
|
isTodoIdentifier,
|
|
20
21
|
isTodoRef,
|
|
22
|
+
isTodoTestResult,
|
|
21
23
|
todoSelfDigest,
|
|
22
24
|
validateEvidenceDescriptor,
|
|
23
25
|
} from './todo-contracts.mjs';
|
|
@@ -333,6 +335,16 @@ async function readNoteTextInput(repoRoot, inputRef) {
|
|
|
333
335
|
catch { throw new TodoStoreError('INPUT_UNREADABLE', 'note_input_invalid_utf8'); }
|
|
334
336
|
}
|
|
335
337
|
|
|
338
|
+
async function readTestResultInput(repoRoot, inputRef) {
|
|
339
|
+
const result = await readNoteTextInput(repoRoot, inputRef);
|
|
340
|
+
if (!isTodoTestResult(result)) {
|
|
341
|
+
throw new TodoStoreError('INVALID_TEST_RESULT', 'test_result_must_be_non_empty_markdown', undefined, {
|
|
342
|
+
contract_id: TODO_TEST_RESULT_CONTRACT_ID,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
|
|
336
348
|
function taskRef(plan, taskId) {
|
|
337
349
|
return { project_id: plan.project_id, plan_key: plan.plan_key, task_id: taskId };
|
|
338
350
|
}
|
|
@@ -650,12 +662,15 @@ function terminalAuditDoneAdvisory(plan, phases) {
|
|
|
650
662
|
|
|
651
663
|
async function mutate({
|
|
652
664
|
repoRoot, env, planKey, taskId, kind, payload, evidenceRef, advisory = null,
|
|
653
|
-
noteContext = null, structureContext = null,
|
|
665
|
+
noteContext = null, structureContext = null, testResultRef = null,
|
|
654
666
|
}) {
|
|
655
667
|
const actor = mutationActor(env);
|
|
656
668
|
const evidence = evidenceRef === null ? null : await readEvidenceInput(repoRoot, evidenceRef);
|
|
669
|
+
const testResult = testResultRef === null ? null : await readTestResultInput(repoRoot, testResultRef);
|
|
657
670
|
let eventPayload = payload;
|
|
658
|
-
if (kind === 'done' && payload === 'authored')
|
|
671
|
+
if (kind === 'done' && payload === 'authored') {
|
|
672
|
+
eventPayload = { evidence, ...(testResult === null ? {} : { test_result: testResult }) };
|
|
673
|
+
}
|
|
659
674
|
if (kind === 'done' && payload === 'evidence_promotion') {
|
|
660
675
|
eventPayload = { done_mode: 'evidence_promotion', imported: true, evidence };
|
|
661
676
|
}
|
|
@@ -1672,7 +1687,7 @@ async function todoDetail({ repoRoot, planKey, taskId }) {
|
|
|
1672
1687
|
repoRoot, store, planKey, taskId: task.task_id,
|
|
1673
1688
|
});
|
|
1674
1689
|
const result = {
|
|
1675
|
-
schema: 'lattice.todo_detail_result.
|
|
1690
|
+
schema: 'lattice.todo_detail_result.v3',
|
|
1676
1691
|
project_id: store.project_id,
|
|
1677
1692
|
plan_key: planKey,
|
|
1678
1693
|
plan_version: member.plan.plan_version,
|
|
@@ -3926,12 +3941,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3926
3941
|
&& argv[3] === '--task' && isTodoIdentifier(argv[4])) {
|
|
3927
3942
|
action = (repoRoot) => mutate({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
3928
3943
|
kind: 'unblock', payload: {}, evidenceRef: null });
|
|
3929
|
-
} else if (argv.length === 7 && argv[0] === 'done'
|
|
3944
|
+
} else if ((argv.length === 7 || argv.length === 9) && argv[0] === 'done'
|
|
3930
3945
|
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
3931
3946
|
&& argv[3] === '--task' && isTodoIdentifier(argv[4])
|
|
3932
|
-
&& argv[5] === '--evidence' && isTodoRef(argv[6])
|
|
3947
|
+
&& argv[5] === '--evidence' && isTodoRef(argv[6])
|
|
3948
|
+
&& (argv.length === 7 || (argv[7] === '--test-result' && isTodoRef(argv[8])))) {
|
|
3933
3949
|
action = (repoRoot) => mutate({ repoRoot, env, planKey: argv[2], taskId: argv[4],
|
|
3934
|
-
kind: 'done', payload: 'authored', evidenceRef: argv[6] });
|
|
3950
|
+
kind: 'done', payload: 'authored', evidenceRef: argv[6], testResultRef: argv[8] ?? null });
|
|
3935
3951
|
} else if (argv.length === 8 && argv[0] === 'evidence' && argv[1] === 'promote'
|
|
3936
3952
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
3937
3953
|
&& argv[4] === '--task' && isTodoIdentifier(argv[5])
|
package/src/todo-contracts.mjs
CHANGED
|
@@ -62,10 +62,14 @@ const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
|
62
62
|
const NOTE_FORBIDDEN_CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
63
63
|
|
|
64
64
|
export const TODO_DESIGN_MEMO_PROMPT = 'あなたがこのToDoに対して、何も考えていないならば、設計メモに `NO_PLAN` と書いてください';
|
|
65
|
+
export const TODO_TEST_RESULT_CONTRACT_ID = 'lattice.todo_test_result.v1';
|
|
65
66
|
|
|
66
67
|
export const isTodoDigest = (value) => typeof value === 'string' && DIGEST.test(value);
|
|
67
68
|
export const isTodoIdentifier = (value) => typeof value === 'string' && IDENTIFIER.test(value);
|
|
68
69
|
export const isNonNegativeSafeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
70
|
+
export const isTodoTestResult = (value) => typeof value === 'string' && value.trim().length > 0
|
|
71
|
+
&& Buffer.byteLength(value, 'utf8') <= TODO_LIMITS.noteBodyBytes
|
|
72
|
+
&& !NOTE_FORBIDDEN_CONTROL.test(value);
|
|
69
73
|
|
|
70
74
|
export function isStrictTodoTimestamp(value) {
|
|
71
75
|
return isCanonicalUtcTimestamp(value);
|
|
@@ -527,8 +531,10 @@ function validPayload(event) {
|
|
|
527
531
|
if (event.kind === 'block') return exactRecord(payload, ['reason']) && nullableText(payload.reason) && payload.reason !== null;
|
|
528
532
|
if (event.kind === 'unblock') return exactRecord(payload, []);
|
|
529
533
|
if (event.kind === 'done' && payload?.done_mode === 'authored') {
|
|
530
|
-
return exactRecord(payload, ['done_mode', 'imported', 'evidence'])
|
|
531
|
-
|
|
534
|
+
return (exactRecord(payload, ['done_mode', 'imported', 'evidence'])
|
|
535
|
+
|| exactRecord(payload, ['done_mode', 'imported', 'evidence', 'test_result']))
|
|
536
|
+
&& payload.imported === false && evidence(payload.evidence)
|
|
537
|
+
&& (payload.test_result === undefined || isTodoTestResult(payload.test_result));
|
|
532
538
|
}
|
|
533
539
|
if (event.kind === 'done' && payload?.done_mode === 'historical_import') {
|
|
534
540
|
return exactRecord(payload, ['done_mode', 'imported', 'status', 'completed_at', 'evidence'])
|
|
@@ -569,22 +575,29 @@ function validPayload(event) {
|
|
|
569
575
|
}
|
|
570
576
|
|
|
571
577
|
function validCarriedState(value) {
|
|
572
|
-
|
|
578
|
+
const legacy = exactRecord(value, [
|
|
573
579
|
'status', 'started_at', 'done_at', 'blocked_reason', 'evidence', 'imported',
|
|
574
|
-
])
|
|
580
|
+
]);
|
|
581
|
+
const resultAware = exactRecord(value, [
|
|
582
|
+
'status', 'started_at', 'done_at', 'blocked_reason', 'evidence', 'imported', 'test_result',
|
|
583
|
+
]);
|
|
584
|
+
if ((!legacy && !resultAware) || !['pending', 'in-progress', 'blocked', 'done'].includes(value.status)
|
|
575
585
|
|| (value.started_at !== null && !isStrictTodoTimestamp(value.started_at))
|
|
576
586
|
|| (value.done_at !== null && !isStrictTodoTimestamp(value.done_at))
|
|
577
587
|
|| (value.blocked_reason !== null && !nullableText(value.blocked_reason))
|
|
578
588
|
|| typeof value.imported !== 'boolean') return false;
|
|
589
|
+
const testResult = resultAware ? value.test_result : null;
|
|
590
|
+
if (testResult !== null && !isTodoTestResult(testResult)) return false;
|
|
579
591
|
if (value.status === 'pending') return value.started_at === null && value.done_at === null
|
|
580
|
-
&& value.blocked_reason === null && value.evidence === null && value.imported === false
|
|
592
|
+
&& value.blocked_reason === null && value.evidence === null && value.imported === false
|
|
593
|
+
&& testResult === null;
|
|
581
594
|
const activeEvidenceValid = value.imported
|
|
582
595
|
? value.evidence === null || validateTodoImportSource(value.evidence)
|
|
583
596
|
: value.evidence === null;
|
|
584
597
|
if (value.status === 'in-progress') return value.done_at === null && value.blocked_reason === null
|
|
585
|
-
&& activeEvidenceValid;
|
|
598
|
+
&& activeEvidenceValid && testResult === null;
|
|
586
599
|
if (value.status === 'blocked') return value.done_at === null && value.blocked_reason !== null
|
|
587
|
-
&& activeEvidenceValid;
|
|
600
|
+
&& activeEvidenceValid && testResult === null;
|
|
588
601
|
return value.blocked_reason === null && value.evidence !== null
|
|
589
602
|
&& (value.imported ? validateTodoImportSource(value.evidence) : evidence(value.evidence));
|
|
590
603
|
}
|
|
@@ -700,20 +713,35 @@ export function validateTodoSnapshot(value) {
|
|
|
700
713
|
'schema', 'project_id', 'plan_key', 'plan_version', 'projection_version', 'through_sequence',
|
|
701
714
|
'journal_head_digest', 'tasks', 'phases', 'snapshot_digest',
|
|
702
715
|
]);
|
|
703
|
-
|
|
716
|
+
const v3 = value?.schema === 'lattice.todo_snapshot.v3' && exactRecord(value, [
|
|
717
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'projection_version', 'through_sequence',
|
|
718
|
+
'journal_head_digest', 'tasks', 'snapshot_digest',
|
|
719
|
+
]);
|
|
720
|
+
const v4 = value?.schema === 'lattice.todo_snapshot.v4' && exactRecord(value, [
|
|
721
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'projection_version', 'through_sequence',
|
|
722
|
+
'journal_head_digest', 'tasks', 'phases', 'snapshot_digest',
|
|
723
|
+
]);
|
|
724
|
+
const resultAware = v3 || v4;
|
|
725
|
+
const phaseAware = v2 || v4;
|
|
726
|
+
return (v1 || v2 || v3 || v4) && isTodoIdentifier(value.project_id)
|
|
704
727
|
&& isTodoIdentifier(value.plan_key) && isTodoIdentifier(value.plan_version)
|
|
705
|
-
&& value.projection_version === (v1 ? 1 : 2
|
|
728
|
+
&& value.projection_version === (v1 ? 1 : v2 ? 2 : v3 ? 3 : 4)
|
|
729
|
+
&& isNonNegativeSafeInteger(value.through_sequence)
|
|
706
730
|
&& isTodoDigest(value.journal_head_digest) && Array.isArray(value.tasks)
|
|
707
731
|
&& value.tasks.length > 0 && value.tasks.length <= TODO_LIMITS.tasksPerPlan
|
|
708
732
|
&& value.tasks.every((entry) => exactRecord(entry, [
|
|
709
733
|
'task_id', 'status', 'started_at', 'done_at', 'blocked_reason', 'evidence', 'evidence_unverified', 'imported',
|
|
734
|
+
...(resultAware ? ['test_result'] : []),
|
|
710
735
|
]) && isTodoIdentifier(entry.task_id) && ['pending', 'in-progress', 'blocked', 'done'].includes(entry.status)
|
|
711
736
|
&& (entry.started_at === null || isStrictTodoTimestamp(entry.started_at))
|
|
712
737
|
&& (entry.done_at === null || isStrictTodoTimestamp(entry.done_at)) && nullableText(entry.blocked_reason)
|
|
713
738
|
&& (entry.evidence === null || evidence(entry.evidence) || validateTodoImportSource(entry.evidence))
|
|
714
|
-
&& typeof entry.evidence_unverified === 'boolean' && typeof entry.imported === 'boolean'
|
|
739
|
+
&& typeof entry.evidence_unverified === 'boolean' && typeof entry.imported === 'boolean'
|
|
740
|
+
&& (!resultAware || (entry.status === 'done'
|
|
741
|
+
? entry.test_result === null || isTodoTestResult(entry.test_result)
|
|
742
|
+
: entry.test_result === null)))
|
|
715
743
|
&& value.tasks.every((entry, index) => index === 0 || value.tasks[index - 1].task_id < entry.task_id)
|
|
716
|
-
&& (!
|
|
744
|
+
&& (!phaseAware || (Array.isArray(value.phases) && value.phases.length > 0
|
|
717
745
|
&& value.phases.every((entry) => exactRecord(entry, [
|
|
718
746
|
'phase_id', 'status', 'review_event_digest', 'decision_event_digest', 'decision_evidence',
|
|
719
747
|
]) && isTodoIdentifier(entry.phase_id)
|
package/src/todo-store.mjs
CHANGED
|
@@ -302,7 +302,7 @@ async function readPlanScopedJournal(repoRoot, journalRef) {
|
|
|
302
302
|
|
|
303
303
|
function taskState(taskId) {
|
|
304
304
|
return { task_id: taskId, status: 'pending', started_at: null, done_at: null, blocked_reason: null,
|
|
305
|
-
evidence: null, evidence_unverified: false, imported: false };
|
|
305
|
+
evidence: null, evidence_unverified: false, imported: false, test_result: null };
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
function emptyPhaseState(phaseId) {
|
|
@@ -685,6 +685,7 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
685
685
|
plan_key: plan.plan_key, task_id: event.task_id,
|
|
686
686
|
});
|
|
687
687
|
state.status = 'done'; state.done_at = event.recorded_at; state.evidence = event.payload.evidence;
|
|
688
|
+
state.test_result = event.payload.test_result ?? null;
|
|
688
689
|
state.imported = false;
|
|
689
690
|
completion.set(event.task_id, { mode: 'authored', completed_at: event.recorded_at });
|
|
690
691
|
} else if (event.payload.done_mode === 'historical_import') {
|
|
@@ -731,7 +732,7 @@ function replay(plan, events, { now = new Date(), verifyEvidence, verifyImportSo
|
|
|
731
732
|
}
|
|
732
733
|
const startedSuccessor = localSuccessors(plan, event.task_id).some((id) => states.get(id).status !== 'pending');
|
|
733
734
|
if (startedSuccessor && event.payload.override_reason === null) fail('STORE_INCONSISTENT', 'reopen_has_started_successor');
|
|
734
|
-
state.status = 'in-progress'; state.done_at = null; state.evidence = null;
|
|
735
|
+
state.status = 'in-progress'; state.done_at = null; state.evidence = null; state.test_result = null;
|
|
735
736
|
completion.delete(event.task_id);
|
|
736
737
|
}
|
|
737
738
|
}
|
|
@@ -774,11 +775,21 @@ function snapshotFor(plan, events, tasks) {
|
|
|
774
775
|
// ——ADR 0147以降の暗黙terminal-audit Phaseの状態は、この関数の外(readTodoStore/
|
|
775
776
|
// appendTodoEventが返す`phases`という導出ビュー)で供給する。
|
|
776
777
|
const phasePlan = isPhaseTodoPlanSchema(plan.schema);
|
|
778
|
+
const resultAware = events.some((event) => event.kind === 'done'
|
|
779
|
+
&& typeof event.payload?.test_result === 'string')
|
|
780
|
+
|| events.some((event) => event.kind === 'plan_genesis'
|
|
781
|
+
&& event.state_migration?.some(({ state }) => typeof state?.test_result === 'string'));
|
|
782
|
+
const snapshotTasks = resultAware ? tasks.map((task) => ({ ...task, test_result: task.test_result ?? null }))
|
|
783
|
+
: tasks.map(({ test_result: _testResult, ...task }) => task);
|
|
777
784
|
const snapshot = {
|
|
778
|
-
schema:
|
|
785
|
+
schema: resultAware
|
|
786
|
+
? (phasePlan ? 'lattice.todo_snapshot.v4' : 'lattice.todo_snapshot.v3')
|
|
787
|
+
: (phasePlan ? 'lattice.todo_snapshot.v2' : 'lattice.todo_snapshot.v1'),
|
|
779
788
|
project_id: plan.project_id, plan_key: plan.plan_key,
|
|
780
|
-
plan_version: plan.plan_version,
|
|
781
|
-
|
|
789
|
+
plan_version: plan.plan_version,
|
|
790
|
+
projection_version: resultAware ? (phasePlan ? 4 : 3) : (phasePlan ? 2 : 1),
|
|
791
|
+
through_sequence: head.sequence,
|
|
792
|
+
journal_head_digest: head.event_digest, tasks: snapshotTasks, snapshot_digest: '',
|
|
782
793
|
...(phasePlan ? { phases: projectPhaseStates(plan, events,
|
|
783
794
|
new Map(tasks.map((task) => [task.task_id, task]))) } : {}),
|
|
784
795
|
};
|
|
@@ -1515,9 +1526,14 @@ export async function rebuildTodoSnapshot(options = {}) {
|
|
|
1515
1526
|
|
|
1516
1527
|
function nextEvent(input, storeMember) {
|
|
1517
1528
|
const previous = storeMember.journal.events.at(-1);
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1529
|
+
let payload = input.payload;
|
|
1530
|
+
if (input.kind === 'done' && (exactRecord(input.payload, ['evidence'])
|
|
1531
|
+
|| exactRecord(input.payload, ['evidence', 'test_result']))) {
|
|
1532
|
+
payload = {
|
|
1533
|
+
done_mode: 'authored', imported: false, evidence: input.payload.evidence,
|
|
1534
|
+
...(input.payload.test_result === undefined ? {} : { test_result: input.payload.test_result }),
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1521
1537
|
const phaseCapablePlan = isPhaseTodoPlanSchema(storeMember.plan.schema);
|
|
1522
1538
|
const phaseKind = ['phase_review', 'phase_accept', 'phase_reject', 'phase_reopen', 'phase_close_unaudited']
|
|
1523
1539
|
.includes(input.kind);
|
|
@@ -2613,6 +2629,7 @@ function stateMigrationFor(previous, revision) {
|
|
|
2613
2629
|
return { ...migration, state: {
|
|
2614
2630
|
status: state.status, started_at: state.started_at, done_at: state.done_at,
|
|
2615
2631
|
blocked_reason: state.blocked_reason, evidence: state.evidence, imported: state.imported,
|
|
2632
|
+
...(typeof state.test_result === 'string' ? { test_result: state.test_result } : {}),
|
|
2616
2633
|
} };
|
|
2617
2634
|
});
|
|
2618
2635
|
}
|