@quolu/lattice 0.18.0 → 0.20.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/boundary-observation-compiler-v2.mjs +5 -0
- package/src/runtime-contracts.mjs +49 -8
- package/src/runtime-front-end.mjs +44 -4
- package/src/todo-cli.mjs +3 -0
- package/src/todo-independence-contracts.mjs +24 -8
- package/src/todo-independence-guidance.mjs +9 -1
- package/src/todo-independence.mjs +12 -0
package/package.json
CHANGED
|
@@ -15,6 +15,11 @@ const PROVENANCE_SOURCES = new Set([
|
|
|
15
15
|
const SENSOR_STATUSES = new Set([
|
|
16
16
|
'ready',
|
|
17
17
|
'symbol_absent',
|
|
18
|
+
// 不存在path関連。宣言の有無で3つに分かれる(ADR 0136)。宣言が無いabsentはpath_absent、
|
|
19
|
+
// 宣言があるのに実在するのはcreates_path_present、fs観測が記録に無いのはcreates_unverified。
|
|
20
|
+
'path_absent',
|
|
21
|
+
'creates_path_present',
|
|
22
|
+
'creates_unverified',
|
|
18
23
|
'empty',
|
|
19
24
|
'unresolved',
|
|
20
25
|
'command_failure',
|
|
@@ -140,10 +140,23 @@ const STATE_EFFECT_KINDS = Object.freeze([
|
|
|
140
140
|
'external_effect',
|
|
141
141
|
]);
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
/**
|
|
144
|
+
* 所有宣言。`creates: true`は「このpathはまだ存在せず、このTODOが作る」という創作の意思である
|
|
145
|
+
* (ADR 0135 Decision 3・ADR 0136)。
|
|
146
|
+
*
|
|
147
|
+
* `kind`は`path`のまま据え置く。存在の有無は資源の種類ではなく資源の状態であり、kindを分けると
|
|
148
|
+
* 既存のpath判定(write交差の免除、conflict resourceのkind)が全部この宣言を取りこぼす。
|
|
149
|
+
*
|
|
150
|
+
* 値は`true`だけを受理する。`false`は「存在するpath」と同義で、同じ事実に2つの書き方を
|
|
151
|
+
* 与えることになる。省略が既定である。
|
|
152
|
+
*/
|
|
153
|
+
function ownEntry(value, { allowCreates = false } = {}) {
|
|
154
|
+
if (!plainObject(value)) return false;
|
|
155
|
+
const creates = Object.hasOwn(value, 'creates');
|
|
156
|
+
if (creates && !allowCreates) return false;
|
|
157
|
+
return exactRecord(value, creates ? ['kind', 'target', 'creates'] : ['kind', 'target'])
|
|
146
158
|
&& OWN_KINDS.includes(value.kind)
|
|
159
|
+
&& (!creates || (value.creates === true && value.kind === 'path'))
|
|
147
160
|
&& typeof value.target === 'string'
|
|
148
161
|
&& value.target.length > 0
|
|
149
162
|
&& Buffer.byteLength(value.target, 'utf8') <= MAX_PATH_BYTES;
|
|
@@ -221,6 +234,17 @@ const WITNESS_PROVENANCE = Object.freeze([
|
|
|
221
234
|
'manual_state_effect',
|
|
222
235
|
]);
|
|
223
236
|
|
|
237
|
+
/**
|
|
238
|
+
* 現行のrun request契約。v2は`owns[].creates`だけがv1との差であり、境界宣言としては同値である。
|
|
239
|
+
* 既存requestの書き換えを要求しないため、v1は読み口として残す。
|
|
240
|
+
*/
|
|
241
|
+
export const RUN_REQUEST_SCHEMA = 'lattice.run_request.v2';
|
|
242
|
+
export const RUN_REQUEST_LEGACY_SCHEMAS = Object.freeze(['lattice.run_request.v1']);
|
|
243
|
+
export const RUN_REQUEST_SCHEMAS = Object.freeze([
|
|
244
|
+
RUN_REQUEST_SCHEMA,
|
|
245
|
+
...RUN_REQUEST_LEGACY_SCHEMAS,
|
|
246
|
+
]);
|
|
247
|
+
|
|
224
248
|
export const RUN_REQUEST_FIELDS = Object.freeze([
|
|
225
249
|
'schema',
|
|
226
250
|
'request_id',
|
|
@@ -276,7 +300,9 @@ export function explainRunRequest(value) {
|
|
|
276
300
|
return reject('non_canonical_request_bytes', '');
|
|
277
301
|
}
|
|
278
302
|
if (!exactRecord(value, RUN_REQUEST_FIELDS)) return reject('unexpected_or_missing_top_level_keys', '');
|
|
279
|
-
if (value.schema
|
|
303
|
+
if (!RUN_REQUEST_SCHEMAS.includes(value.schema)) return reject('schema_mismatch', '/schema');
|
|
304
|
+
// 創作宣言はv2から。v1のclosed shapeは余分fieldを拒否するので加算互換が成立しない。
|
|
305
|
+
const allowCreates = value.schema === RUN_REQUEST_SCHEMA;
|
|
280
306
|
if (!identifier(value.request_id)) return reject('invalid_identifier', '/request_id');
|
|
281
307
|
if (!exactRecord(value.repo, ['base_sha', 'root_kind'])) return reject('unexpected_or_missing_keys', '/repo');
|
|
282
308
|
if (!gitSha(value.repo.base_sha)) return reject('invalid_git_sha', '/repo/base_sha');
|
|
@@ -301,7 +327,9 @@ export function explainRunRequest(value) {
|
|
|
301
327
|
const at = `/manual_witness/${todoId}`;
|
|
302
328
|
if (!plainObject(witness)) return reject('not_an_object', at);
|
|
303
329
|
if (!exactRecord(witness, MANUAL_WITNESS_FIELDS)) return reject('unexpected_or_missing_keys', at);
|
|
304
|
-
if (!boundedArray(witness.owns,
|
|
330
|
+
if (!boundedArray(witness.owns, (own) => ownEntry(own, { allowCreates }))) {
|
|
331
|
+
return reject('invalid_own_entries', `${at}/owns`);
|
|
332
|
+
}
|
|
305
333
|
if (!repoPathArray(witness.reads)) return reject('invalid_repo_relative_paths', `${at}/reads`);
|
|
306
334
|
if (!repoPathArray(witness.writes, { allowPrefix: true })) return reject('invalid_repo_relative_paths', `${at}/writes`);
|
|
307
335
|
if (!boundedArray(witness.resources, identifier)) return reject('invalid_identifier', `${at}/resources`);
|
|
@@ -371,7 +399,18 @@ export function verifyRuntimePlanBinding(options = {}) {
|
|
|
371
399
|
return plan.nodes.every((node) => requestTodoIds.has(node.todo_id));
|
|
372
400
|
}
|
|
373
401
|
|
|
374
|
-
/**
|
|
402
|
+
/**
|
|
403
|
+
* `lattice.boundary_manifest.v3`。witness_provenanceはresourceごとに区別する。
|
|
404
|
+
*
|
|
405
|
+
* v3は`owns[].creates`だけがv2との差である。宣言が持っていた「このpathはまだ無い」を
|
|
406
|
+
* 記録側でも保つ——落とすと、manifestだけを読む消費者が既存fileと同じ扱いをする。
|
|
407
|
+
* 旧v2 manifestはrun storeに残るので読み口として受理する。
|
|
408
|
+
*/
|
|
409
|
+
export const BOUNDARY_MANIFEST_SCHEMA = 'lattice.boundary_manifest.v3';
|
|
410
|
+
export const BOUNDARY_MANIFEST_SCHEMAS = Object.freeze([
|
|
411
|
+
BOUNDARY_MANIFEST_SCHEMA,
|
|
412
|
+
'lattice.boundary_manifest.v2',
|
|
413
|
+
]);
|
|
375
414
|
export function validateRuntimeBoundaryManifest(value) {
|
|
376
415
|
return validateSafely(value, (manifest) => (
|
|
377
416
|
exactRecord(manifest, [
|
|
@@ -388,9 +427,11 @@ export function validateRuntimeBoundaryManifest(value) {
|
|
|
388
427
|
'witness_provenance',
|
|
389
428
|
'manifest_digest',
|
|
390
429
|
])
|
|
391
|
-
&& manifest.schema
|
|
430
|
+
&& BOUNDARY_MANIFEST_SCHEMAS.includes(manifest.schema)
|
|
392
431
|
&& identifier(manifest.todo_id)
|
|
393
|
-
&& boundedArray(manifest.owns, ownEntry
|
|
432
|
+
&& boundedArray(manifest.owns, (own) => ownEntry(own, {
|
|
433
|
+
allowCreates: manifest.schema === BOUNDARY_MANIFEST_SCHEMA,
|
|
434
|
+
}))
|
|
394
435
|
&& repoPathArray(manifest.reads)
|
|
395
436
|
&& repoPathArray(manifest.writes, { allowPrefix: true })
|
|
396
437
|
&& boundedArray(manifest.resources, identifier)
|
|
@@ -6,6 +6,7 @@ import { portableSensorOutcome } from './sensor-adapter.mjs';
|
|
|
6
6
|
import { compileSchedulabilityGraphV2 } from './schedulability-compiler-v2.mjs';
|
|
7
7
|
import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
|
|
8
8
|
import {
|
|
9
|
+
BOUNDARY_MANIFEST_SCHEMA,
|
|
9
10
|
SENSOR_EXPECT_KINDS,
|
|
10
11
|
SENSOR_QUERY_OPERATIONS,
|
|
11
12
|
selfDigest,
|
|
@@ -265,11 +266,44 @@ function entryNode(entry) {
|
|
|
265
266
|
return null;
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
/**
|
|
270
|
+
* 創作宣言されたpathの裏付けを決める(ADR 0136)。
|
|
271
|
+
*
|
|
272
|
+
* 宣言と観測が一致する時だけ`ready`にする。fresh absentは推測ではなくfsのlstat結果なので、
|
|
273
|
+
* 「観測できなかった」ではなく「観測して、無かった」である。存在しないfileに依存するものは
|
|
274
|
+
* 構造的に存在しえないため、affected testが空であることまで確かめれば裏付けは閉じる。
|
|
275
|
+
*
|
|
276
|
+
* 既に存在するpathへ創作を宣言していたら`creates_path_present`で止める。実害は無いが、
|
|
277
|
+
* 宣言が実態からずれているのを黙って通すと、宣言と観測が一致しているという前提が崩れる。
|
|
278
|
+
* 宣言していないabsent pathは従来どおり`path_absent`のままにする——綴り違いを
|
|
279
|
+
* 「黙って通る創作境界」にしないための線である。
|
|
280
|
+
*/
|
|
281
|
+
function creationBoundaryStatus(outcome, expectPath) {
|
|
282
|
+
// sensorが答えられていない状況は、この関数の管轄でない。通常のstatus判定へ返す。
|
|
283
|
+
if (!['ready', 'empty'].includes(outcome.status)) return null;
|
|
284
|
+
const target = affectedTarget(outcome.raw, expectPath);
|
|
285
|
+
// fs観測が記録に無いなら、不存在は確かめられていない。宣言だけで裏付けにしない。
|
|
286
|
+
if (target === null || target === undefined) return 'creates_unverified';
|
|
287
|
+
if (target.path_state !== 'absent') return 'creates_path_present';
|
|
288
|
+
const payload = affectedPayload(outcome.raw, expectPath);
|
|
289
|
+
if (payload === null
|
|
290
|
+
|| !Array.isArray(payload.changedFiles)
|
|
291
|
+
|| payload.changedFiles.length !== 1
|
|
292
|
+
|| payload.changedFiles[0] !== expectPath
|
|
293
|
+
|| !Array.isArray(payload.affectedTests)
|
|
294
|
+
|| payload.affectedTests.length !== 0) return 'creates_unverified';
|
|
295
|
+
return 'ready';
|
|
296
|
+
}
|
|
297
|
+
|
|
268
298
|
/**
|
|
269
299
|
* 束縛queryのoutcomeへexpectをexact照合し、sensor statusを決める。
|
|
270
300
|
* fuzzy解決・空結果は依存なしへ丸めず、unknown系statusへ落とす(AGENTS.md)。
|
|
271
301
|
*/
|
|
272
302
|
function resolveBindingStatus(binding, outcome) {
|
|
303
|
+
if (binding.expect.kind === 'affected' && binding.creates === true) {
|
|
304
|
+
const declared = creationBoundaryStatus(outcome, binding.expect.path);
|
|
305
|
+
if (declared !== null) return declared;
|
|
306
|
+
}
|
|
273
307
|
if (outcome.status !== 'ready') return outcome.status;
|
|
274
308
|
const { expect } = binding;
|
|
275
309
|
if (expect.kind === 'affected') {
|
|
@@ -401,7 +435,13 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
401
435
|
const driftDetails = [];
|
|
402
436
|
for (const todoId of todoIds) {
|
|
403
437
|
const witness = request.manual_witness[todoId];
|
|
404
|
-
|
|
438
|
+
// 創作宣言はowns側にある。裏付けを決めるのはbindingなので、覆っているbindingへ写す。
|
|
439
|
+
const creating = new Set(witness.owns
|
|
440
|
+
.filter((own) => own.creates === true).map((own) => own.target));
|
|
441
|
+
const bindings = normalizeProvenanceQueries(witness, todoId)
|
|
442
|
+
.map((binding) => (creating.size > 0
|
|
443
|
+
&& [...creating].some((target) => bindingCoversOwn(binding, { kind: 'path', target }))
|
|
444
|
+
? { ...binding, creates: true } : binding));
|
|
405
445
|
for (const binding of bindings) {
|
|
406
446
|
const query = queryById.get(binding.query_id);
|
|
407
447
|
if (query === undefined) {
|
|
@@ -697,7 +737,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
697
737
|
guidance: hasFreshAbsentPath
|
|
698
738
|
? {
|
|
699
739
|
code: 'BOOTSTRAP_OWNERSHIP_SEAM',
|
|
700
|
-
message: '
|
|
740
|
+
message: '不存在と観測されたpathは、そのTODOが作るならowns entryへcreates: trueを宣言して再compileする。宣言せずに裏付けを得るなら、親が空の専用seamをbase commitへ先行追加し、sensor sync後に同じrequestを再compileする',
|
|
701
741
|
}
|
|
702
742
|
: {
|
|
703
743
|
code: 'ACQUIRE_OWNERSHIP_EVIDENCE',
|
|
@@ -729,7 +769,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
729
769
|
: 'manual_state_effect';
|
|
730
770
|
}
|
|
731
771
|
const manifest = {
|
|
732
|
-
schema:
|
|
772
|
+
schema: BOUNDARY_MANIFEST_SCHEMA,
|
|
733
773
|
todo_id: todoId,
|
|
734
774
|
owns: witness.owns,
|
|
735
775
|
reads: witness.reads,
|
|
@@ -751,7 +791,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
751
791
|
};
|
|
752
792
|
manifest.manifest_digest = selfDigest(manifest, 'manifest_digest');
|
|
753
793
|
if (!validateRuntimeBoundaryManifest(manifest)) {
|
|
754
|
-
fail(`生成manifestがboundary_manifest
|
|
794
|
+
fail(`生成manifestがboundary_manifest contractを満たさない: ${todoId}`);
|
|
755
795
|
}
|
|
756
796
|
manifests[todoId] = manifest;
|
|
757
797
|
}
|
package/src/todo-cli.mjs
CHANGED
|
@@ -461,6 +461,7 @@ async function startAdvisory({ repoRoot, store, projection, planKey, taskId }) {
|
|
|
461
461
|
contractSuperseded: isTodoIndependenceLegacyMarker(artifact),
|
|
462
462
|
conflictWithActive: conflictsWithActive[0]?.severability ?? null,
|
|
463
463
|
conflictBetweenReady: readyConflict?.severability ?? null,
|
|
464
|
+
verdictsAbsent: selfUnknowns.some(({ kind }) => kind === 'plan_verdicts_absent'),
|
|
464
465
|
}),
|
|
465
466
|
};
|
|
466
467
|
}
|
|
@@ -836,6 +837,8 @@ async function independence({ repoRoot, requestedPlanKey }) {
|
|
|
836
837
|
.some(({ unknowns }) => unknowns.some(({ kind }) => kind === 'record_stale')),
|
|
837
838
|
conflictWithActive: projected.frontier.conflicts_with_active[0]?.severability ?? null,
|
|
838
839
|
conflictBetweenReady: projected.frontier.serialize_pairs[0]?.severability ?? null,
|
|
840
|
+
verdictsAbsent: projected.frontier.unknown
|
|
841
|
+
.some(({ unknowns }) => unknowns.some(({ kind }) => kind === 'plan_verdicts_absent')),
|
|
839
842
|
}),
|
|
840
843
|
frontier: projected.frontier,
|
|
841
844
|
result_digest: '',
|
|
@@ -8,21 +8,31 @@ import {
|
|
|
8
8
|
} from './todo-contracts.mjs';
|
|
9
9
|
import {
|
|
10
10
|
RUN_REQUEST_CLAIM_MODE,
|
|
11
|
+
RUN_REQUEST_SCHEMA,
|
|
11
12
|
explainRunRequest,
|
|
12
13
|
selfDigest as runtimeSelfDigest,
|
|
13
14
|
} from './runtime-contracts.mjs';
|
|
14
15
|
import { TODO_INDEPENDENCE_GUIDANCE_CODES } from './todo-independence-guidance.mjs';
|
|
15
16
|
|
|
16
|
-
export const TODO_WITNESS_SET_SCHEMA = 'lattice.todo_witness_set.
|
|
17
|
+
export const TODO_WITNESS_SET_SCHEMA = 'lattice.todo_witness_set.v3';
|
|
17
18
|
/**
|
|
18
|
-
* まだ受理する旧witness set契約。v1はconcern anchor
|
|
19
|
-
*
|
|
19
|
+
* まだ受理する旧witness set契約。v1はconcern anchorを、v2は創作宣言を持てないだけで、
|
|
20
|
+
* 境界宣言としてはv3と同値である。既存宣言を書き換えさせないために読み口を残す。
|
|
20
21
|
*/
|
|
21
|
-
export const TODO_WITNESS_SET_LEGACY_SCHEMAS = Object.freeze([
|
|
22
|
+
export const TODO_WITNESS_SET_LEGACY_SCHEMAS = Object.freeze([
|
|
23
|
+
'lattice.todo_witness_set.v2',
|
|
24
|
+
'lattice.todo_witness_set.v1',
|
|
25
|
+
]);
|
|
22
26
|
export const TODO_WITNESS_SET_SCHEMAS = Object.freeze([
|
|
23
27
|
TODO_WITNESS_SET_SCHEMA,
|
|
24
28
|
...TODO_WITNESS_SET_LEGACY_SCHEMAS,
|
|
25
29
|
]);
|
|
30
|
+
/** 宣言できる欄はversionごとに違う。どの版から使えるかを1箇所で持つ。 */
|
|
31
|
+
const CONCERN_ANCHOR_SCHEMAS = Object.freeze([
|
|
32
|
+
TODO_WITNESS_SET_SCHEMA,
|
|
33
|
+
'lattice.todo_witness_set.v2',
|
|
34
|
+
]);
|
|
35
|
+
const CREATES_SCHEMAS = Object.freeze([TODO_WITNESS_SET_SCHEMA]);
|
|
26
36
|
|
|
27
37
|
/** 1 taskが宣言できるconcern anchorの資源数と、資源あたりのsymbol数の上限。 */
|
|
28
38
|
export const TODO_CONCERN_ANCHOR_LIMIT = 256;
|
|
@@ -117,7 +127,7 @@ function boundedText(value, maximumBytes = 4_096) {
|
|
|
117
127
|
export function synthesizeWitnessRunRequest(witnessSet, { baseSha, requestId }) {
|
|
118
128
|
const taskIds = Object.keys(witnessSet.manual_witness).sort(compareText);
|
|
119
129
|
const request = {
|
|
120
|
-
schema:
|
|
130
|
+
schema: RUN_REQUEST_SCHEMA,
|
|
121
131
|
request_id: requestId,
|
|
122
132
|
repo: { base_sha: baseSha, root_kind: 'git' },
|
|
123
133
|
capacity: witnessSet.capacity,
|
|
@@ -212,13 +222,19 @@ export function explainTodoWitnessSet(value) {
|
|
|
212
222
|
});
|
|
213
223
|
const explained = explainRunRequest(probe);
|
|
214
224
|
if (!explained.valid) return reject(explained.reason, explained.path);
|
|
215
|
-
//
|
|
216
|
-
|
|
225
|
+
// 版ごとの欄。probeはRUN_REQUEST_SCHEMAで合成するので創作宣言はそこを通ってしまう。
|
|
226
|
+
// 旧版の宣言に新しい欄を書けないことは、ここだけが見る。
|
|
217
227
|
for (const taskId of taskIds) {
|
|
218
228
|
const witness = value.manual_witness[taskId];
|
|
229
|
+
if (!CREATES_SCHEMAS.includes(value.schema)
|
|
230
|
+
&& witness.owns.some((own) => Object.hasOwn(own, 'creates'))) {
|
|
231
|
+
return reject('creates_require_witness_set_v3', `/manual_witness/${taskId}/owns`);
|
|
232
|
+
}
|
|
219
233
|
const at = `/manual_witness/${taskId}/concern_anchors`;
|
|
220
234
|
if (!Object.hasOwn(witness, 'concern_anchors')) continue;
|
|
221
|
-
if (
|
|
235
|
+
if (!CONCERN_ANCHOR_SCHEMAS.includes(value.schema)) {
|
|
236
|
+
return reject('concern_anchors_require_witness_set_v2', at);
|
|
237
|
+
}
|
|
222
238
|
const anchors = explainConcernAnchors(witness.concern_anchors, witness.owns, at);
|
|
223
239
|
if (!anchors.valid) return reject(anchors.reason, anchors.path);
|
|
224
240
|
}
|
|
@@ -17,6 +17,7 @@ export const TODO_INDEPENDENCE_GUIDANCE_CODES = Object.freeze([
|
|
|
17
17
|
'independence_stale_for_task',
|
|
18
18
|
'independence_conflict_with_active',
|
|
19
19
|
'independence_conflict_between_ready',
|
|
20
|
+
'independence_verdicts_absent',
|
|
20
21
|
'independence_verified',
|
|
21
22
|
]);
|
|
22
23
|
|
|
@@ -90,6 +91,10 @@ const CATALOG = Object.freeze({
|
|
|
90
91
|
message: '他のready工程と同じ資源を書く記録がある。同時に着手すると衝突する。',
|
|
91
92
|
next_action: 'serialize_or_split_boundary',
|
|
92
93
|
}),
|
|
94
|
+
independence_verdicts_absent: Object.freeze({
|
|
95
|
+
message: '判定が途中で止まっており、記録はどの組についてもverdictを持たない。自分にunknownが無いことは、他と干渉しない証拠にならない。',
|
|
96
|
+
next_action: 'resolve_unknowns_then_recompile',
|
|
97
|
+
}),
|
|
93
98
|
independence_verified: Object.freeze({
|
|
94
99
|
message: '記録時点の宣言境界では、他のready工程と干渉しない。',
|
|
95
100
|
next_action: 'none',
|
|
@@ -164,7 +169,7 @@ export function todoIndependenceGuidance(code, { severability = null } = {}) {
|
|
|
164
169
|
*/
|
|
165
170
|
export function selectIndependenceGuidance({
|
|
166
171
|
coverage, taskDeclared, taskStale, conflictWithActive = null, conflictBetweenReady = null,
|
|
167
|
-
contractSuperseded = false, readyCount = null,
|
|
172
|
+
contractSuperseded = false, readyCount = null, verdictsAbsent = false,
|
|
168
173
|
}) {
|
|
169
174
|
// 着手候補が無いなら述べる対象が無い。ここを通さないと、readyが空のとき
|
|
170
175
|
// 「未検査taskが1件も無い」が空虚に真になり、記録が古くても検証済みへ倒れる。
|
|
@@ -186,6 +191,9 @@ export function selectIndependenceGuidance({
|
|
|
186
191
|
if (coverage === 'superseded') return todoIndependenceGuidance('independence_superseded');
|
|
187
192
|
if (!taskDeclared) return todoIndependenceGuidance('independence_task_undeclared');
|
|
188
193
|
if (taskStale) return todoIndependenceGuidance('independence_stale_for_task');
|
|
194
|
+
// 記録は新しく、この工程自身にも問題が無い。それでもverdictが1つも無いなら、
|
|
195
|
+
// 述べられるのは「干渉しない」ではなく「まだ何も判定していない」である。
|
|
196
|
+
if (verdictsAbsent) return todoIndependenceGuidance('independence_verdicts_absent');
|
|
189
197
|
return todoIndependenceGuidance('independence_verified');
|
|
190
198
|
}
|
|
191
199
|
|
|
@@ -386,6 +386,18 @@ export function projectIndependenceFrontier({
|
|
|
386
386
|
for (const entry of artifact.unknowns) {
|
|
387
387
|
if (ready.includes(entry.task_id)) noteUnknown(entry.task_id, entry.kind, entry.ref);
|
|
388
388
|
}
|
|
389
|
+
// 記録全体がverdictを持たない場合、どのpairについても判定は存在しない。
|
|
390
|
+
//
|
|
391
|
+
// compileがBOUNDARY_UNKNOWNで止まると、front endはpairwise verdictを1つも返さないので
|
|
392
|
+
// artifact.conflictsは空になる。この空をそのまま読むと、自分にunknownが無いreadyどうしが
|
|
393
|
+
// 「ぶつかる記録が無い=独立」として並列グループへ入る。実際には**誰についても判定していない**。
|
|
394
|
+
// 新規fileを1つ宣言したToDoが混ざるだけで、無関係なToDoの実competitionが消えて
|
|
395
|
+
// 検証済み並列に見える——不在を証拠に読み替える、最も危険な向きの誤りである。
|
|
396
|
+
if (artifact.outcome !== 'compiled') {
|
|
397
|
+
for (const taskId of ready) {
|
|
398
|
+
if (covered.has(taskId)) noteUnknown(taskId, 'plan_verdicts_absent', artifact.outcome);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
389
401
|
// planが進んだ記録はtask単位に救えない(topology自体が別物)。
|
|
390
402
|
// HEADだけが進んだ場合は、宣言境界に触れたtaskだけを落とす。
|
|
391
403
|
if (coverage === 'superseded') {
|