@quolu/lattice 0.14.0 → 0.15.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 +2 -2
- package/src/cli-help.mjs +3 -0
- package/src/project-cli.mjs +2 -0
- package/src/seam-proposal-contracts.mjs +427 -0
- package/src/seam-proposal-queries.mjs +484 -0
- package/src/seam-proposal.mjs +1763 -0
- package/src/todo-cli.mjs +285 -10
- package/src/todo-gantt-html.mjs +60 -1
- package/src/todo-gantt-layout.mjs +76 -0
- package/src/todo-independence-contracts.mjs +61 -8
- package/src/todo-independence-guidance.mjs +41 -1
- package/src/todo-independence.mjs +71 -10
- package/src/todo-store.mjs +103 -2
|
@@ -14,8 +14,13 @@ import {
|
|
|
14
14
|
import { TODO_INDEPENDENCE_GUIDANCE_CODES } from './todo-independence-guidance.mjs';
|
|
15
15
|
|
|
16
16
|
export const TODO_WITNESS_SET_SCHEMA = 'lattice.todo_witness_set.v1';
|
|
17
|
-
export const TODO_INDEPENDENCE_SCHEMA = 'lattice.todo_independence.
|
|
17
|
+
export const TODO_INDEPENDENCE_SCHEMA = 'lattice.todo_independence.v3';
|
|
18
18
|
export const TODO_INDEPENDENCE_PROJECTION_SCHEMA = 'lattice.todo_independence_projection.v2';
|
|
19
|
+
export const TODO_INDEPENDENCE_LEGACY_MARKER_SCHEMA = 'lattice.todo_independence_legacy_marker.v1';
|
|
20
|
+
export const TODO_INDEPENDENCE_LEGACY_SCHEMAS = Object.freeze([
|
|
21
|
+
'lattice.todo_independence.v1',
|
|
22
|
+
'lattice.todo_independence.v2',
|
|
23
|
+
]);
|
|
19
24
|
|
|
20
25
|
/** boundary compileが一度に扱えるToDo数(runtime front-endのMAX_COLLECTIONと同じ閉じ方)。 */
|
|
21
26
|
export const TODO_INDEPENDENCE_TASK_LIMIT = 256;
|
|
@@ -53,6 +58,19 @@ function plain(value) {
|
|
|
53
58
|
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
/**
|
|
62
|
+
* 既知の旧independence artifactを、単なるschema文字列だけでなく版をまたいで不変な
|
|
63
|
+
* identity fieldの型までで識別する。本体構造や自己digestは旧契約validatorの再実装に
|
|
64
|
+
* なるため、ここでは検査しない。
|
|
65
|
+
*/
|
|
66
|
+
export function isTodoIndependenceLegacyArtifactIdentity(value) {
|
|
67
|
+
return plain(value) && TODO_INDEPENDENCE_LEGACY_SCHEMAS.includes(value.schema)
|
|
68
|
+
&& isTodoIdentifier(value.project_id) && isTodoIdentifier(value.plan_key)
|
|
69
|
+
&& isTodoIdentifier(value.plan_version) && isTodoDigest(value.topology_digest)
|
|
70
|
+
&& isTodoDigest(value.witness_set_digest) && isTodoDigest(value.result_digest)
|
|
71
|
+
&& isGitSha(value.base_sha);
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
function boundedList(value, validator, limit = TODO_INDEPENDENCE_LIST_LIMIT) {
|
|
57
75
|
return Array.isArray(value) && value.length <= limit && value.every(validator);
|
|
58
76
|
}
|
|
@@ -139,12 +157,39 @@ export function validateTodoWitnessSet(value) {
|
|
|
139
157
|
}
|
|
140
158
|
|
|
141
159
|
function conflictEntry(value) {
|
|
142
|
-
return exactRecord(value, ['task_ids', 'resource_id'
|
|
160
|
+
return exactRecord(value, ['task_ids', 'resource_id'])
|
|
143
161
|
&& Array.isArray(value.task_ids) && value.task_ids.length === 2
|
|
144
162
|
&& value.task_ids.every(isTodoIdentifier)
|
|
145
163
|
&& compareText(value.task_ids[0], value.task_ids[1]) < 0
|
|
146
|
-
&& boundedText(value.resource_id)
|
|
147
|
-
|
|
164
|
+
&& boundedText(value.resource_id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function repoRelativeResourceTarget(value) {
|
|
168
|
+
if (!boundedText(value) || value.startsWith('/') || value.includes('\\')
|
|
169
|
+
|| /^[A-Za-z]:/u.test(value)) return false;
|
|
170
|
+
const body = value.endsWith('/') ? value.slice(0, -1) : value;
|
|
171
|
+
return body.length > 0 && body.split('/')
|
|
172
|
+
.every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function conflictResourceEntry(value) {
|
|
176
|
+
if (!exactRecord(value, ['resource_id', 'kind', 'target'])
|
|
177
|
+
|| !boundedText(value.resource_id)
|
|
178
|
+
|| !TODO_INDEPENDENCE_CONFLICT_KINDS.includes(value.kind)) return false;
|
|
179
|
+
return value.kind === 'path'
|
|
180
|
+
? repoRelativeResourceTarget(value.target)
|
|
181
|
+
: boundedText(value.target);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function isTodoIndependenceLegacyMarker(value) {
|
|
185
|
+
return exactRecord(value, [
|
|
186
|
+
'schema', 'legacy_schema', 'project_id', 'plan_key', 'plan_version',
|
|
187
|
+
'topology_digest', 'base_sha',
|
|
188
|
+
])
|
|
189
|
+
&& value.schema === TODO_INDEPENDENCE_LEGACY_MARKER_SCHEMA
|
|
190
|
+
&& TODO_INDEPENDENCE_LEGACY_SCHEMAS.includes(value.legacy_schema)
|
|
191
|
+
&& isTodoIdentifier(value.project_id) && isTodoIdentifier(value.plan_key)
|
|
192
|
+
&& value.plan_version === null && value.topology_digest === null && value.base_sha === null;
|
|
148
193
|
}
|
|
149
194
|
|
|
150
195
|
/**
|
|
@@ -188,10 +233,10 @@ function wavePlan(value, taskIds) {
|
|
|
188
233
|
}
|
|
189
234
|
|
|
190
235
|
/**
|
|
191
|
-
* `lattice.todo_independence.
|
|
236
|
+
* `lattice.todo_independence.v3`。
|
|
192
237
|
*
|
|
193
238
|
* conflict/precedence/unknownはnormalized boundary graphから採る(ADR 0127 Decision 4)。
|
|
194
|
-
* conflict
|
|
239
|
+
* conflictは`conflict_resources`のresource idを参照し、kindとnormalized targetを一度だけ保持する。
|
|
195
240
|
* `task_boundaries`はtask別の宣言境界で、鮮度のdiff交差判定をartifactだけで閉じるために持つ。
|
|
196
241
|
* `wave_plan`はschedulability compileが`compiled`を返した時だけ持ち、unknownが残る間はnullになる。
|
|
197
242
|
* verdictに現れないペアをverified独立と読めるのは、両taskにunknownが無いときだけである。
|
|
@@ -200,7 +245,7 @@ export function validateTodoIndependence(value) {
|
|
|
200
245
|
try {
|
|
201
246
|
if (!exactRecord(value, [
|
|
202
247
|
'schema', 'project_id', 'plan_key', 'plan_version', 'topology_digest', 'base_sha',
|
|
203
|
-
'witness_set_digest', 'compiled_at', 'task_ids', 'task_boundaries', 'conflicts',
|
|
248
|
+
'witness_set_digest', 'compiled_at', 'task_ids', 'task_boundaries', 'conflict_resources', 'conflicts',
|
|
204
249
|
'precedences', 'unknowns', 'wave_plan', 'outcome', 'result_digest',
|
|
205
250
|
])) return false;
|
|
206
251
|
if (value.schema !== TODO_INDEPENDENCE_SCHEMA) return false;
|
|
@@ -221,10 +266,16 @@ export function validateTodoIndependence(value) {
|
|
|
221
266
|
|| !value.task_boundaries.every((entry, index) => entry.task_id === value.task_ids[index])) {
|
|
222
267
|
return false;
|
|
223
268
|
}
|
|
269
|
+
if (!boundedList(value.conflict_resources, conflictResourceEntry)
|
|
270
|
+
|| !strictlySorted(value.conflict_resources, (entry) => entry.resource_id)) return false;
|
|
271
|
+
const conflictResourceIds = new Set(value.conflict_resources.map(({ resource_id: id }) => id));
|
|
224
272
|
if (!boundedList(value.conflicts, conflictEntry)
|
|
225
273
|
|| !value.conflicts.every((entry) => entry.task_ids.every((taskId) => known.has(taskId)))
|
|
274
|
+
|| !value.conflicts.every((entry) => conflictResourceIds.has(entry.resource_id))
|
|
226
275
|
|| !strictlySorted(value.conflicts, (entry) => (
|
|
227
276
|
`${entry.task_ids[0]}\0${entry.task_ids[1]}\0${entry.resource_id}`))) return false;
|
|
277
|
+
const referencedResourceIds = new Set(value.conflicts.map(({ resource_id: id }) => id));
|
|
278
|
+
if (referencedResourceIds.size !== value.conflict_resources.length) return false;
|
|
228
279
|
if (!boundedList(value.precedences, precedenceEntry)
|
|
229
280
|
|| !value.precedences.every((entry) => known.has(entry.from_task_id) && known.has(entry.to_task_id))
|
|
230
281
|
|| !strictlySorted(value.precedences, (entry) => (
|
|
@@ -345,7 +396,9 @@ export function validateTodoIndependenceProjection(value) {
|
|
|
345
396
|
// 記録が無い状態でcompile済みidentityを名乗らない。
|
|
346
397
|
if (value.coverage === 'missing'
|
|
347
398
|
&& (value.compiled_base_sha !== null || value.topology_digest !== null)) return false;
|
|
348
|
-
if (value.coverage
|
|
399
|
+
if (['verified', 'stale'].includes(value.coverage) && value.compiled_base_sha === null) return false;
|
|
400
|
+
if (value.coverage === 'superseded' && value.compiled_base_sha === null
|
|
401
|
+
&& (value.plan_version !== null || value.topology_digest !== null)) return false;
|
|
349
402
|
for (const key of ['active_task_ids', 'uncovered_active_task_ids']) {
|
|
350
403
|
if (!Array.isArray(value[key]) || value[key].length > TODO_INDEPENDENCE_TASK_LIMIT
|
|
351
404
|
|| !value[key].every(isTodoIdentifier) || !strictlySorted(value[key])) return false;
|
|
@@ -12,6 +12,7 @@ export const TODO_INDEPENDENCE_GUIDANCE_CODES = Object.freeze([
|
|
|
12
12
|
'independence_no_ready_frontier',
|
|
13
13
|
'independence_unrecorded',
|
|
14
14
|
'independence_task_undeclared',
|
|
15
|
+
'independence_contract_superseded',
|
|
15
16
|
'independence_superseded',
|
|
16
17
|
'independence_stale_for_task',
|
|
17
18
|
'independence_conflict_with_active',
|
|
@@ -19,6 +20,13 @@ export const TODO_INDEPENDENCE_GUIDANCE_CODES = Object.freeze([
|
|
|
19
20
|
'independence_verified',
|
|
20
21
|
]);
|
|
21
22
|
|
|
23
|
+
export const SEAM_PROPOSAL_GUIDANCE_CODES = Object.freeze([
|
|
24
|
+
'seam_proposal_unrecorded',
|
|
25
|
+
'seam_proposal_superseded',
|
|
26
|
+
'seam_proposal_stale',
|
|
27
|
+
'seam_proposal_verified',
|
|
28
|
+
]);
|
|
29
|
+
|
|
22
30
|
const CATALOG = Object.freeze({
|
|
23
31
|
independence_no_ready_frontier: Object.freeze({
|
|
24
32
|
message: '着手候補が無いため、並列可否を述べる対象が無い。',
|
|
@@ -32,6 +40,10 @@ const CATALOG = Object.freeze({
|
|
|
32
40
|
message: 'この工程はwitness setで宣言されていないため、記録には含まれていない。',
|
|
33
41
|
next_action: 'add_task_to_witness_set_then_compile',
|
|
34
42
|
}),
|
|
43
|
+
independence_contract_superseded: Object.freeze({
|
|
44
|
+
message: '記録は旧契約versionで書かれており、現在の並列可否の判定としては読めない。現在の契約での再compileが次の一歩になる。',
|
|
45
|
+
next_action: 'recompile_independence',
|
|
46
|
+
}),
|
|
35
47
|
independence_superseded: Object.freeze({
|
|
36
48
|
message: 'planが改訂され、記録は別のtopologyについての判定になっている。',
|
|
37
49
|
next_action: 'migrate_witness_set_then_compile',
|
|
@@ -52,6 +64,22 @@ const CATALOG = Object.freeze({
|
|
|
52
64
|
message: '記録時点の宣言境界では、他のready工程と干渉しない。',
|
|
53
65
|
next_action: 'none',
|
|
54
66
|
}),
|
|
67
|
+
seam_proposal_unrecorded: Object.freeze({
|
|
68
|
+
message: 'このplanのseam提案はまだ生成していない。提案対象が無いのではなく、記録が存在しない。',
|
|
69
|
+
next_action: 'compile_seam_proposal',
|
|
70
|
+
}),
|
|
71
|
+
seam_proposal_superseded: Object.freeze({
|
|
72
|
+
message: '参照元のplanまたは並列可否記録が更新され、このseam提案は現在の競合についての記録ではない。',
|
|
73
|
+
next_action: 'compile_seam_proposal',
|
|
74
|
+
}),
|
|
75
|
+
seam_proposal_stale: Object.freeze({
|
|
76
|
+
message: 'seam提案の生成後にHEADが進み、記録時点の構造証拠は現在のcodeを指していない。',
|
|
77
|
+
next_action: 'compile_seam_proposal',
|
|
78
|
+
}),
|
|
79
|
+
seam_proposal_verified: Object.freeze({
|
|
80
|
+
message: 'seam提案の記録は現在のplan、並列可否記録、HEADと一致している。',
|
|
81
|
+
next_action: 'none',
|
|
82
|
+
}),
|
|
55
83
|
});
|
|
56
84
|
|
|
57
85
|
/** 切断可能性の言い換え。conflictの案内へ添える。 */
|
|
@@ -82,7 +110,7 @@ export function todoIndependenceGuidance(code, { severability = null } = {}) {
|
|
|
82
110
|
*/
|
|
83
111
|
export function selectIndependenceGuidance({
|
|
84
112
|
coverage, taskDeclared, taskStale, conflictWithActive = null, conflictBetweenReady = null,
|
|
85
|
-
readyCount = null,
|
|
113
|
+
contractSuperseded = false, readyCount = null,
|
|
86
114
|
}) {
|
|
87
115
|
// 着手候補が無いなら述べる対象が無い。ここを通さないと、readyが空のとき
|
|
88
116
|
// 「未検査taskが1件も無い」が空虚に真になり、記録が古くても検証済みへ倒れる。
|
|
@@ -97,6 +125,9 @@ export function selectIndependenceGuidance({
|
|
|
97
125
|
severability: conflictBetweenReady,
|
|
98
126
|
});
|
|
99
127
|
}
|
|
128
|
+
if (contractSuperseded) {
|
|
129
|
+
return todoIndependenceGuidance('independence_contract_superseded');
|
|
130
|
+
}
|
|
100
131
|
if (coverage === 'missing') return todoIndependenceGuidance('independence_unrecorded');
|
|
101
132
|
if (coverage === 'superseded') return todoIndependenceGuidance('independence_superseded');
|
|
102
133
|
if (!taskDeclared) return todoIndependenceGuidance('independence_task_undeclared');
|
|
@@ -104,6 +135,15 @@ export function selectIndependenceGuidance({
|
|
|
104
135
|
return todoIndependenceGuidance('independence_verified');
|
|
105
136
|
}
|
|
106
137
|
|
|
138
|
+
export function selectSeamProposalGuidance({ coverage }) {
|
|
139
|
+
const code = coverage === 'missing' ? 'seam_proposal_unrecorded'
|
|
140
|
+
: coverage === 'superseded' ? 'seam_proposal_superseded'
|
|
141
|
+
: coverage === 'stale' ? 'seam_proposal_stale'
|
|
142
|
+
: coverage === 'verified' ? 'seam_proposal_verified' : null;
|
|
143
|
+
if (code === null) throw new TypeError(`unknown seam proposal coverage: ${coverage}`);
|
|
144
|
+
return todoIndependenceGuidance(code);
|
|
145
|
+
}
|
|
146
|
+
|
|
107
147
|
/**
|
|
108
148
|
* 宣言からcompileを経て読むまでの順序。helpとMCP instructionsが同じ手順を語るための正本。
|
|
109
149
|
* 面ごとに手順を書き直すと、片方だけが古くなる。
|
|
@@ -2,6 +2,7 @@ import { digestTodoArtifact, isTodoIdentifier, todoSelfDigest } from './todo-con
|
|
|
2
2
|
import {
|
|
3
3
|
TODO_INDEPENDENCE_SCHEMA,
|
|
4
4
|
isGitSha,
|
|
5
|
+
isTodoIndependenceLegacyMarker,
|
|
5
6
|
severabilityOfConflictKind,
|
|
6
7
|
synthesizeWitnessRunRequest,
|
|
7
8
|
validateTodoIndependence,
|
|
@@ -50,34 +51,65 @@ export async function collectWitnessSensorEvidence({ cwd, witnessSet, execute =
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
|
-
* conflict
|
|
54
|
+
* conflictから参照されるnormalized resourceを正規化してartifactへ載せる。
|
|
54
55
|
*
|
|
55
56
|
* `graph.conflicts`はresource_idしか持たず、宣言由来のstate resource idは任意文字列なので
|
|
56
57
|
* prefixからkindを復元できない。normalized resourceを引けない場合は、切断可能性を
|
|
57
58
|
* 不明のまま記録するのでなくtyped failで止める。
|
|
58
59
|
*/
|
|
59
60
|
function conflictsFrom(verdicts, resources) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const resourceById = new Map();
|
|
62
|
+
for (const resource of Array.isArray(resources) ? resources : []) {
|
|
63
|
+
const resourceId = resource?.resource_id;
|
|
64
|
+
if (resourceById.has(resourceId)) {
|
|
65
|
+
const previous = resourceById.get(resourceId);
|
|
66
|
+
if (previous.kind !== resource?.kind) {
|
|
67
|
+
fail('INDEPENDENCE_RESOURCE_KIND_MISMATCH', 'conflict_resource_kind_mismatch', {
|
|
68
|
+
resource_id: resourceId ?? null,
|
|
69
|
+
observed_kinds: [previous.kind ?? null, resource?.kind ?? null],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (previous.target !== resource?.target) {
|
|
73
|
+
fail('INDEPENDENCE_RESOURCE_TARGET_MISMATCH', 'conflict_resource_target_mismatch', {
|
|
74
|
+
resource_id: resourceId ?? null,
|
|
75
|
+
observed_targets: [previous.target ?? null, resource?.target ?? null],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
fail('INDEPENDENCE_RESOURCE_DUPLICATE', 'conflict_resource_duplicate', {
|
|
79
|
+
resource_id: resourceId ?? null,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
resourceById.set(resourceId, { kind: resource?.kind, target: resource?.target });
|
|
83
|
+
}
|
|
84
|
+
const conflicts = verdicts
|
|
63
85
|
.filter((verdict) => verdict.type === 'conflict')
|
|
64
86
|
.map((verdict) => {
|
|
65
|
-
const
|
|
87
|
+
const resource = resourceById.get(verdict.resource_id);
|
|
88
|
+
const kind = resource?.kind;
|
|
66
89
|
if (!['symbol', 'path', 'state', 'effect'].includes(kind)) {
|
|
67
90
|
fail('INDEPENDENCE_RESOURCE_KIND_UNRESOLVED', 'conflict_resource_kind_unresolved', {
|
|
68
91
|
resource_id: verdict.resource_id, observed_kind: kind ?? null,
|
|
69
92
|
});
|
|
70
93
|
}
|
|
94
|
+
if (typeof resource.target !== 'string' || resource.target.length === 0) {
|
|
95
|
+
fail('INDEPENDENCE_RESOURCE_TARGET_UNRESOLVED', 'conflict_resource_target_unresolved', {
|
|
96
|
+
resource_id: verdict.resource_id, observed_target: resource.target ?? null,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
71
99
|
return {
|
|
72
100
|
task_ids: [...verdict.todo_ids].sort(compareText),
|
|
73
101
|
resource_id: verdict.resource_id,
|
|
74
|
-
kind,
|
|
75
102
|
};
|
|
76
103
|
})
|
|
77
104
|
.sort((left, right) => compareText(
|
|
78
105
|
`${left.task_ids[0]}\0${left.task_ids[1]}\0${left.resource_id}`,
|
|
79
106
|
`${right.task_ids[0]}\0${right.task_ids[1]}\0${right.resource_id}`,
|
|
80
107
|
));
|
|
108
|
+
const used = new Set(conflicts.map(({ resource_id: resourceId }) => resourceId));
|
|
109
|
+
const conflictResources = [...used]
|
|
110
|
+
.map((resourceId) => ({ resource_id: resourceId, ...resourceById.get(resourceId) }))
|
|
111
|
+
.sort((left, right) => compareText(left.resource_id, right.resource_id));
|
|
112
|
+
return { conflicts, conflictResources };
|
|
81
113
|
}
|
|
82
114
|
|
|
83
115
|
/**
|
|
@@ -141,7 +173,7 @@ function unknownsFrom(detail) {
|
|
|
141
173
|
}
|
|
142
174
|
|
|
143
175
|
/**
|
|
144
|
-
* witness setとsensor evidenceから`lattice.todo_independence.
|
|
176
|
+
* witness setとsensor evidenceから`lattice.todo_independence.v3`を作る。
|
|
145
177
|
*
|
|
146
178
|
* compileは宣言済みtaskの部分集合へ閉じる(ADR 0127 Decision 4)。未宣言taskを混ぜると、
|
|
147
179
|
* unknownが1件でも出た時点で宣言済みtask同士の判定まで失われるためである。
|
|
@@ -187,6 +219,9 @@ export function compileTodoIndependence(options = {}) {
|
|
|
187
219
|
}
|
|
188
220
|
|
|
189
221
|
const dispatchable = compiled.outcome === 'dispatchable';
|
|
222
|
+
const conflictProjection = dispatchable
|
|
223
|
+
? conflictsFrom(compiled.pairwise_verdicts, compiled.resources)
|
|
224
|
+
: { conflicts: [], conflictResources: [] };
|
|
190
225
|
const artifact = {
|
|
191
226
|
schema: TODO_INDEPENDENCE_SCHEMA,
|
|
192
227
|
project_id: plan.project_id,
|
|
@@ -201,8 +236,8 @@ export function compileTodoIndependence(options = {}) {
|
|
|
201
236
|
task_id: taskId,
|
|
202
237
|
paths: boundaryPathsOf(witnessSet.manual_witness[taskId]),
|
|
203
238
|
})),
|
|
204
|
-
|
|
205
|
-
|
|
239
|
+
conflict_resources: conflictProjection.conflictResources,
|
|
240
|
+
conflicts: conflictProjection.conflicts,
|
|
206
241
|
precedences: dispatchable ? precedencesFrom(compiled.pairwise_verdicts) : [],
|
|
207
242
|
unknowns: dispatchable ? [] : unknownsFrom(compiled.detail),
|
|
208
243
|
wave_plan: dispatchable
|
|
@@ -290,6 +325,24 @@ export function projectIndependenceFrontier({
|
|
|
290
325
|
};
|
|
291
326
|
}
|
|
292
327
|
|
|
328
|
+
if (isTodoIndependenceLegacyMarker(artifact)) {
|
|
329
|
+
return {
|
|
330
|
+
coverage: 'superseded',
|
|
331
|
+
drift: null,
|
|
332
|
+
active_task_ids: active,
|
|
333
|
+
uncovered_active_task_ids: active,
|
|
334
|
+
frontier: {
|
|
335
|
+
parallel_groups: [],
|
|
336
|
+
serialize_pairs: [],
|
|
337
|
+
conflicts_with_active: [],
|
|
338
|
+
unknown: ready.map((taskId) => ({
|
|
339
|
+
task_id: taskId,
|
|
340
|
+
unknowns: [{ kind: 'record_superseded', ref: artifact.legacy_schema }],
|
|
341
|
+
})),
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
293
346
|
const superseded = artifact.plan_version !== plan.plan_version
|
|
294
347
|
|| artifact.topology_digest !== plan.topology_digest;
|
|
295
348
|
const coverage = superseded ? 'superseded'
|
|
@@ -382,9 +435,17 @@ export function projectIndependenceFrontier({
|
|
|
382
435
|
});
|
|
383
436
|
};
|
|
384
437
|
|
|
438
|
+
const conflictResourceById = new Map(artifact.conflict_resources
|
|
439
|
+
.map((resource) => [resource.resource_id, resource]));
|
|
385
440
|
for (const conflict of artifact.conflicts) {
|
|
441
|
+
const resource = conflictResourceById.get(conflict.resource_id);
|
|
442
|
+
if (resource === undefined) {
|
|
443
|
+
fail('INDEPENDENCE_RESOURCE_KIND_UNRESOLVED', 'conflict_resource_kind_unresolved', {
|
|
444
|
+
resource_id: conflict.resource_id, observed_kind: null,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
386
447
|
notePair(conflict.task_ids[0], conflict.task_ids[1], 'conflict', conflict.resource_id,
|
|
387
|
-
|
|
448
|
+
resource.kind);
|
|
388
449
|
}
|
|
389
450
|
for (const precedence of artifact.precedences) {
|
|
390
451
|
notePair(precedence.from_task_id, precedence.to_task_id, 'precedence', precedence.reason, null);
|
package/src/todo-store.mjs
CHANGED
|
@@ -21,9 +21,12 @@ import {
|
|
|
21
21
|
validateTodoSnapshot,
|
|
22
22
|
} from './todo-contracts.mjs';
|
|
23
23
|
import {
|
|
24
|
+
TODO_INDEPENDENCE_LEGACY_MARKER_SCHEMA,
|
|
25
|
+
isTodoIndependenceLegacyArtifactIdentity,
|
|
24
26
|
validateTodoIndependence,
|
|
25
27
|
validateTodoWitnessSet,
|
|
26
28
|
} from './todo-independence-contracts.mjs';
|
|
29
|
+
import { validateSeamProposal } from './seam-proposal-contracts.mjs';
|
|
27
30
|
import { sha256Bytes, verifyLinearHashChain } from './hash-chain.mjs';
|
|
28
31
|
import {
|
|
29
32
|
parseTodoSourceRef,
|
|
@@ -3609,12 +3612,25 @@ export async function readTodoIndependenceArtifact(options = {}) {
|
|
|
3609
3612
|
const member = activeMember(store, options.planKey);
|
|
3610
3613
|
const ref = todoIndependenceRef(member.plan.plan_key, member.plan.plan_version);
|
|
3611
3614
|
try {
|
|
3612
|
-
|
|
3615
|
+
const artifact = await readArtifact(repoRoot, ref, {
|
|
3613
3616
|
code: 'INDEPENDENCE_ARTIFACT_INVALID',
|
|
3614
3617
|
maxBytes: INDEPENDENCE_ARTIFACT_BYTES,
|
|
3615
|
-
|
|
3618
|
+
// 旧契約は本体を信用せず、canonical JSONと版をまたいで不変なidentityの型だけを見る。
|
|
3619
|
+
// schema名だけの壊れた記録や現行v3をこの分岐へ逃がさず、旧版集合も明示したものだけに閉じる。
|
|
3620
|
+
validate: (value) => validateTodoIndependence(value)
|
|
3621
|
+
|| isTodoIndependenceLegacyArtifactIdentity(value),
|
|
3616
3622
|
missing: true,
|
|
3617
3623
|
});
|
|
3624
|
+
if (artifact === null || validateTodoIndependence(artifact)) return artifact;
|
|
3625
|
+
return {
|
|
3626
|
+
schema: TODO_INDEPENDENCE_LEGACY_MARKER_SCHEMA,
|
|
3627
|
+
legacy_schema: artifact.schema,
|
|
3628
|
+
project_id: member.plan.project_id,
|
|
3629
|
+
plan_key: member.plan.plan_key,
|
|
3630
|
+
plan_version: null,
|
|
3631
|
+
topology_digest: null,
|
|
3632
|
+
base_sha: null,
|
|
3633
|
+
};
|
|
3618
3634
|
} catch (error) {
|
|
3619
3635
|
// 読めない記録は握りつぶさない。ただしどのplanをどう直すかまで言わないと、
|
|
3620
3636
|
// 消費者は「壊れている」以上のことができない。
|
|
@@ -3630,6 +3646,91 @@ export async function readTodoIndependenceArtifact(options = {}) {
|
|
|
3630
3646
|
}
|
|
3631
3647
|
}
|
|
3632
3648
|
|
|
3649
|
+
const SEAM_PROPOSAL_ARTIFACT_NAME = 'seam-proposal.json';
|
|
3650
|
+
const SEAM_PROPOSAL_ARTIFACT_BYTES = 4_194_304;
|
|
3651
|
+
|
|
3652
|
+
/** independence.jsonと同じplan versionディレクトリへ並置し、manifestへ登録しない。 */
|
|
3653
|
+
export function todoSeamProposalRef(planKey, planVersion) {
|
|
3654
|
+
return `${STORE_ROOT_REF}/plans/${planKey}/${planVersion}/${SEAM_PROPOSAL_ARTIFACT_NAME}`;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
/**
|
|
3658
|
+
* seam proposal artifactをactive planと現在のindependence artifactへbindして書く。
|
|
3659
|
+
* planの正本とmanifestには触れない。
|
|
3660
|
+
*/
|
|
3661
|
+
export async function writeTodoSeamProposalArtifact(options = {}) {
|
|
3662
|
+
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
3663
|
+
const { artifact } = options;
|
|
3664
|
+
if (!validateSeamProposal(artifact)) {
|
|
3665
|
+
fail('SEAM_PROPOSAL_ARTIFACT_INVALID', 'seam_proposal_artifact_invalid');
|
|
3666
|
+
}
|
|
3667
|
+
return withLock(repoRoot, async () => {
|
|
3668
|
+
const store = await readTodoStore({ repoRoot, forWrite: true, now: options.now });
|
|
3669
|
+
if (store.project_id !== artifact.project_id) {
|
|
3670
|
+
fail('SEAM_PROPOSAL_BINDING_MISMATCH', 'project_id_mismatch', {
|
|
3671
|
+
expected: store.project_id, actual: artifact.project_id,
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
const member = activeMember(store, artifact.plan_key);
|
|
3675
|
+
const binding = artifact.source_binding;
|
|
3676
|
+
if (member.plan.plan_version !== binding.plan_version) {
|
|
3677
|
+
fail('SEAM_PROPOSAL_BINDING_MISMATCH', 'plan_version_mismatch', {
|
|
3678
|
+
expected: member.plan.plan_version, actual: binding.plan_version,
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3681
|
+
if (member.plan.topology_digest !== binding.topology_digest) {
|
|
3682
|
+
fail('SEAM_PROPOSAL_BINDING_MISMATCH', 'topology_digest_mismatch', {
|
|
3683
|
+
expected: member.plan.topology_digest, actual: binding.topology_digest,
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
const independenceArtifact = await readTodoIndependenceArtifact({
|
|
3687
|
+
repoRoot, store, planKey: artifact.plan_key,
|
|
3688
|
+
});
|
|
3689
|
+
if (independenceArtifact === null
|
|
3690
|
+
|| !validateTodoIndependence(independenceArtifact)
|
|
3691
|
+
|| independenceArtifact.schema !== binding.independence_schema
|
|
3692
|
+
|| independenceArtifact.result_digest !== binding.independence_result_digest
|
|
3693
|
+
|| independenceArtifact.witness_set_digest !== binding.witness_set_digest
|
|
3694
|
+
|| independenceArtifact.plan_version !== binding.plan_version
|
|
3695
|
+
|| independenceArtifact.topology_digest !== binding.topology_digest
|
|
3696
|
+
|| independenceArtifact.base_sha !== binding.base_sha) {
|
|
3697
|
+
fail('SEAM_PROPOSAL_BINDING_MISMATCH', 'independence_binding_mismatch');
|
|
3698
|
+
}
|
|
3699
|
+
const ref = todoSeamProposalRef(artifact.plan_key, binding.plan_version);
|
|
3700
|
+
await atomicWrite(path.resolve(repoRoot, ref), canonicalLine(artifact));
|
|
3701
|
+
return { ref, artifact };
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
/**
|
|
3706
|
+
* active planに並置されたseam proposal artifactを読む。
|
|
3707
|
+
* 無ければnull、壊れた記録はtyped failureとし、missingへ丸めない。
|
|
3708
|
+
*/
|
|
3709
|
+
export async function readTodoSeamProposalArtifact(options = {}) {
|
|
3710
|
+
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
3711
|
+
const store = options.store ?? await readTodoStore({ repoRoot, now: options.now });
|
|
3712
|
+
const member = activeMember(store, options.planKey);
|
|
3713
|
+
const ref = todoSeamProposalRef(member.plan.plan_key, member.plan.plan_version);
|
|
3714
|
+
try {
|
|
3715
|
+
return await readArtifact(repoRoot, ref, {
|
|
3716
|
+
code: 'SEAM_PROPOSAL_ARTIFACT_INVALID',
|
|
3717
|
+
maxBytes: SEAM_PROPOSAL_ARTIFACT_BYTES,
|
|
3718
|
+
validate: validateSeamProposal,
|
|
3719
|
+
missing: true,
|
|
3720
|
+
});
|
|
3721
|
+
} catch (error) {
|
|
3722
|
+
if (error instanceof TodoStoreError && error.code === 'SEAM_PROPOSAL_ARTIFACT_INVALID') {
|
|
3723
|
+
throw new TodoStoreError(error.code, error.detail.reason, undefined, {
|
|
3724
|
+
...error.detail,
|
|
3725
|
+
plan_key: member.plan.plan_key,
|
|
3726
|
+
artifact_ref: ref,
|
|
3727
|
+
next_action: 'recompile_seam_proposal_or_remove_stale_record',
|
|
3728
|
+
});
|
|
3729
|
+
}
|
|
3730
|
+
throw error;
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3633
3734
|
const WITNESS_SET_BYTES = 4_194_304;
|
|
3634
3735
|
|
|
3635
3736
|
/**
|