@quolu/lattice 0.24.0 → 0.26.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 +3 -5
- package/src/cli-help.mjs +1 -0
- package/src/runtime-hold-recompile.mjs +13 -1
- package/src/runtime-seam-treatment.mjs +20 -1
- package/src/seam-commit.mjs +0 -0
- package/src/todo-cli.mjs +56 -0
- package/src/witness-scaffold.mjs +150 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,10 +59,8 @@
|
|
|
59
59
|
"prepublishOnly": "npm run verify:release-commit",
|
|
60
60
|
"test": "node scripts/run-product-tests.mjs",
|
|
61
61
|
"test:sensor": "npm --prefix sensor test",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"check:project-identity": "node --check src/project-identity.mjs",
|
|
65
|
-
"ci": "npm run test && npm run test:sensor && npm run check && npm run check:project-identity && npm run verify:todo-store",
|
|
62
|
+
"check": "node scripts/check-syntax.mjs",
|
|
63
|
+
"ci": "npm run test && npm run test:sensor && npm run check && npm run verify:todo-store",
|
|
66
64
|
"verify:todo-store": "node bin/lattice.mjs todo verify --json"
|
|
67
65
|
}
|
|
68
66
|
}
|
package/src/cli-help.mjs
CHANGED
|
@@ -70,6 +70,7 @@ Write commands:
|
|
|
70
70
|
evidence promote --plan <key> --task <id> --evidence <file>
|
|
71
71
|
independence compile --plan <key> --input <file> # witness setとsensorから並列可否を記録する
|
|
72
72
|
independence witness migrate --plan <key> # revision後の宣言をtask migrationで写す
|
|
73
|
+
independence witness scaffold --plan <key> --input <draft> # 下書きとfresh観測から宣言を書き出す
|
|
73
74
|
seam-proposal compile --plan <key> # 並列可否記録と実sensorからseam提案を記録する
|
|
74
75
|
seam-proposal apply --plan <key> # 記録済み提案を隔離worktreeで適用し五条件で採否を決める
|
|
75
76
|
seam-proposal land --plan <key> --names <file> # 採用された変換を本ツリーへ着地させる
|
|
@@ -65,6 +65,7 @@ function sha16(value) {
|
|
|
65
65
|
|
|
66
66
|
const WITNESS_KINDS = Object.freeze(['state', 'schema', 'invariant', 'effect', 'external_effect']);
|
|
67
67
|
const HEX_DIGEST = /^[0-9a-f]{64}$/u;
|
|
68
|
+
const HEX_SHA1 = /^[0-9a-f]{40}$/u;
|
|
68
69
|
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/u;
|
|
69
70
|
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
70
71
|
|
|
@@ -620,12 +621,19 @@ export function recompileNextEpochPlan(options = {}) {
|
|
|
620
621
|
if (!exactRecord(options, [
|
|
621
622
|
'runId', 'request', 'plan', 'manifests', 'packets', 'events', 'holdDecision',
|
|
622
623
|
'additionalConflicts', 'recordedAt',
|
|
624
|
+
]) && !exactRecord(options, [
|
|
625
|
+
'runId', 'request', 'plan', 'manifests', 'packets', 'events', 'holdDecision',
|
|
626
|
+
'additionalConflicts', 'recordedAt', 'successorBaseSha',
|
|
623
627
|
])) {
|
|
624
628
|
fail('recompileNextEpochPlan optionsがexact shapeでない');
|
|
625
629
|
}
|
|
626
630
|
const {
|
|
627
631
|
runId, request, plan, manifests, packets, events, holdDecision, additionalConflicts, recordedAt,
|
|
632
|
+
successorBaseSha = null,
|
|
628
633
|
} = options;
|
|
634
|
+
if (successorBaseSha !== null && !HEX_SHA1.test(successorBaseSha)) {
|
|
635
|
+
fail('successorBaseShaがgit shaでない');
|
|
636
|
+
}
|
|
629
637
|
if (!validateRuntimePlan(plan)) fail('planがruntime_plan.v1 contractを満たさない');
|
|
630
638
|
if (!validateHoldDecision(holdDecision)) fail('holdDecisionがcontractを満たさない');
|
|
631
639
|
if (!Array.isArray(additionalConflicts)) fail('additionalConflictsがarrayではない');
|
|
@@ -673,7 +681,11 @@ export function recompileNextEpochPlan(options = {}) {
|
|
|
673
681
|
plan_ref: newPlanRef,
|
|
674
682
|
plan_epoch: newEpoch,
|
|
675
683
|
request_digest: request.request_digest,
|
|
676
|
-
|
|
684
|
+
// 変換でsourceの構造が変わったなら、再開先は変換を含むbaseでなければならない。
|
|
685
|
+
// 旧baseのまま再開させると、splitが所有を宣言した新pathがworktreeに存在しない(ADR 0141)。
|
|
686
|
+
// carry-over側のrebind packetはcontent不変が要件なので、そちらは触らない——
|
|
687
|
+
// 継続する作業は自分のworktreeで走り続けており、baseを付け替える対象ではない。
|
|
688
|
+
base_sha: successorBaseSha ?? plan.base_sha,
|
|
677
689
|
nodes: structuredClone(plan.nodes),
|
|
678
690
|
precedence: structuredClone(plan.precedence),
|
|
679
691
|
conflicts: mergedConflicts,
|
|
@@ -98,7 +98,8 @@ export function buildRuntimeSeamSplit({
|
|
|
98
98
|
export async function resolveRuntimeSeamTreatment(options = {}) {
|
|
99
99
|
const {
|
|
100
100
|
finding, witnessSet, pathNames = {}, predeclaredTreatments = [],
|
|
101
|
-
applyConflict, baseSha, manifestDigest, affectedTests = [],
|
|
101
|
+
applyConflict, commitTransform, baseSha, manifestDigest, affectedTests = [],
|
|
102
|
+
taskMigrationDigest,
|
|
102
103
|
} = options;
|
|
103
104
|
|
|
104
105
|
if (finding?.kind === 'observed_write_conflict' && typeof finding.path === 'string') {
|
|
@@ -122,6 +123,13 @@ export async function resolveRuntimeSeamTreatment(options = {}) {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
const applied = await applyConflict({ conflict });
|
|
126
|
+
if (applied?.outcome?.decision === 'accepted' && typeof commitTransform !== 'function') {
|
|
127
|
+
// 確定する手段が無いまま採用すると、splitは所有を宣言するのに再開先へその成果が無い。
|
|
128
|
+
// 「変換した」と言いながら再開できない状態を作らない(ADR 0141)。
|
|
129
|
+
return {
|
|
130
|
+
lane: 'intentional_serial', treatment: null, split: null, reasons: ['committer_absent'],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
125
133
|
if (applied?.outcome?.decision !== 'accepted') {
|
|
126
134
|
return {
|
|
127
135
|
lane: 'intentional_serial',
|
|
@@ -140,6 +148,15 @@ export async function resolveRuntimeSeamTreatment(options = {}) {
|
|
|
140
148
|
if (built.split === null) {
|
|
141
149
|
return { lane: 'intentional_serial', treatment: null, split: null, reasons: built.reasons };
|
|
142
150
|
}
|
|
151
|
+
// 再開先が実在するbaseを要る。branchを動かさずcommitとして確定し、そのshaを後継planへ渡す。
|
|
152
|
+
const committed = await commitTransform({
|
|
153
|
+
conflict, files: applied.files ?? {}, candidateId: conflict.candidateId,
|
|
154
|
+
});
|
|
155
|
+
if (!/^[0-9a-f]{40}$/u.test(committed?.commitSha ?? '')) {
|
|
156
|
+
return {
|
|
157
|
+
lane: 'intentional_serial', treatment: null, split: null, reasons: ['transform_not_committed'],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
143
160
|
return {
|
|
144
161
|
lane: 'seam_transform',
|
|
145
162
|
treatment: {
|
|
@@ -148,6 +165,8 @@ export async function resolveRuntimeSeamTreatment(options = {}) {
|
|
|
148
165
|
},
|
|
149
166
|
split: built.split,
|
|
150
167
|
files: applied.files ?? null,
|
|
168
|
+
successor_base_sha: committed.commitSha,
|
|
169
|
+
successor_base_ref: committed.ref ?? null,
|
|
151
170
|
reasons: [],
|
|
152
171
|
};
|
|
153
172
|
}
|
|
Binary file
|
package/src/todo-cli.mjs
CHANGED
|
@@ -85,7 +85,12 @@ import {
|
|
|
85
85
|
validateSeamProposalProjection,
|
|
86
86
|
} from './seam-proposal-contracts.mjs';
|
|
87
87
|
import { compileSeamProposalArtifact, declaredConcernSymbols } from './seam-proposal.mjs';
|
|
88
|
+
import { collectSensorEvidence } from './sensor-adapter.mjs';
|
|
88
89
|
import { applySeamProposal } from './seam-apply.mjs';
|
|
90
|
+
import {
|
|
91
|
+
WITNESS_DRAFT_SCHEMA, buildWitnessObservationQuerySet, buildWitnessSet, serializeWitnessSet,
|
|
92
|
+
validateWitnessDraft,
|
|
93
|
+
} from './witness-scaffold.mjs';
|
|
89
94
|
import {
|
|
90
95
|
parseTodoSourceRef, todoLegacyReconciliationDigest, validatePhaseTodoRevision,
|
|
91
96
|
validateTodoRevision, validateTodoRevisionSet,
|
|
@@ -938,6 +943,53 @@ async function seamProposalCompile({ repoRoot, planKey }) {
|
|
|
938
943
|
return result;
|
|
939
944
|
}
|
|
940
945
|
|
|
946
|
+
/**
|
|
947
|
+
* 下書きとfresh観測から、そのまま通るwitness setを書き出す。
|
|
948
|
+
*
|
|
949
|
+
* 推定はしない。何を所有し何を触るかは下書きが述べ、ここが供給するのはAIには作れないもの——
|
|
950
|
+
* affected testのfresh観測、query setとprovenanceの配線、canonical bytesと自己digest——だけである。
|
|
951
|
+
*/
|
|
952
|
+
async function witnessScaffold({ repoRoot, planKey, inputRef }) {
|
|
953
|
+
const draft = await readJsonInput(repoRoot, inputRef, {
|
|
954
|
+
validate: validateWitnessDraft,
|
|
955
|
+
invalidCode: 'WITNESS_DRAFT_INVALID',
|
|
956
|
+
});
|
|
957
|
+
if (draft.plan_key !== planKey) {
|
|
958
|
+
throw new TodoStoreError('INPUT_INVALID', 'witness_draft_plan_mismatch', undefined, {
|
|
959
|
+
draft_plan_key: draft.plan_key, plan_key: planKey,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
const { queries, paths } = buildWitnessObservationQuerySet(draft);
|
|
963
|
+
const collected = await collectSensorEvidence({ cwd: repoRoot, querySet: { queries } });
|
|
964
|
+
const affectedTestsByPath = {};
|
|
965
|
+
queries.forEach((query, index) => {
|
|
966
|
+
if (query.operation !== 'affected') return;
|
|
967
|
+
const entry = collected.outcomes[index]?.targets?.[0];
|
|
968
|
+
// 観測できていないものを空配列へ丸めない。丸めるとdriftでcompileが落ちる。
|
|
969
|
+
if (entry?.path_state === 'absent' || !Array.isArray(entry?.data?.affectedTests)) return;
|
|
970
|
+
affectedTestsByPath[query.target] = [...entry.data.affectedTests];
|
|
971
|
+
});
|
|
972
|
+
const { witnessSet, reasons } = buildWitnessSet({ draft, affectedTestsByPath });
|
|
973
|
+
if (witnessSet === null) {
|
|
974
|
+
throw new TodoStoreError('WITNESS_SCAFFOLD_INCOMPLETE', 'witness_scaffold_incomplete', undefined, {
|
|
975
|
+
reasons, next_action: 'resolve_declaration_then_retry',
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
const ref = todoWitnessRef(planKey);
|
|
979
|
+
await mkdir(path.dirname(path.join(repoRoot, ref)), { recursive: true });
|
|
980
|
+
await writeFile(path.join(repoRoot, ref), serializeWitnessSet(witnessSet));
|
|
981
|
+
return {
|
|
982
|
+
schema: 'lattice.todo_witness_scaffold_result.v1',
|
|
983
|
+
project_id: draft.project_id,
|
|
984
|
+
plan_key: planKey,
|
|
985
|
+
witness_ref: ref,
|
|
986
|
+
observed_paths: paths,
|
|
987
|
+
task_count: Object.keys(witnessSet.manual_witness).length,
|
|
988
|
+
witness_set_digest: witnessSet.witness_set_digest,
|
|
989
|
+
next_action: 'compile_independence',
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
941
993
|
/**
|
|
942
994
|
* 着地時に使うsurface名。提案が出すhash由来の仮名を、人が読む名前へ置き換える。
|
|
943
995
|
*
|
|
@@ -1729,6 +1781,10 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
1729
1781
|
} else if (argv.length === 4 && argv[0] === 'seam-proposal' && argv[1] === 'apply'
|
|
1730
1782
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])) {
|
|
1731
1783
|
action = (repoRoot) => seamProposalApply({ repoRoot, planKey: argv[3] });
|
|
1784
|
+
} else if (argv.length === 7 && argv[0] === 'independence' && argv[1] === 'witness'
|
|
1785
|
+
&& argv[2] === 'scaffold' && argv[3] === '--plan' && isTodoIdentifier(argv[4])
|
|
1786
|
+
&& argv[5] === '--input' && isTodoRef(argv[6])) {
|
|
1787
|
+
action = (repoRoot) => witnessScaffold({ repoRoot, planKey: argv[4], inputRef: argv[6] });
|
|
1732
1788
|
} else if (argv.length === 6 && argv[0] === 'seam-proposal' && argv[1] === 'land'
|
|
1733
1789
|
&& argv[2] === '--plan' && isTodoIdentifier(argv[3])
|
|
1734
1790
|
&& argv[4] === '--names' && isTodoRef(argv[5])) {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宣言を書くための道具(AGENTS.md「装置の境界」)。
|
|
3
|
+
*
|
|
4
|
+
* 推定はしない。何を所有し、係争資源の中で何を触るかはAIが決める——Latticeを操作するAIは装置の
|
|
5
|
+
* 一部であり、そこへ同じ能力を二重化しない。ここが供給するのは、AIには作れないものだけである。
|
|
6
|
+
*
|
|
7
|
+
* - `affected_tests`のfresh観測。宣言と観測はbinding単位でexact比較されるので、手で当てると外れる。
|
|
8
|
+
* - `sensor_query_set`と`sensor_provenance`の配線。宣言と観測の裏付けが別の資源を指す事故を防ぐ。
|
|
9
|
+
* - canonical bytesと自己digest。非canonicalな宣言は独立性判定を通ってseam提案でだけ落ちる。
|
|
10
|
+
*
|
|
11
|
+
* この3つは2026-07-27の作業で実際に踏んだ摩擦であり、道具が無いと毎回踏む。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { canonicalizeTodoArtifact, isTodoIdentifier, isTodoRef, todoSelfDigest } from './todo-contracts.mjs';
|
|
15
|
+
import { TODO_WITNESS_SET_SCHEMA } from './todo-independence-contracts.mjs';
|
|
16
|
+
|
|
17
|
+
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
18
|
+
const sortedUnique = (values) => [...new Set(values)].sort(compareText);
|
|
19
|
+
|
|
20
|
+
export const WITNESS_DRAFT_SCHEMA = 'lattice.todo_witness_draft.v1';
|
|
21
|
+
|
|
22
|
+
function reject(reasons) {
|
|
23
|
+
return { witnessSet: null, queries: [], reasons: sortedUnique(reasons) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 下書きの形。AIが書く欄だけを持ち、観測で埋まる欄は持たない。 */
|
|
27
|
+
export function validateWitnessDraft(value) {
|
|
28
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
29
|
+
if (value.schema !== WITNESS_DRAFT_SCHEMA) return false;
|
|
30
|
+
if (!isTodoIdentifier(value.project_id) || !isTodoIdentifier(value.plan_key)) return false;
|
|
31
|
+
if (value.capacity === null || typeof value.capacity !== 'object'
|
|
32
|
+
|| !Number.isSafeInteger(value.capacity.executors) || value.capacity.executors < 1) return false;
|
|
33
|
+
if (value.tasks === null || typeof value.tasks !== 'object' || Array.isArray(value.tasks)) return false;
|
|
34
|
+
const entries = Object.entries(value.tasks);
|
|
35
|
+
if (entries.length === 0) return false;
|
|
36
|
+
return entries.every(([taskId, task]) => isTodoIdentifier(taskId)
|
|
37
|
+
&& task !== null && typeof task === 'object' && !Array.isArray(task)
|
|
38
|
+
&& Array.isArray(task.owns) && task.owns.every(isTodoRef)
|
|
39
|
+
&& (task.reads === undefined || (Array.isArray(task.reads) && task.reads.every(isTodoRef)))
|
|
40
|
+
&& (task.unknowns === undefined || (Array.isArray(task.unknowns)
|
|
41
|
+
&& task.unknowns.every((entry) => entry !== null && typeof entry === 'object'
|
|
42
|
+
&& isTodoIdentifier(entry.kind) && typeof entry.ref === 'string' && entry.ref.length > 0)))
|
|
43
|
+
&& (task.concern_anchors === undefined || (Array.isArray(task.concern_anchors)
|
|
44
|
+
&& task.concern_anchors.every((anchor) => anchor !== null && typeof anchor === 'object'
|
|
45
|
+
&& isTodoRef(anchor.within) && Array.isArray(anchor.symbols)
|
|
46
|
+
&& anchor.symbols.length > 0 && anchor.symbols.every(isTodoIdentifier)))));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function queryIdFor(index) {
|
|
50
|
+
return `witness-affected-${String(index).padStart(3, '0')}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 下書きから、観測に要るquery setを組む。所有pathごとに1つのaffected queryを引く。 */
|
|
54
|
+
export function buildWitnessObservationQuerySet(draft) {
|
|
55
|
+
const paths = sortedUnique(Object.values(draft.tasks).flatMap(({ owns }) => owns));
|
|
56
|
+
return {
|
|
57
|
+
queries: [
|
|
58
|
+
{ id: 'witness-status', operation: 'status' },
|
|
59
|
+
...paths.map((target, index) => ({
|
|
60
|
+
id: queryIdFor(index), operation: 'affected', target,
|
|
61
|
+
})),
|
|
62
|
+
],
|
|
63
|
+
paths,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 下書きと観測から、そのまま通るwitness setを組む。
|
|
69
|
+
*
|
|
70
|
+
* @param {object} options
|
|
71
|
+
* @param {object} options.draft `lattice.todo_witness_draft.v1`
|
|
72
|
+
* @param {object} options.affectedTestsByPath 所有pathごとのfresh観測
|
|
73
|
+
*/
|
|
74
|
+
export function buildWitnessSet({ draft, affectedTestsByPath } = {}) {
|
|
75
|
+
if (!validateWitnessDraft(draft)) return reject(['draft_invalid']);
|
|
76
|
+
const { paths } = buildWitnessObservationQuerySet(draft);
|
|
77
|
+
const queryIdByPath = new Map(paths.map((target, index) => [target, queryIdFor(index)]));
|
|
78
|
+
|
|
79
|
+
const reasons = [];
|
|
80
|
+
const manualWitness = {};
|
|
81
|
+
for (const [taskId, task] of Object.entries(draft.tasks).sort(([left], [right]) => compareText(left, right))) {
|
|
82
|
+
const owns = sortedUnique(task.owns);
|
|
83
|
+
if (owns.length === 0) { reasons.push(`owns_empty:${taskId}`); continue; }
|
|
84
|
+
// affected_testsは宣言とfresh観測をbinding単位でexact比較する。複数pathを所有すると
|
|
85
|
+
// 観測集合が一致しない限り必ず落ちるので、今の契約では表現できない(2026-07-27の実測)。
|
|
86
|
+
if (owns.length > 1) { reasons.push(`multiple_owned_paths_unsupported:${taskId}`); continue; }
|
|
87
|
+
const [target] = owns;
|
|
88
|
+
const affected = affectedTestsByPath?.[target];
|
|
89
|
+
// 観測できていないことを空配列へ丸めない。丸めるとdriftでcompileが落ちる。
|
|
90
|
+
if (!Array.isArray(affected)) { reasons.push(`affected_tests_unobserved:${target}`); continue; }
|
|
91
|
+
for (const anchor of task.concern_anchors ?? []) {
|
|
92
|
+
// `within`は自分が所有している資源に限る。所有していない資源の内側に担当を主張させない。
|
|
93
|
+
if (!owns.includes(anchor.within)) reasons.push(`anchor_outside_owned:${taskId}:${anchor.within}`);
|
|
94
|
+
}
|
|
95
|
+
manualWitness[taskId] = {
|
|
96
|
+
owns: [{ kind: 'path', target }],
|
|
97
|
+
reads: sortedUnique(task.reads ?? []),
|
|
98
|
+
writes: [target],
|
|
99
|
+
resources: [],
|
|
100
|
+
state_effects: [],
|
|
101
|
+
sensor_provenance: {
|
|
102
|
+
queries: [{
|
|
103
|
+
query_id: queryIdByPath.get(target),
|
|
104
|
+
expect: { kind: 'affected', path: target },
|
|
105
|
+
}],
|
|
106
|
+
},
|
|
107
|
+
affected_tests: sortedUnique(affected),
|
|
108
|
+
// 明示unknownは下書きが持つ。観測で埋まる欄ではなく、書き手が「ここは確定していない」と
|
|
109
|
+
// 述べる欄なので、道具が発明も削除もしない。
|
|
110
|
+
unknowns: [...(task.unknowns ?? [])]
|
|
111
|
+
.map(({ kind, ref }) => ({ kind, ref }))
|
|
112
|
+
.sort((left, right) => compareText(`${left.kind}\u0000${left.ref}`, `${right.kind}\u0000${right.ref}`)),
|
|
113
|
+
...(Array.isArray(task.concern_anchors) && task.concern_anchors.length > 0
|
|
114
|
+
? {
|
|
115
|
+
concern_anchors: [...task.concern_anchors]
|
|
116
|
+
.map((anchor) => ({
|
|
117
|
+
within: { kind: 'path', target: anchor.within },
|
|
118
|
+
symbols: sortedUnique(anchor.symbols),
|
|
119
|
+
}))
|
|
120
|
+
.sort((left, right) => compareText(left.within.target, right.within.target)),
|
|
121
|
+
}
|
|
122
|
+
: {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (reasons.length > 0) return reject(reasons);
|
|
126
|
+
|
|
127
|
+
const witnessSet = {
|
|
128
|
+
schema: TODO_WITNESS_SET_SCHEMA,
|
|
129
|
+
project_id: draft.project_id,
|
|
130
|
+
plan_key: draft.plan_key,
|
|
131
|
+
capacity: { executors: draft.capacity.executors },
|
|
132
|
+
sensor_query_set: {
|
|
133
|
+
queries: [
|
|
134
|
+
...paths.map((target) => ({
|
|
135
|
+
id: queryIdByPath.get(target), operation: 'affected', target,
|
|
136
|
+
})),
|
|
137
|
+
{ id: 'witness-status', operation: 'status' },
|
|
138
|
+
].sort((left, right) => compareText(left.id, right.id)),
|
|
139
|
+
},
|
|
140
|
+
manual_witness: manualWitness,
|
|
141
|
+
witness_set_digest: '',
|
|
142
|
+
};
|
|
143
|
+
witnessSet.witness_set_digest = todoSelfDigest(witnessSet, 'witness_set_digest');
|
|
144
|
+
return { witnessSet, reasons: [] };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** storeへ置くbytes。canonical+末尾LFでないと、判定は通るのにseam提案で落ちる。 */
|
|
148
|
+
export function serializeWitnessSet(witnessSet) {
|
|
149
|
+
return `${canonicalizeTodoArtifact(witnessSet)}\n`;
|
|
150
|
+
}
|