@quolu/lattice 0.52.0 → 0.52.2
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/LICENSE +147 -147
- package/README.ja.md +355 -355
- package/README.md +258 -258
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice-scripted-adapter.mjs +0 -0
- package/bin/lattice-scripted-worker.mjs +0 -0
- package/bin/lattice-work-order-adapter.mjs +0 -0
- package/bin/lattice.mjs +0 -0
- package/docs/bridge-setup.md +132 -132
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
- package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
- package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
- package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
- package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
- package/package.json +103 -103
- package/sensor/LICENSE +21 -21
- package/sensor/NOTICE +19 -19
- package/sensor/dist/bin/lattice-sensor.js +9 -9
- package/sensor/dist/db/index.js +24 -24
- package/sensor/dist/db/migrations.js +41 -41
- package/sensor/dist/db/queries.js +164 -164
- package/sensor/dist/db/schema.sql +205 -205
- package/sensor/dist/directory.js +5 -5
- package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/sensor/dist/mcp/liveness-watchdog.js +53 -53
- package/sensor/dist/mcp/server-instructions.js +95 -95
- package/sensor/package.json +56 -56
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +297 -297
- package/src/bridge-config.mjs +346 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1053
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +697 -697
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1502
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -520
- package/src/runtime-cli.mjs +4588 -4584
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -600
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -162
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -477
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -290
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -834
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1188
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1156
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1167
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +3141 -3126
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -625
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +4322 -4314
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,595 +1,595 @@
|
|
|
1
|
-
import {
|
|
2
|
-
exactRecord,
|
|
3
|
-
isNonNegativeSafeInteger,
|
|
4
|
-
isStrictTodoTimestamp,
|
|
5
|
-
isTodoDigest,
|
|
6
|
-
isTodoIdentifier,
|
|
7
|
-
todoSelfDigest,
|
|
8
|
-
} from './todo-contracts.mjs';
|
|
9
|
-
import {
|
|
10
|
-
RUN_REQUEST_CLAIM_MODE,
|
|
11
|
-
RUN_REQUEST_DECLARATIVE_SCHEMA,
|
|
12
|
-
RUN_REQUEST_PREDICTION_SCHEMA,
|
|
13
|
-
RUN_REQUEST_SCHEMA,
|
|
14
|
-
explainRunRequest,
|
|
15
|
-
selfDigest as runtimeSelfDigest,
|
|
16
|
-
} from './runtime-contracts.mjs';
|
|
17
|
-
import { TODO_INDEPENDENCE_GUIDANCE_CODES } from './todo-independence-guidance.mjs';
|
|
18
|
-
|
|
19
|
-
export const TODO_WITNESS_SET_SCHEMA = 'lattice.todo_witness_set.v5';
|
|
20
|
-
/**
|
|
21
|
-
* まだ受理する旧witness set契約。v3以前の厳密なcompile意味を変えず、
|
|
22
|
-
* 既存宣言を書き換えさせないために読み口を残す。
|
|
23
|
-
*/
|
|
24
|
-
export const TODO_WITNESS_SET_LEGACY_SCHEMAS = Object.freeze([
|
|
25
|
-
'lattice.todo_witness_set.v4',
|
|
26
|
-
'lattice.todo_witness_set.v3',
|
|
27
|
-
'lattice.todo_witness_set.v2',
|
|
28
|
-
'lattice.todo_witness_set.v1',
|
|
29
|
-
]);
|
|
30
|
-
export const TODO_WITNESS_SET_SCHEMAS = Object.freeze([
|
|
31
|
-
TODO_WITNESS_SET_SCHEMA,
|
|
32
|
-
...TODO_WITNESS_SET_LEGACY_SCHEMAS,
|
|
33
|
-
]);
|
|
34
|
-
/** 宣言できる欄はversionごとに違う。どの版から使えるかを1箇所で持つ。 */
|
|
35
|
-
const CONCERN_ANCHOR_SCHEMAS = Object.freeze([
|
|
36
|
-
TODO_WITNESS_SET_SCHEMA,
|
|
37
|
-
'lattice.todo_witness_set.v4',
|
|
38
|
-
'lattice.todo_witness_set.v3',
|
|
39
|
-
'lattice.todo_witness_set.v2',
|
|
40
|
-
]);
|
|
41
|
-
const CREATES_SCHEMAS = Object.freeze([
|
|
42
|
-
TODO_WITNESS_SET_SCHEMA, 'lattice.todo_witness_set.v4', 'lattice.todo_witness_set.v3',
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
/** 1 taskが宣言できるconcern anchorの資源数と、資源あたりのsymbol数の上限。 */
|
|
46
|
-
export const TODO_CONCERN_ANCHOR_LIMIT = 256;
|
|
47
|
-
export const TODO_INDEPENDENCE_SCHEMA = 'lattice.todo_independence.v5';
|
|
48
|
-
export const TODO_INDEPENDENCE_PROJECTION_SCHEMA = 'lattice.todo_independence_projection.v2';
|
|
49
|
-
export const TODO_INDEPENDENCE_LEGACY_MARKER_SCHEMA = 'lattice.todo_independence_legacy_marker.v1';
|
|
50
|
-
export const TODO_INDEPENDENCE_LEGACY_SCHEMAS = Object.freeze([
|
|
51
|
-
'lattice.todo_independence.v1',
|
|
52
|
-
'lattice.todo_independence.v2',
|
|
53
|
-
'lattice.todo_independence.v3',
|
|
54
|
-
// v4は`scope_expanded`を持つが`first_seen_path_count`/`growth_events`が整数固定だった。
|
|
55
|
-
// v5でnull(=subset gapで累計が分からない)を足したので、**同じ版名で形を変えず**版を上げる。
|
|
56
|
-
// 同名変更は「新readerが旧artifactを読める」後方互換しか満たさず、**旧readerが新artifactを
|
|
57
|
-
// 読めない前方互換破壊**になる(2026-08-09のP0と同型・suzune [1183] / kanade [1200])。
|
|
58
|
-
'lattice.todo_independence.v4',
|
|
59
|
-
]);
|
|
60
|
-
|
|
61
|
-
/** boundary compileが一度に扱えるToDo数(runtime front-endのMAX_COLLECTIONと同じ閉じ方)。 */
|
|
62
|
-
export const TODO_INDEPENDENCE_TASK_LIMIT = 256;
|
|
63
|
-
export const TODO_INDEPENDENCE_LIST_LIMIT = 4_096;
|
|
64
|
-
export const TODO_INDEPENDENCE_COVERAGE = Object.freeze([
|
|
65
|
-
'verified', 'stale', 'superseded', 'missing',
|
|
66
|
-
]);
|
|
67
|
-
|
|
68
|
-
/** conflictを生んだresourceの種別。切断可能性の導出はこの種別だけを根拠にする。 */
|
|
69
|
-
export const TODO_INDEPENDENCE_CONFLICT_KINDS = Object.freeze([
|
|
70
|
-
'symbol', 'path', 'state', 'effect', 'line',
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
/** 切断可能性。code seamで切れるのはsymbol/path起因のconflictだけ(ADR 0128 Decision 2)。 */
|
|
74
|
-
export const TODO_INDEPENDENCE_SEVERABILITY = Object.freeze(['code_seam', 'serial']);
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* conflict kindから切断可能性を導く。
|
|
78
|
-
*
|
|
79
|
-
* 共有state/effectと意味的なlineはcode seamでは切断できない(RC1 boundary compilerの分類規則と同一)。
|
|
80
|
-
* read×write交差から実体化される`rw-*`はkind=stateなのでserialへ倒れる。seam候補を
|
|
81
|
-
* 見逃す方向にしか外れない保守的な誤りであり、既知の限界として受け入れる。
|
|
82
|
-
*/
|
|
83
|
-
export function severabilityOfConflictKind(kind) {
|
|
84
|
-
return ['symbol', 'path'].includes(kind) ? 'code_seam' : 'serial';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const GIT_SHA = /^[0-9a-f]{40}$/u;
|
|
88
|
-
const PROBE_BASE_SHA = '0'.repeat(40);
|
|
89
|
-
|
|
90
|
-
export const isGitSha = (value) => typeof value === 'string' && GIT_SHA.test(value);
|
|
91
|
-
|
|
92
|
-
function plain(value) {
|
|
93
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
94
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* 既知の旧independence artifactを、単なるschema文字列だけでなく版をまたいで不変な
|
|
99
|
-
* identity fieldの型までで識別する。本体構造や自己digestは旧契約validatorの再実装に
|
|
100
|
-
* なるため、ここでは検査しない。
|
|
101
|
-
*/
|
|
102
|
-
export function isTodoIndependenceLegacyArtifactIdentity(value) {
|
|
103
|
-
return plain(value) && TODO_INDEPENDENCE_LEGACY_SCHEMAS.includes(value.schema)
|
|
104
|
-
&& isTodoIdentifier(value.project_id) && isTodoIdentifier(value.plan_key)
|
|
105
|
-
&& isTodoIdentifier(value.plan_version) && isTodoDigest(value.topology_digest)
|
|
106
|
-
&& isTodoDigest(value.witness_set_digest) && isTodoDigest(value.result_digest)
|
|
107
|
-
&& isGitSha(value.base_sha);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* task別の宣言膨張の記録(v4から)。**判定を持たない——事実だけを置く。**
|
|
112
|
-
*
|
|
113
|
-
* 何が膨張の原因か(上流の契約確定に追従したのか、自分の変更の後始末か、元から在った面の
|
|
114
|
-
* 見落としか、思いつきで盛ったのか)は**機械には区別できない**。区別するのはAIであり、
|
|
115
|
-
* 装置が言えるのは「増えた」「何が増減した」「何回目か」「合流点か」までである。
|
|
116
|
-
* ここへ閾値も勧告も置かない(勧告はadvisory側の仕事)。
|
|
117
|
-
*
|
|
118
|
-
* `removed_paths`を持つのは、増加だけを見ると「盛った」と「宣言をやり直した」が同じ顔に
|
|
119
|
-
* なるからである。増減を両方見ないと読み手が誤る。
|
|
120
|
-
*
|
|
121
|
-
* `first_seen_path_count`と`growth_events`が**null**なのは「**このtaskの履歴が分からない**」で、
|
|
122
|
-
* 0とは違う。witnessはwave単位のsubsetで書き出せるので、比較相手のartifactに当該taskが
|
|
123
|
-
* 居ないことがある——その時に今回を初回と置くと、本当の初回と膨張回数が消える。
|
|
124
|
-
* 初回(比較相手が無い)は`compared_witness_digest=null`+数値、
|
|
125
|
-
* subset入替(比較相手はあるがtaskが居ない)は`compared_witness_digest=digest`+nullで区別する。
|
|
126
|
-
*/
|
|
127
|
-
function scopeExpansionEntry(value) {
|
|
128
|
-
return plain(value)
|
|
129
|
-
&& exactRecord(value, [
|
|
130
|
-
'task_id', 'compared_witness_digest', 'first_seen_path_count', 'path_count',
|
|
131
|
-
'added_paths', 'removed_paths', 'growth_events', 'gate_shape',
|
|
132
|
-
])
|
|
133
|
-
&& isTodoIdentifier(value.task_id)
|
|
134
|
-
&& (value.compared_witness_digest === null || isTodoDigest(value.compared_witness_digest))
|
|
135
|
-
&& (value.first_seen_path_count === null
|
|
136
|
-
|| (Number.isSafeInteger(value.first_seen_path_count) && value.first_seen_path_count >= 0))
|
|
137
|
-
&& Number.isSafeInteger(value.path_count) && value.path_count >= 0
|
|
138
|
-
&& boundedList(value.added_paths, (entry) => typeof entry === 'string' && entry.length > 0)
|
|
139
|
-
&& strictlySorted(value.added_paths)
|
|
140
|
-
&& boundedList(value.removed_paths, (entry) => typeof entry === 'string' && entry.length > 0)
|
|
141
|
-
&& strictlySorted(value.removed_paths)
|
|
142
|
-
&& (value.growth_events === null
|
|
143
|
-
|| (Number.isSafeInteger(value.growth_events) && value.growth_events >= 0))
|
|
144
|
-
&& typeof value.gate_shape === 'boolean';
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function boundedList(value, validator, limit = TODO_INDEPENDENCE_LIST_LIMIT) {
|
|
148
|
-
return Array.isArray(value) && value.length <= limit && value.every(validator);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
152
|
-
|
|
153
|
-
function strictlySorted(values, key = (value) => value) {
|
|
154
|
-
return values.every((value, index) => index === 0 || compareText(key(values[index - 1]), key(value)) < 0);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function sorted(values, key = (value) => value) {
|
|
158
|
-
return values.every((value, index) => index === 0 || compareText(key(values[index - 1]), key(value)) <= 0);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function boundedText(value, maximumBytes = 4_096) {
|
|
162
|
-
return typeof value === 'string' && value.length > 0
|
|
163
|
-
&& Buffer.byteLength(value) <= maximumBytes
|
|
164
|
-
&& !/[\u0000-\u001f\u007f]/u.test(value);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* witness setを、同じ宣言を持つ`lattice.run_request.v1`へ写す。
|
|
169
|
-
*
|
|
170
|
-
* manual witnessとsensor query setの判定正本は`explainRunRequest`だけが持つ(ADR 0123)。
|
|
171
|
-
* ここで同じ規則を書き直すと契約が二箇所へ分裂するため、検証もcompileもこの合成を通す。
|
|
172
|
-
* `requestId`はplan identityとwitness digestから導出され、run lifecycleへは登録されない。
|
|
173
|
-
*
|
|
174
|
-
* `concern_anchors`はここで落とす。並列可否の判定はこの合成requestだけを入力にするので、
|
|
175
|
-
* 落としておけばconcern宣言が判定へ影響しないことが構造で保証される(testの主張ではない)。
|
|
176
|
-
* 宣言はseam束縛の入力であり、witness setから直接読む。
|
|
177
|
-
*/
|
|
178
|
-
export function synthesizeWitnessRunRequest(witnessSet, { baseSha, requestId }) {
|
|
179
|
-
const taskIds = Object.keys(witnessSet.manual_witness).sort(compareText);
|
|
180
|
-
const request = {
|
|
181
|
-
schema: witnessSet.schema === TODO_WITNESS_SET_SCHEMA
|
|
182
|
-
? RUN_REQUEST_SCHEMA
|
|
183
|
-
: witnessSet.schema === 'lattice.todo_witness_set.v4'
|
|
184
|
-
? RUN_REQUEST_PREDICTION_SCHEMA
|
|
185
|
-
: RUN_REQUEST_DECLARATIVE_SCHEMA,
|
|
186
|
-
request_id: requestId,
|
|
187
|
-
repo: { base_sha: baseSha, root_kind: 'git' },
|
|
188
|
-
capacity: witnessSet.capacity,
|
|
189
|
-
todos: taskIds.map((taskId) => ({ todo_id: taskId })),
|
|
190
|
-
manual_witness: Object.fromEntries(taskIds.map((taskId) => {
|
|
191
|
-
const { concern_anchors: _concernAnchors, ...boundary } = witnessSet.manual_witness[taskId];
|
|
192
|
-
return [taskId, boundary];
|
|
193
|
-
})),
|
|
194
|
-
sensor_query_set: witnessSet.sensor_query_set,
|
|
195
|
-
executor_capability: { adapters: ['todo-independence'] },
|
|
196
|
-
claim_mode: RUN_REQUEST_CLAIM_MODE,
|
|
197
|
-
request_digest: '',
|
|
198
|
-
};
|
|
199
|
-
request.request_digest = runtimeSelfDigest(request, 'request_digest');
|
|
200
|
-
return request;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* 1 taskのconcern anchor宣言を検査する。
|
|
205
|
-
*
|
|
206
|
-
* `within`はそのtask自身が`owns`で主張している資源に限る。所有していない資源の内側に
|
|
207
|
-
* 担当を主張させない。symbol名の実在・資源内包含・task間排他はsensorとcompile側が見る。
|
|
208
|
-
*/
|
|
209
|
-
function explainConcernAnchors(anchors, owns, at) {
|
|
210
|
-
const reject = (reason, path) => ({ valid: false, reason, path });
|
|
211
|
-
if (!Array.isArray(anchors) || anchors.length > TODO_CONCERN_ANCHOR_LIMIT) {
|
|
212
|
-
return reject('bounded_collection_violation', at);
|
|
213
|
-
}
|
|
214
|
-
const ownedKeys = new Set(owns.map((own) => `${own.kind}\0${own.target}`));
|
|
215
|
-
for (const [index, entry] of anchors.entries()) {
|
|
216
|
-
const entryAt = `${at}/${index}`;
|
|
217
|
-
if (!exactRecord(entry, ['within', 'symbols'])) {
|
|
218
|
-
return reject('unexpected_or_missing_keys', entryAt);
|
|
219
|
-
}
|
|
220
|
-
if (!exactRecord(entry.within, ['kind', 'target'])
|
|
221
|
-
|| !['symbol', 'path'].includes(entry.within.kind)) {
|
|
222
|
-
return reject('invalid_concern_anchor_resource', `${entryAt}/within`);
|
|
223
|
-
}
|
|
224
|
-
const targetValid = entry.within.kind === 'path'
|
|
225
|
-
? repoRelativeResourceTarget(entry.within.target)
|
|
226
|
-
: boundedText(entry.within.target, 1_024);
|
|
227
|
-
if (!targetValid) return reject('invalid_concern_anchor_resource', `${entryAt}/within`);
|
|
228
|
-
if (!ownedKeys.has(`${entry.within.kind}\0${entry.within.target}`)) {
|
|
229
|
-
return reject('concern_anchor_resource_not_owned', `${entryAt}/within`);
|
|
230
|
-
}
|
|
231
|
-
if (!Array.isArray(entry.symbols) || entry.symbols.length < 1
|
|
232
|
-
|| entry.symbols.length > TODO_CONCERN_ANCHOR_LIMIT) {
|
|
233
|
-
return reject('bounded_collection_violation', `${entryAt}/symbols`);
|
|
234
|
-
}
|
|
235
|
-
if (!entry.symbols.every((symbol) => boundedText(symbol, 1_024))) {
|
|
236
|
-
return reject('invalid_concern_anchor_symbol', `${entryAt}/symbols`);
|
|
237
|
-
}
|
|
238
|
-
if (!strictlySorted(entry.symbols)) {
|
|
239
|
-
return reject('unsorted_or_duplicate_collection', `${entryAt}/symbols`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (!strictlySorted(anchors, (entry) => `${entry.within.kind}\0${entry.within.target}`)) {
|
|
243
|
-
return reject('unsorted_or_duplicate_collection', at);
|
|
244
|
-
}
|
|
245
|
-
return { valid: true };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* `lattice.todo_witness_set.v2`(およびconcern anchorを持たない旧v1)を検証し、
|
|
250
|
-
* 拒否理由とpathを返す。
|
|
251
|
-
*
|
|
252
|
-
* 自分のshapeだけをここで見て、witness本体はprobe requestへ合成して
|
|
253
|
-
* `explainRunRequest`へ委譲する。probeのbase_shaは検証専用の定数であり永続化しない。
|
|
254
|
-
* `concern_anchors`はprobeから落ちるので、ここだけが判定正本になる。
|
|
255
|
-
*/
|
|
256
|
-
export function explainTodoWitnessSet(value) {
|
|
257
|
-
const reject = (reason, at = '') => ({ valid: false, reason, path: at });
|
|
258
|
-
try {
|
|
259
|
-
if (!exactRecord(value, [
|
|
260
|
-
'schema', 'project_id', 'plan_key', 'capacity', 'sensor_query_set',
|
|
261
|
-
'manual_witness', 'witness_set_digest',
|
|
262
|
-
])) return reject('unexpected_or_missing_top_level_keys');
|
|
263
|
-
if (!TODO_WITNESS_SET_SCHEMAS.includes(value.schema)) return reject('schema_mismatch', '/schema');
|
|
264
|
-
if (!isTodoIdentifier(value.project_id)) return reject('invalid_identifier', '/project_id');
|
|
265
|
-
if (!isTodoIdentifier(value.plan_key)) return reject('invalid_identifier', '/plan_key');
|
|
266
|
-
if (!plain(value.manual_witness)) return reject('not_an_object', '/manual_witness');
|
|
267
|
-
const taskIds = Object.keys(value.manual_witness);
|
|
268
|
-
if (taskIds.length < 1 || taskIds.length > TODO_INDEPENDENCE_TASK_LIMIT) {
|
|
269
|
-
return reject('bounded_collection_violation', '/manual_witness');
|
|
270
|
-
}
|
|
271
|
-
if (!taskIds.every(isTodoIdentifier)) return reject('invalid_identifier', '/manual_witness');
|
|
272
|
-
if (value.witness_set_digest !== todoSelfDigest(value, 'witness_set_digest')) {
|
|
273
|
-
return reject('witness_set_digest_mismatch', '/witness_set_digest');
|
|
274
|
-
}
|
|
275
|
-
for (const taskId of taskIds) {
|
|
276
|
-
const witness = value.manual_witness[taskId];
|
|
277
|
-
if (plain(witness) && Object.hasOwn(witness, 'lines')
|
|
278
|
-
&& value.schema !== TODO_WITNESS_SET_SCHEMA) {
|
|
279
|
-
return reject('lines_require_witness_set_v5', `/manual_witness/${taskId}/lines`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
const probe = synthesizeWitnessRunRequest(value, {
|
|
283
|
-
baseSha: PROBE_BASE_SHA, requestId: 'witness-set-probe',
|
|
284
|
-
});
|
|
285
|
-
const explained = explainRunRequest(probe);
|
|
286
|
-
if (!explained.valid) return reject(explained.reason, explained.path);
|
|
287
|
-
// 版ごとの欄。probeはRUN_REQUEST_SCHEMAで合成するので創作宣言はそこを通ってしまう。
|
|
288
|
-
// 旧版の宣言に新しい欄を書けないことは、ここだけが見る。
|
|
289
|
-
for (const taskId of taskIds) {
|
|
290
|
-
const witness = value.manual_witness[taskId];
|
|
291
|
-
if (!CREATES_SCHEMAS.includes(value.schema)
|
|
292
|
-
&& witness.owns.some((own) => Object.hasOwn(own, 'creates'))) {
|
|
293
|
-
return reject('creates_require_witness_set_v3', `/manual_witness/${taskId}/owns`);
|
|
294
|
-
}
|
|
295
|
-
const at = `/manual_witness/${taskId}/concern_anchors`;
|
|
296
|
-
if (!Object.hasOwn(witness, 'concern_anchors')) continue;
|
|
297
|
-
if (!CONCERN_ANCHOR_SCHEMAS.includes(value.schema)) {
|
|
298
|
-
return reject('concern_anchors_require_witness_set_v2', at);
|
|
299
|
-
}
|
|
300
|
-
const anchors = explainConcernAnchors(witness.concern_anchors, witness.owns, at);
|
|
301
|
-
if (!anchors.valid) return reject(anchors.reason, anchors.path);
|
|
302
|
-
}
|
|
303
|
-
return { valid: true };
|
|
304
|
-
} catch {
|
|
305
|
-
return reject('non_canonical_witness_set_bytes');
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function validateTodoWitnessSet(value) {
|
|
310
|
-
return explainTodoWitnessSet(value).valid;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function conflictEntry(value) {
|
|
314
|
-
return exactRecord(value, ['task_ids', 'resource_id'])
|
|
315
|
-
&& Array.isArray(value.task_ids) && value.task_ids.length === 2
|
|
316
|
-
&& value.task_ids.every(isTodoIdentifier)
|
|
317
|
-
&& compareText(value.task_ids[0], value.task_ids[1]) < 0
|
|
318
|
-
&& boundedText(value.resource_id);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function repoRelativeResourceTarget(value) {
|
|
322
|
-
if (!boundedText(value) || value.startsWith('/') || value.includes('\\')
|
|
323
|
-
|| /^[A-Za-z]:/u.test(value)) return false;
|
|
324
|
-
const body = value.endsWith('/') ? value.slice(0, -1) : value;
|
|
325
|
-
return body.length > 0 && body.split('/')
|
|
326
|
-
.every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function conflictResourceEntry(value) {
|
|
330
|
-
if (!exactRecord(value, ['resource_id', 'kind', 'target'])
|
|
331
|
-
|| !boundedText(value.resource_id)
|
|
332
|
-
|| !TODO_INDEPENDENCE_CONFLICT_KINDS.includes(value.kind)) return false;
|
|
333
|
-
return value.kind === 'path'
|
|
334
|
-
? repoRelativeResourceTarget(value.target)
|
|
335
|
-
: boundedText(value.target);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export function isTodoIndependenceLegacyMarker(value) {
|
|
339
|
-
return exactRecord(value, [
|
|
340
|
-
'schema', 'legacy_schema', 'project_id', 'plan_key', 'plan_version',
|
|
341
|
-
'topology_digest', 'base_sha',
|
|
342
|
-
])
|
|
343
|
-
&& value.schema === TODO_INDEPENDENCE_LEGACY_MARKER_SCHEMA
|
|
344
|
-
&& TODO_INDEPENDENCE_LEGACY_SCHEMAS.includes(value.legacy_schema)
|
|
345
|
-
&& isTodoIdentifier(value.project_id) && isTodoIdentifier(value.plan_key)
|
|
346
|
-
&& value.plan_version === null && value.topology_digest === null && value.base_sha === null;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* task別の宣言境界。鮮度判定をartifactとgit diffだけで閉じるために持つ(ADR 0128 Decision 4)。
|
|
351
|
-
* witness setを読み直さないので、参照コストは定数のまま保たれる。
|
|
352
|
-
*/
|
|
353
|
-
function taskBoundaryEntry(value) {
|
|
354
|
-
return exactRecord(value, ['task_id', 'paths'])
|
|
355
|
-
&& isTodoIdentifier(value.task_id)
|
|
356
|
-
&& Array.isArray(value.paths) && value.paths.length <= TODO_INDEPENDENCE_LIST_LIMIT
|
|
357
|
-
&& value.paths.every((path) => boundedText(path)) && strictlySorted(value.paths);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function precedenceEntry(value) {
|
|
361
|
-
return exactRecord(value, ['from_task_id', 'to_task_id', 'reason'])
|
|
362
|
-
&& isTodoIdentifier(value.from_task_id) && isTodoIdentifier(value.to_task_id)
|
|
363
|
-
&& value.from_task_id !== value.to_task_id && boundedText(value.reason);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function unknownEntry(value) {
|
|
367
|
-
return exactRecord(value, ['task_id', 'kind', 'ref'])
|
|
368
|
-
&& isTodoIdentifier(value.task_id) && isTodoIdentifier(value.kind) && boundedText(value.ref);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function waveEntry(value) {
|
|
372
|
-
return exactRecord(value, ['task_ids'])
|
|
373
|
-
&& Array.isArray(value.task_ids) && value.task_ids.length >= 1
|
|
374
|
-
&& value.task_ids.length <= TODO_INDEPENDENCE_TASK_LIMIT
|
|
375
|
-
&& value.task_ids.every(isTodoIdentifier) && strictlySorted(value.task_ids);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function wavePlan(value, taskIds) {
|
|
379
|
-
if (value === null) return true;
|
|
380
|
-
if (!exactRecord(value, ['waves', 'minimum_feasible_waves'])
|
|
381
|
-
|| !boundedList(value.waves, waveEntry, TODO_INDEPENDENCE_TASK_LIMIT)
|
|
382
|
-
|| !isNonNegativeSafeInteger(value.minimum_feasible_waves)
|
|
383
|
-
|| value.waves.length !== value.minimum_feasible_waves) return false;
|
|
384
|
-
const scheduled = value.waves.flatMap((wave) => wave.task_ids).sort(compareText);
|
|
385
|
-
return scheduled.length === taskIds.length
|
|
386
|
-
&& scheduled.every((taskId, index) => taskId === taskIds[index]);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* `lattice.todo_independence.v3`。
|
|
391
|
-
*
|
|
392
|
-
* conflict/precedence/unknownはnormalized boundary graphから採る(ADR 0127 Decision 4)。
|
|
393
|
-
* conflictは`conflict_resources`のresource idを参照し、kindとnormalized targetを一度だけ保持する。
|
|
394
|
-
* `task_boundaries`はtask別の宣言境界で、鮮度のdiff交差判定をartifactだけで閉じるために持つ。
|
|
395
|
-
* `wave_plan`はschedulability compileが`compiled`を返した時だけ持ち、unknownが残る間はnullになる。
|
|
396
|
-
* verdictに現れないペアをverified独立と読めるのは、両taskにunknownが無いときだけである。
|
|
397
|
-
*/
|
|
398
|
-
export function validateTodoIndependence(value) {
|
|
399
|
-
try {
|
|
400
|
-
if (!exactRecord(value, [
|
|
401
|
-
'schema', 'project_id', 'plan_key', 'plan_version', 'topology_digest', 'base_sha',
|
|
402
|
-
'witness_set_digest', 'compiled_at', 'task_ids', 'task_boundaries', 'conflict_resources', 'conflicts',
|
|
403
|
-
'precedences', 'unknowns', 'wave_plan', 'scope_expanded', 'outcome', 'result_digest',
|
|
404
|
-
])) return false;
|
|
405
|
-
if (value.schema !== TODO_INDEPENDENCE_SCHEMA) return false;
|
|
406
|
-
if (!isTodoIdentifier(value.project_id) || !isTodoIdentifier(value.plan_key)
|
|
407
|
-
|| !isTodoIdentifier(value.plan_version)) return false;
|
|
408
|
-
if (!isTodoDigest(value.topology_digest) || !isGitSha(value.base_sha)
|
|
409
|
-
|| !isTodoDigest(value.witness_set_digest)
|
|
410
|
-
|| !isStrictTodoTimestamp(value.compiled_at)) return false;
|
|
411
|
-
if (!Array.isArray(value.task_ids) || value.task_ids.length < 1
|
|
412
|
-
|| value.task_ids.length > TODO_INDEPENDENCE_TASK_LIMIT
|
|
413
|
-
|| !value.task_ids.every(isTodoIdentifier) || !strictlySorted(value.task_ids)) return false;
|
|
414
|
-
const known = new Set(value.task_ids);
|
|
415
|
-
// 宣言境界はcompile対象taskとちょうど一対一で対応する。欠けたtaskがあると、
|
|
416
|
-
// そのtaskだけ交差判定ができないのに全体はverifiedを名乗れてしまう。
|
|
417
|
-
if (!boundedList(value.task_boundaries, taskBoundaryEntry, TODO_INDEPENDENCE_TASK_LIMIT)
|
|
418
|
-
|| value.task_boundaries.length !== value.task_ids.length
|
|
419
|
-
|| !strictlySorted(value.task_boundaries, (entry) => entry.task_id)
|
|
420
|
-
|| !value.task_boundaries.every((entry, index) => entry.task_id === value.task_ids[index])) {
|
|
421
|
-
return false;
|
|
422
|
-
}
|
|
423
|
-
// scope_expandedはtask_idsとちょうど一対一。欠けたtaskがあると「膨張していない」と
|
|
424
|
-
// 「まだ見ていない」が同じ顔になる。
|
|
425
|
-
if (!boundedList(value.scope_expanded, scopeExpansionEntry, TODO_INDEPENDENCE_TASK_LIMIT)
|
|
426
|
-
|| value.scope_expanded.length !== value.task_ids.length
|
|
427
|
-
|| !strictlySorted(value.scope_expanded, (entry) => entry.task_id)
|
|
428
|
-
|| !value.scope_expanded.every((entry, index) => entry.task_id === value.task_ids[index])) {
|
|
429
|
-
return false;
|
|
430
|
-
}
|
|
431
|
-
if (!boundedList(value.conflict_resources, conflictResourceEntry)
|
|
432
|
-
|| !strictlySorted(value.conflict_resources, (entry) => entry.resource_id)) return false;
|
|
433
|
-
const conflictResourceIds = new Set(value.conflict_resources.map(({ resource_id: id }) => id));
|
|
434
|
-
if (!boundedList(value.conflicts, conflictEntry)
|
|
435
|
-
|| !value.conflicts.every((entry) => entry.task_ids.every((taskId) => known.has(taskId)))
|
|
436
|
-
|| !value.conflicts.every((entry) => conflictResourceIds.has(entry.resource_id))
|
|
437
|
-
|| !strictlySorted(value.conflicts, (entry) => (
|
|
438
|
-
`${entry.task_ids[0]}\0${entry.task_ids[1]}\0${entry.resource_id}`))) return false;
|
|
439
|
-
const referencedResourceIds = new Set(value.conflicts.map(({ resource_id: id }) => id));
|
|
440
|
-
if (referencedResourceIds.size !== value.conflict_resources.length) return false;
|
|
441
|
-
if (!boundedList(value.precedences, precedenceEntry)
|
|
442
|
-
|| !value.precedences.every((entry) => known.has(entry.from_task_id) && known.has(entry.to_task_id))
|
|
443
|
-
|| !strictlySorted(value.precedences, (entry) => (
|
|
444
|
-
`${entry.from_task_id}\0${entry.to_task_id}\0${entry.reason}`))) return false;
|
|
445
|
-
if (!boundedList(value.unknowns, unknownEntry)
|
|
446
|
-
|| !value.unknowns.every((entry) => known.has(entry.task_id))
|
|
447
|
-
|| !sorted(value.unknowns, (entry) => (
|
|
448
|
-
`${entry.task_id}\0${entry.kind}\0${entry.ref}`))) return false;
|
|
449
|
-
if (!['compiled', 'unknown'].includes(value.outcome)) return false;
|
|
450
|
-
// unknownが残る限りwave planは主張しない。compiledならunknownは空でなければならない。
|
|
451
|
-
if (value.outcome === 'compiled' && value.unknowns.length > 0) return false;
|
|
452
|
-
if (value.outcome === 'unknown' && value.wave_plan !== null) return false;
|
|
453
|
-
if (!wavePlan(value.wave_plan, value.task_ids)) return false;
|
|
454
|
-
return isTodoDigest(value.result_digest)
|
|
455
|
-
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
456
|
-
} catch {
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function groupEntry(value) {
|
|
462
|
-
return exactRecord(value, ['task_ids'])
|
|
463
|
-
&& Array.isArray(value.task_ids) && value.task_ids.length >= 1
|
|
464
|
-
&& value.task_ids.length <= TODO_INDEPENDENCE_TASK_LIMIT
|
|
465
|
-
&& value.task_ids.every(isTodoIdentifier) && strictlySorted(value.task_ids);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function pairKindAndSeverability(value) {
|
|
469
|
-
// conflictはkindを持ちseverabilityがそこから導かれる。precedenceは順序制約であって
|
|
470
|
-
// resource起因ではないためkindを持たず、常にserialになる。
|
|
471
|
-
if (value.type === 'conflict') {
|
|
472
|
-
return TODO_INDEPENDENCE_CONFLICT_KINDS.includes(value.kind)
|
|
473
|
-
&& value.severability === severabilityOfConflictKind(value.kind);
|
|
474
|
-
}
|
|
475
|
-
return value.kind === null && value.severability === 'serial';
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function serializeEntry(value) {
|
|
479
|
-
return exactRecord(value, ['task_ids', 'type', 'detail', 'kind', 'severability'])
|
|
480
|
-
&& Array.isArray(value.task_ids) && value.task_ids.length === 2
|
|
481
|
-
&& value.task_ids.every(isTodoIdentifier)
|
|
482
|
-
&& compareText(value.task_ids[0], value.task_ids[1]) < 0
|
|
483
|
-
&& ['conflict', 'precedence'].includes(value.type) && boundedText(value.detail)
|
|
484
|
-
&& pairKindAndSeverability(value);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* 着手候補と進行中ToDoの競合。
|
|
489
|
-
*
|
|
490
|
-
* v1は両端がready集合のペアだけを採っており、片端がactiveのペアを黙って捨てていた。
|
|
491
|
-
* 着手する瞬間に最も危ないのはこの組み合わせなので、独立した面として持つ(ADR 0128 Decision 3)。
|
|
492
|
-
*/
|
|
493
|
-
function activeConflictEntry(value) {
|
|
494
|
-
return exactRecord(value, [
|
|
495
|
-
'ready_task_id', 'active_task_id', 'type', 'detail', 'kind', 'severability',
|
|
496
|
-
]) && isTodoIdentifier(value.ready_task_id) && isTodoIdentifier(value.active_task_id)
|
|
497
|
-
&& value.ready_task_id !== value.active_task_id
|
|
498
|
-
&& ['conflict', 'precedence'].includes(value.type) && boundedText(value.detail)
|
|
499
|
-
&& pairKindAndSeverability(value);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* 案内。単一正本のcatalogが返した形をそのまま載せる(ADR 0130 Decision 1・2)。
|
|
504
|
-
* 面ごとに文言を組み立て直さないため、ここではshapeだけを検査する。
|
|
505
|
-
*/
|
|
506
|
-
function guidanceEntry(value) {
|
|
507
|
-
return exactRecord(value, ['code', 'message', 'next_action'])
|
|
508
|
-
&& TODO_INDEPENDENCE_GUIDANCE_CODES.includes(value.code)
|
|
509
|
-
&& boundedText(value.message) && isTodoIdentifier(value.next_action);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* 鮮度の内訳。`coverage`がsha水準の事実を述べるのに対し、こちらは
|
|
514
|
-
* 「そのdiffが宣言境界に触れたか」というtask単位の事実を述べる(ADR 0128 Decision 4)。
|
|
515
|
-
*/
|
|
516
|
-
function driftEntry(value) {
|
|
517
|
-
if (value === null) return true;
|
|
518
|
-
return exactRecord(value, ['base_reachable', 'changed_path_count', 'intersecting_task_ids'])
|
|
519
|
-
&& typeof value.base_reachable === 'boolean'
|
|
520
|
-
&& isNonNegativeSafeInteger(value.changed_path_count)
|
|
521
|
-
&& Array.isArray(value.intersecting_task_ids)
|
|
522
|
-
&& value.intersecting_task_ids.length <= TODO_INDEPENDENCE_TASK_LIMIT
|
|
523
|
-
&& value.intersecting_task_ids.every(isTodoIdentifier)
|
|
524
|
-
&& strictlySorted(value.intersecting_task_ids);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function unknownTaskEntry(value) {
|
|
528
|
-
return exactRecord(value, ['task_id', 'unknowns'])
|
|
529
|
-
&& isTodoIdentifier(value.task_id)
|
|
530
|
-
&& Array.isArray(value.unknowns) && value.unknowns.length >= 1
|
|
531
|
-
&& value.unknowns.length <= TODO_INDEPENDENCE_LIST_LIMIT
|
|
532
|
-
&& value.unknowns.every((entry) => exactRecord(entry, ['kind', 'ref'])
|
|
533
|
-
&& isTodoIdentifier(entry.kind) && boundedText(entry.ref));
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* `lattice.todo_independence_projection.v2`。
|
|
538
|
-
*
|
|
539
|
-
* ready frontierを「検証済み並列グループ」「直列化すべき組」「未検査」へ分けた読み出し面。
|
|
540
|
-
* v2は進行中ToDoとの競合(`conflicts_with_active`)と鮮度の内訳(`drift`)を加える。
|
|
541
|
-
* `todo_status_result`と`dispatch_frontier`は変更せず、加算の別面として持つ(ADR 0124の規律)。
|
|
542
|
-
*/
|
|
543
|
-
export function validateTodoIndependenceProjection(value) {
|
|
544
|
-
try {
|
|
545
|
-
if (!exactRecord(value, [
|
|
546
|
-
'schema', 'project_id', 'plan_key', 'coverage', 'compiled_base_sha', 'current_base_sha',
|
|
547
|
-
'plan_version', 'topology_digest', 'active_task_ids', 'uncovered_active_task_ids',
|
|
548
|
-
'drift', 'guidance', 'frontier', 'result_digest',
|
|
549
|
-
])) return false;
|
|
550
|
-
if (value.schema !== TODO_INDEPENDENCE_PROJECTION_SCHEMA) return false;
|
|
551
|
-
if (!isTodoIdentifier(value.project_id)) return false;
|
|
552
|
-
if (value.plan_key !== null && !isTodoIdentifier(value.plan_key)) return false;
|
|
553
|
-
if (!TODO_INDEPENDENCE_COVERAGE.includes(value.coverage)) return false;
|
|
554
|
-
if (value.compiled_base_sha !== null && !isGitSha(value.compiled_base_sha)) return false;
|
|
555
|
-
if (!isGitSha(value.current_base_sha)) return false;
|
|
556
|
-
if (value.plan_version !== null && !isTodoIdentifier(value.plan_version)) return false;
|
|
557
|
-
if (value.topology_digest !== null && !isTodoDigest(value.topology_digest)) return false;
|
|
558
|
-
// 記録が無い状態でcompile済みidentityを名乗らない。
|
|
559
|
-
if (value.coverage === 'missing'
|
|
560
|
-
&& (value.compiled_base_sha !== null || value.topology_digest !== null)) return false;
|
|
561
|
-
if (['verified', 'stale'].includes(value.coverage) && value.compiled_base_sha === null) return false;
|
|
562
|
-
if (value.coverage === 'superseded' && value.compiled_base_sha === null
|
|
563
|
-
&& (value.plan_version !== null || value.topology_digest !== null)) return false;
|
|
564
|
-
for (const key of ['active_task_ids', 'uncovered_active_task_ids']) {
|
|
565
|
-
if (!Array.isArray(value[key]) || value[key].length > TODO_INDEPENDENCE_TASK_LIMIT
|
|
566
|
-
|| !value[key].every(isTodoIdentifier) || !strictlySorted(value[key])) return false;
|
|
567
|
-
}
|
|
568
|
-
// 宣言のないactiveは考慮済みactiveの部分集合でなければならない。
|
|
569
|
-
const activeSet = new Set(value.active_task_ids);
|
|
570
|
-
if (!value.uncovered_active_task_ids.every((taskId) => activeSet.has(taskId))) return false;
|
|
571
|
-
if (!driftEntry(value.drift)) return false;
|
|
572
|
-
if (!guidanceEntry(value.guidance)) return false;
|
|
573
|
-
// driftはstale時の内訳。それ以外で語ると鮮度の事実を二重に主張することになる。
|
|
574
|
-
if (value.coverage !== 'stale' && value.drift !== null) return false;
|
|
575
|
-
if (!exactRecord(value.frontier, [
|
|
576
|
-
'parallel_groups', 'serialize_pairs', 'conflicts_with_active', 'unknown',
|
|
577
|
-
])) return false;
|
|
578
|
-
if (!boundedList(value.frontier.parallel_groups, groupEntry, TODO_INDEPENDENCE_TASK_LIMIT)
|
|
579
|
-
|| !strictlySorted(value.frontier.parallel_groups, (entry) => entry.task_ids[0])) return false;
|
|
580
|
-
if (!boundedList(value.frontier.serialize_pairs, serializeEntry)
|
|
581
|
-
|| !strictlySorted(value.frontier.serialize_pairs, (entry) => (
|
|
582
|
-
`${entry.task_ids[0]}\0${entry.task_ids[1]}\0${entry.type}\0${entry.detail}`))) return false;
|
|
583
|
-
if (!boundedList(value.frontier.conflicts_with_active, activeConflictEntry)
|
|
584
|
-
|| !strictlySorted(value.frontier.conflicts_with_active, (entry) => (
|
|
585
|
-
`${entry.ready_task_id}\0${entry.active_task_id}\0${entry.type}\0${entry.detail}`))) {
|
|
586
|
-
return false;
|
|
587
|
-
}
|
|
588
|
-
if (!boundedList(value.frontier.unknown, unknownTaskEntry, TODO_INDEPENDENCE_TASK_LIMIT)
|
|
589
|
-
|| !strictlySorted(value.frontier.unknown, (entry) => entry.task_id)) return false;
|
|
590
|
-
return isTodoDigest(value.result_digest)
|
|
591
|
-
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
592
|
-
} catch {
|
|
593
|
-
return false;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
exactRecord,
|
|
3
|
+
isNonNegativeSafeInteger,
|
|
4
|
+
isStrictTodoTimestamp,
|
|
5
|
+
isTodoDigest,
|
|
6
|
+
isTodoIdentifier,
|
|
7
|
+
todoSelfDigest,
|
|
8
|
+
} from './todo-contracts.mjs';
|
|
9
|
+
import {
|
|
10
|
+
RUN_REQUEST_CLAIM_MODE,
|
|
11
|
+
RUN_REQUEST_DECLARATIVE_SCHEMA,
|
|
12
|
+
RUN_REQUEST_PREDICTION_SCHEMA,
|
|
13
|
+
RUN_REQUEST_SCHEMA,
|
|
14
|
+
explainRunRequest,
|
|
15
|
+
selfDigest as runtimeSelfDigest,
|
|
16
|
+
} from './runtime-contracts.mjs';
|
|
17
|
+
import { TODO_INDEPENDENCE_GUIDANCE_CODES } from './todo-independence-guidance.mjs';
|
|
18
|
+
|
|
19
|
+
export const TODO_WITNESS_SET_SCHEMA = 'lattice.todo_witness_set.v5';
|
|
20
|
+
/**
|
|
21
|
+
* まだ受理する旧witness set契約。v3以前の厳密なcompile意味を変えず、
|
|
22
|
+
* 既存宣言を書き換えさせないために読み口を残す。
|
|
23
|
+
*/
|
|
24
|
+
export const TODO_WITNESS_SET_LEGACY_SCHEMAS = Object.freeze([
|
|
25
|
+
'lattice.todo_witness_set.v4',
|
|
26
|
+
'lattice.todo_witness_set.v3',
|
|
27
|
+
'lattice.todo_witness_set.v2',
|
|
28
|
+
'lattice.todo_witness_set.v1',
|
|
29
|
+
]);
|
|
30
|
+
export const TODO_WITNESS_SET_SCHEMAS = Object.freeze([
|
|
31
|
+
TODO_WITNESS_SET_SCHEMA,
|
|
32
|
+
...TODO_WITNESS_SET_LEGACY_SCHEMAS,
|
|
33
|
+
]);
|
|
34
|
+
/** 宣言できる欄はversionごとに違う。どの版から使えるかを1箇所で持つ。 */
|
|
35
|
+
const CONCERN_ANCHOR_SCHEMAS = Object.freeze([
|
|
36
|
+
TODO_WITNESS_SET_SCHEMA,
|
|
37
|
+
'lattice.todo_witness_set.v4',
|
|
38
|
+
'lattice.todo_witness_set.v3',
|
|
39
|
+
'lattice.todo_witness_set.v2',
|
|
40
|
+
]);
|
|
41
|
+
const CREATES_SCHEMAS = Object.freeze([
|
|
42
|
+
TODO_WITNESS_SET_SCHEMA, 'lattice.todo_witness_set.v4', 'lattice.todo_witness_set.v3',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** 1 taskが宣言できるconcern anchorの資源数と、資源あたりのsymbol数の上限。 */
|
|
46
|
+
export const TODO_CONCERN_ANCHOR_LIMIT = 256;
|
|
47
|
+
export const TODO_INDEPENDENCE_SCHEMA = 'lattice.todo_independence.v5';
|
|
48
|
+
export const TODO_INDEPENDENCE_PROJECTION_SCHEMA = 'lattice.todo_independence_projection.v2';
|
|
49
|
+
export const TODO_INDEPENDENCE_LEGACY_MARKER_SCHEMA = 'lattice.todo_independence_legacy_marker.v1';
|
|
50
|
+
export const TODO_INDEPENDENCE_LEGACY_SCHEMAS = Object.freeze([
|
|
51
|
+
'lattice.todo_independence.v1',
|
|
52
|
+
'lattice.todo_independence.v2',
|
|
53
|
+
'lattice.todo_independence.v3',
|
|
54
|
+
// v4は`scope_expanded`を持つが`first_seen_path_count`/`growth_events`が整数固定だった。
|
|
55
|
+
// v5でnull(=subset gapで累計が分からない)を足したので、**同じ版名で形を変えず**版を上げる。
|
|
56
|
+
// 同名変更は「新readerが旧artifactを読める」後方互換しか満たさず、**旧readerが新artifactを
|
|
57
|
+
// 読めない前方互換破壊**になる(2026-08-09のP0と同型・suzune [1183] / kanade [1200])。
|
|
58
|
+
'lattice.todo_independence.v4',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/** boundary compileが一度に扱えるToDo数(runtime front-endのMAX_COLLECTIONと同じ閉じ方)。 */
|
|
62
|
+
export const TODO_INDEPENDENCE_TASK_LIMIT = 256;
|
|
63
|
+
export const TODO_INDEPENDENCE_LIST_LIMIT = 4_096;
|
|
64
|
+
export const TODO_INDEPENDENCE_COVERAGE = Object.freeze([
|
|
65
|
+
'verified', 'stale', 'superseded', 'missing',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/** conflictを生んだresourceの種別。切断可能性の導出はこの種別だけを根拠にする。 */
|
|
69
|
+
export const TODO_INDEPENDENCE_CONFLICT_KINDS = Object.freeze([
|
|
70
|
+
'symbol', 'path', 'state', 'effect', 'line',
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/** 切断可能性。code seamで切れるのはsymbol/path起因のconflictだけ(ADR 0128 Decision 2)。 */
|
|
74
|
+
export const TODO_INDEPENDENCE_SEVERABILITY = Object.freeze(['code_seam', 'serial']);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* conflict kindから切断可能性を導く。
|
|
78
|
+
*
|
|
79
|
+
* 共有state/effectと意味的なlineはcode seamでは切断できない(RC1 boundary compilerの分類規則と同一)。
|
|
80
|
+
* read×write交差から実体化される`rw-*`はkind=stateなのでserialへ倒れる。seam候補を
|
|
81
|
+
* 見逃す方向にしか外れない保守的な誤りであり、既知の限界として受け入れる。
|
|
82
|
+
*/
|
|
83
|
+
export function severabilityOfConflictKind(kind) {
|
|
84
|
+
return ['symbol', 'path'].includes(kind) ? 'code_seam' : 'serial';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const GIT_SHA = /^[0-9a-f]{40}$/u;
|
|
88
|
+
const PROBE_BASE_SHA = '0'.repeat(40);
|
|
89
|
+
|
|
90
|
+
export const isGitSha = (value) => typeof value === 'string' && GIT_SHA.test(value);
|
|
91
|
+
|
|
92
|
+
function plain(value) {
|
|
93
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
94
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 既知の旧independence artifactを、単なるschema文字列だけでなく版をまたいで不変な
|
|
99
|
+
* identity fieldの型までで識別する。本体構造や自己digestは旧契約validatorの再実装に
|
|
100
|
+
* なるため、ここでは検査しない。
|
|
101
|
+
*/
|
|
102
|
+
export function isTodoIndependenceLegacyArtifactIdentity(value) {
|
|
103
|
+
return plain(value) && TODO_INDEPENDENCE_LEGACY_SCHEMAS.includes(value.schema)
|
|
104
|
+
&& isTodoIdentifier(value.project_id) && isTodoIdentifier(value.plan_key)
|
|
105
|
+
&& isTodoIdentifier(value.plan_version) && isTodoDigest(value.topology_digest)
|
|
106
|
+
&& isTodoDigest(value.witness_set_digest) && isTodoDigest(value.result_digest)
|
|
107
|
+
&& isGitSha(value.base_sha);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* task別の宣言膨張の記録(v4から)。**判定を持たない——事実だけを置く。**
|
|
112
|
+
*
|
|
113
|
+
* 何が膨張の原因か(上流の契約確定に追従したのか、自分の変更の後始末か、元から在った面の
|
|
114
|
+
* 見落としか、思いつきで盛ったのか)は**機械には区別できない**。区別するのはAIであり、
|
|
115
|
+
* 装置が言えるのは「増えた」「何が増減した」「何回目か」「合流点か」までである。
|
|
116
|
+
* ここへ閾値も勧告も置かない(勧告はadvisory側の仕事)。
|
|
117
|
+
*
|
|
118
|
+
* `removed_paths`を持つのは、増加だけを見ると「盛った」と「宣言をやり直した」が同じ顔に
|
|
119
|
+
* なるからである。増減を両方見ないと読み手が誤る。
|
|
120
|
+
*
|
|
121
|
+
* `first_seen_path_count`と`growth_events`が**null**なのは「**このtaskの履歴が分からない**」で、
|
|
122
|
+
* 0とは違う。witnessはwave単位のsubsetで書き出せるので、比較相手のartifactに当該taskが
|
|
123
|
+
* 居ないことがある——その時に今回を初回と置くと、本当の初回と膨張回数が消える。
|
|
124
|
+
* 初回(比較相手が無い)は`compared_witness_digest=null`+数値、
|
|
125
|
+
* subset入替(比較相手はあるがtaskが居ない)は`compared_witness_digest=digest`+nullで区別する。
|
|
126
|
+
*/
|
|
127
|
+
function scopeExpansionEntry(value) {
|
|
128
|
+
return plain(value)
|
|
129
|
+
&& exactRecord(value, [
|
|
130
|
+
'task_id', 'compared_witness_digest', 'first_seen_path_count', 'path_count',
|
|
131
|
+
'added_paths', 'removed_paths', 'growth_events', 'gate_shape',
|
|
132
|
+
])
|
|
133
|
+
&& isTodoIdentifier(value.task_id)
|
|
134
|
+
&& (value.compared_witness_digest === null || isTodoDigest(value.compared_witness_digest))
|
|
135
|
+
&& (value.first_seen_path_count === null
|
|
136
|
+
|| (Number.isSafeInteger(value.first_seen_path_count) && value.first_seen_path_count >= 0))
|
|
137
|
+
&& Number.isSafeInteger(value.path_count) && value.path_count >= 0
|
|
138
|
+
&& boundedList(value.added_paths, (entry) => typeof entry === 'string' && entry.length > 0)
|
|
139
|
+
&& strictlySorted(value.added_paths)
|
|
140
|
+
&& boundedList(value.removed_paths, (entry) => typeof entry === 'string' && entry.length > 0)
|
|
141
|
+
&& strictlySorted(value.removed_paths)
|
|
142
|
+
&& (value.growth_events === null
|
|
143
|
+
|| (Number.isSafeInteger(value.growth_events) && value.growth_events >= 0))
|
|
144
|
+
&& typeof value.gate_shape === 'boolean';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function boundedList(value, validator, limit = TODO_INDEPENDENCE_LIST_LIMIT) {
|
|
148
|
+
return Array.isArray(value) && value.length <= limit && value.every(validator);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
152
|
+
|
|
153
|
+
function strictlySorted(values, key = (value) => value) {
|
|
154
|
+
return values.every((value, index) => index === 0 || compareText(key(values[index - 1]), key(value)) < 0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sorted(values, key = (value) => value) {
|
|
158
|
+
return values.every((value, index) => index === 0 || compareText(key(values[index - 1]), key(value)) <= 0);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function boundedText(value, maximumBytes = 4_096) {
|
|
162
|
+
return typeof value === 'string' && value.length > 0
|
|
163
|
+
&& Buffer.byteLength(value) <= maximumBytes
|
|
164
|
+
&& !/[\u0000-\u001f\u007f]/u.test(value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* witness setを、同じ宣言を持つ`lattice.run_request.v1`へ写す。
|
|
169
|
+
*
|
|
170
|
+
* manual witnessとsensor query setの判定正本は`explainRunRequest`だけが持つ(ADR 0123)。
|
|
171
|
+
* ここで同じ規則を書き直すと契約が二箇所へ分裂するため、検証もcompileもこの合成を通す。
|
|
172
|
+
* `requestId`はplan identityとwitness digestから導出され、run lifecycleへは登録されない。
|
|
173
|
+
*
|
|
174
|
+
* `concern_anchors`はここで落とす。並列可否の判定はこの合成requestだけを入力にするので、
|
|
175
|
+
* 落としておけばconcern宣言が判定へ影響しないことが構造で保証される(testの主張ではない)。
|
|
176
|
+
* 宣言はseam束縛の入力であり、witness setから直接読む。
|
|
177
|
+
*/
|
|
178
|
+
export function synthesizeWitnessRunRequest(witnessSet, { baseSha, requestId }) {
|
|
179
|
+
const taskIds = Object.keys(witnessSet.manual_witness).sort(compareText);
|
|
180
|
+
const request = {
|
|
181
|
+
schema: witnessSet.schema === TODO_WITNESS_SET_SCHEMA
|
|
182
|
+
? RUN_REQUEST_SCHEMA
|
|
183
|
+
: witnessSet.schema === 'lattice.todo_witness_set.v4'
|
|
184
|
+
? RUN_REQUEST_PREDICTION_SCHEMA
|
|
185
|
+
: RUN_REQUEST_DECLARATIVE_SCHEMA,
|
|
186
|
+
request_id: requestId,
|
|
187
|
+
repo: { base_sha: baseSha, root_kind: 'git' },
|
|
188
|
+
capacity: witnessSet.capacity,
|
|
189
|
+
todos: taskIds.map((taskId) => ({ todo_id: taskId })),
|
|
190
|
+
manual_witness: Object.fromEntries(taskIds.map((taskId) => {
|
|
191
|
+
const { concern_anchors: _concernAnchors, ...boundary } = witnessSet.manual_witness[taskId];
|
|
192
|
+
return [taskId, boundary];
|
|
193
|
+
})),
|
|
194
|
+
sensor_query_set: witnessSet.sensor_query_set,
|
|
195
|
+
executor_capability: { adapters: ['todo-independence'] },
|
|
196
|
+
claim_mode: RUN_REQUEST_CLAIM_MODE,
|
|
197
|
+
request_digest: '',
|
|
198
|
+
};
|
|
199
|
+
request.request_digest = runtimeSelfDigest(request, 'request_digest');
|
|
200
|
+
return request;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 1 taskのconcern anchor宣言を検査する。
|
|
205
|
+
*
|
|
206
|
+
* `within`はそのtask自身が`owns`で主張している資源に限る。所有していない資源の内側に
|
|
207
|
+
* 担当を主張させない。symbol名の実在・資源内包含・task間排他はsensorとcompile側が見る。
|
|
208
|
+
*/
|
|
209
|
+
function explainConcernAnchors(anchors, owns, at) {
|
|
210
|
+
const reject = (reason, path) => ({ valid: false, reason, path });
|
|
211
|
+
if (!Array.isArray(anchors) || anchors.length > TODO_CONCERN_ANCHOR_LIMIT) {
|
|
212
|
+
return reject('bounded_collection_violation', at);
|
|
213
|
+
}
|
|
214
|
+
const ownedKeys = new Set(owns.map((own) => `${own.kind}\0${own.target}`));
|
|
215
|
+
for (const [index, entry] of anchors.entries()) {
|
|
216
|
+
const entryAt = `${at}/${index}`;
|
|
217
|
+
if (!exactRecord(entry, ['within', 'symbols'])) {
|
|
218
|
+
return reject('unexpected_or_missing_keys', entryAt);
|
|
219
|
+
}
|
|
220
|
+
if (!exactRecord(entry.within, ['kind', 'target'])
|
|
221
|
+
|| !['symbol', 'path'].includes(entry.within.kind)) {
|
|
222
|
+
return reject('invalid_concern_anchor_resource', `${entryAt}/within`);
|
|
223
|
+
}
|
|
224
|
+
const targetValid = entry.within.kind === 'path'
|
|
225
|
+
? repoRelativeResourceTarget(entry.within.target)
|
|
226
|
+
: boundedText(entry.within.target, 1_024);
|
|
227
|
+
if (!targetValid) return reject('invalid_concern_anchor_resource', `${entryAt}/within`);
|
|
228
|
+
if (!ownedKeys.has(`${entry.within.kind}\0${entry.within.target}`)) {
|
|
229
|
+
return reject('concern_anchor_resource_not_owned', `${entryAt}/within`);
|
|
230
|
+
}
|
|
231
|
+
if (!Array.isArray(entry.symbols) || entry.symbols.length < 1
|
|
232
|
+
|| entry.symbols.length > TODO_CONCERN_ANCHOR_LIMIT) {
|
|
233
|
+
return reject('bounded_collection_violation', `${entryAt}/symbols`);
|
|
234
|
+
}
|
|
235
|
+
if (!entry.symbols.every((symbol) => boundedText(symbol, 1_024))) {
|
|
236
|
+
return reject('invalid_concern_anchor_symbol', `${entryAt}/symbols`);
|
|
237
|
+
}
|
|
238
|
+
if (!strictlySorted(entry.symbols)) {
|
|
239
|
+
return reject('unsorted_or_duplicate_collection', `${entryAt}/symbols`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!strictlySorted(anchors, (entry) => `${entry.within.kind}\0${entry.within.target}`)) {
|
|
243
|
+
return reject('unsorted_or_duplicate_collection', at);
|
|
244
|
+
}
|
|
245
|
+
return { valid: true };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* `lattice.todo_witness_set.v2`(およびconcern anchorを持たない旧v1)を検証し、
|
|
250
|
+
* 拒否理由とpathを返す。
|
|
251
|
+
*
|
|
252
|
+
* 自分のshapeだけをここで見て、witness本体はprobe requestへ合成して
|
|
253
|
+
* `explainRunRequest`へ委譲する。probeのbase_shaは検証専用の定数であり永続化しない。
|
|
254
|
+
* `concern_anchors`はprobeから落ちるので、ここだけが判定正本になる。
|
|
255
|
+
*/
|
|
256
|
+
export function explainTodoWitnessSet(value) {
|
|
257
|
+
const reject = (reason, at = '') => ({ valid: false, reason, path: at });
|
|
258
|
+
try {
|
|
259
|
+
if (!exactRecord(value, [
|
|
260
|
+
'schema', 'project_id', 'plan_key', 'capacity', 'sensor_query_set',
|
|
261
|
+
'manual_witness', 'witness_set_digest',
|
|
262
|
+
])) return reject('unexpected_or_missing_top_level_keys');
|
|
263
|
+
if (!TODO_WITNESS_SET_SCHEMAS.includes(value.schema)) return reject('schema_mismatch', '/schema');
|
|
264
|
+
if (!isTodoIdentifier(value.project_id)) return reject('invalid_identifier', '/project_id');
|
|
265
|
+
if (!isTodoIdentifier(value.plan_key)) return reject('invalid_identifier', '/plan_key');
|
|
266
|
+
if (!plain(value.manual_witness)) return reject('not_an_object', '/manual_witness');
|
|
267
|
+
const taskIds = Object.keys(value.manual_witness);
|
|
268
|
+
if (taskIds.length < 1 || taskIds.length > TODO_INDEPENDENCE_TASK_LIMIT) {
|
|
269
|
+
return reject('bounded_collection_violation', '/manual_witness');
|
|
270
|
+
}
|
|
271
|
+
if (!taskIds.every(isTodoIdentifier)) return reject('invalid_identifier', '/manual_witness');
|
|
272
|
+
if (value.witness_set_digest !== todoSelfDigest(value, 'witness_set_digest')) {
|
|
273
|
+
return reject('witness_set_digest_mismatch', '/witness_set_digest');
|
|
274
|
+
}
|
|
275
|
+
for (const taskId of taskIds) {
|
|
276
|
+
const witness = value.manual_witness[taskId];
|
|
277
|
+
if (plain(witness) && Object.hasOwn(witness, 'lines')
|
|
278
|
+
&& value.schema !== TODO_WITNESS_SET_SCHEMA) {
|
|
279
|
+
return reject('lines_require_witness_set_v5', `/manual_witness/${taskId}/lines`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const probe = synthesizeWitnessRunRequest(value, {
|
|
283
|
+
baseSha: PROBE_BASE_SHA, requestId: 'witness-set-probe',
|
|
284
|
+
});
|
|
285
|
+
const explained = explainRunRequest(probe);
|
|
286
|
+
if (!explained.valid) return reject(explained.reason, explained.path);
|
|
287
|
+
// 版ごとの欄。probeはRUN_REQUEST_SCHEMAで合成するので創作宣言はそこを通ってしまう。
|
|
288
|
+
// 旧版の宣言に新しい欄を書けないことは、ここだけが見る。
|
|
289
|
+
for (const taskId of taskIds) {
|
|
290
|
+
const witness = value.manual_witness[taskId];
|
|
291
|
+
if (!CREATES_SCHEMAS.includes(value.schema)
|
|
292
|
+
&& witness.owns.some((own) => Object.hasOwn(own, 'creates'))) {
|
|
293
|
+
return reject('creates_require_witness_set_v3', `/manual_witness/${taskId}/owns`);
|
|
294
|
+
}
|
|
295
|
+
const at = `/manual_witness/${taskId}/concern_anchors`;
|
|
296
|
+
if (!Object.hasOwn(witness, 'concern_anchors')) continue;
|
|
297
|
+
if (!CONCERN_ANCHOR_SCHEMAS.includes(value.schema)) {
|
|
298
|
+
return reject('concern_anchors_require_witness_set_v2', at);
|
|
299
|
+
}
|
|
300
|
+
const anchors = explainConcernAnchors(witness.concern_anchors, witness.owns, at);
|
|
301
|
+
if (!anchors.valid) return reject(anchors.reason, anchors.path);
|
|
302
|
+
}
|
|
303
|
+
return { valid: true };
|
|
304
|
+
} catch {
|
|
305
|
+
return reject('non_canonical_witness_set_bytes');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function validateTodoWitnessSet(value) {
|
|
310
|
+
return explainTodoWitnessSet(value).valid;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function conflictEntry(value) {
|
|
314
|
+
return exactRecord(value, ['task_ids', 'resource_id'])
|
|
315
|
+
&& Array.isArray(value.task_ids) && value.task_ids.length === 2
|
|
316
|
+
&& value.task_ids.every(isTodoIdentifier)
|
|
317
|
+
&& compareText(value.task_ids[0], value.task_ids[1]) < 0
|
|
318
|
+
&& boundedText(value.resource_id);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function repoRelativeResourceTarget(value) {
|
|
322
|
+
if (!boundedText(value) || value.startsWith('/') || value.includes('\\')
|
|
323
|
+
|| /^[A-Za-z]:/u.test(value)) return false;
|
|
324
|
+
const body = value.endsWith('/') ? value.slice(0, -1) : value;
|
|
325
|
+
return body.length > 0 && body.split('/')
|
|
326
|
+
.every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function conflictResourceEntry(value) {
|
|
330
|
+
if (!exactRecord(value, ['resource_id', 'kind', 'target'])
|
|
331
|
+
|| !boundedText(value.resource_id)
|
|
332
|
+
|| !TODO_INDEPENDENCE_CONFLICT_KINDS.includes(value.kind)) return false;
|
|
333
|
+
return value.kind === 'path'
|
|
334
|
+
? repoRelativeResourceTarget(value.target)
|
|
335
|
+
: boundedText(value.target);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function isTodoIndependenceLegacyMarker(value) {
|
|
339
|
+
return exactRecord(value, [
|
|
340
|
+
'schema', 'legacy_schema', 'project_id', 'plan_key', 'plan_version',
|
|
341
|
+
'topology_digest', 'base_sha',
|
|
342
|
+
])
|
|
343
|
+
&& value.schema === TODO_INDEPENDENCE_LEGACY_MARKER_SCHEMA
|
|
344
|
+
&& TODO_INDEPENDENCE_LEGACY_SCHEMAS.includes(value.legacy_schema)
|
|
345
|
+
&& isTodoIdentifier(value.project_id) && isTodoIdentifier(value.plan_key)
|
|
346
|
+
&& value.plan_version === null && value.topology_digest === null && value.base_sha === null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* task別の宣言境界。鮮度判定をartifactとgit diffだけで閉じるために持つ(ADR 0128 Decision 4)。
|
|
351
|
+
* witness setを読み直さないので、参照コストは定数のまま保たれる。
|
|
352
|
+
*/
|
|
353
|
+
function taskBoundaryEntry(value) {
|
|
354
|
+
return exactRecord(value, ['task_id', 'paths'])
|
|
355
|
+
&& isTodoIdentifier(value.task_id)
|
|
356
|
+
&& Array.isArray(value.paths) && value.paths.length <= TODO_INDEPENDENCE_LIST_LIMIT
|
|
357
|
+
&& value.paths.every((path) => boundedText(path)) && strictlySorted(value.paths);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function precedenceEntry(value) {
|
|
361
|
+
return exactRecord(value, ['from_task_id', 'to_task_id', 'reason'])
|
|
362
|
+
&& isTodoIdentifier(value.from_task_id) && isTodoIdentifier(value.to_task_id)
|
|
363
|
+
&& value.from_task_id !== value.to_task_id && boundedText(value.reason);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function unknownEntry(value) {
|
|
367
|
+
return exactRecord(value, ['task_id', 'kind', 'ref'])
|
|
368
|
+
&& isTodoIdentifier(value.task_id) && isTodoIdentifier(value.kind) && boundedText(value.ref);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function waveEntry(value) {
|
|
372
|
+
return exactRecord(value, ['task_ids'])
|
|
373
|
+
&& Array.isArray(value.task_ids) && value.task_ids.length >= 1
|
|
374
|
+
&& value.task_ids.length <= TODO_INDEPENDENCE_TASK_LIMIT
|
|
375
|
+
&& value.task_ids.every(isTodoIdentifier) && strictlySorted(value.task_ids);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function wavePlan(value, taskIds) {
|
|
379
|
+
if (value === null) return true;
|
|
380
|
+
if (!exactRecord(value, ['waves', 'minimum_feasible_waves'])
|
|
381
|
+
|| !boundedList(value.waves, waveEntry, TODO_INDEPENDENCE_TASK_LIMIT)
|
|
382
|
+
|| !isNonNegativeSafeInteger(value.minimum_feasible_waves)
|
|
383
|
+
|| value.waves.length !== value.minimum_feasible_waves) return false;
|
|
384
|
+
const scheduled = value.waves.flatMap((wave) => wave.task_ids).sort(compareText);
|
|
385
|
+
return scheduled.length === taskIds.length
|
|
386
|
+
&& scheduled.every((taskId, index) => taskId === taskIds[index]);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* `lattice.todo_independence.v3`。
|
|
391
|
+
*
|
|
392
|
+
* conflict/precedence/unknownはnormalized boundary graphから採る(ADR 0127 Decision 4)。
|
|
393
|
+
* conflictは`conflict_resources`のresource idを参照し、kindとnormalized targetを一度だけ保持する。
|
|
394
|
+
* `task_boundaries`はtask別の宣言境界で、鮮度のdiff交差判定をartifactだけで閉じるために持つ。
|
|
395
|
+
* `wave_plan`はschedulability compileが`compiled`を返した時だけ持ち、unknownが残る間はnullになる。
|
|
396
|
+
* verdictに現れないペアをverified独立と読めるのは、両taskにunknownが無いときだけである。
|
|
397
|
+
*/
|
|
398
|
+
export function validateTodoIndependence(value) {
|
|
399
|
+
try {
|
|
400
|
+
if (!exactRecord(value, [
|
|
401
|
+
'schema', 'project_id', 'plan_key', 'plan_version', 'topology_digest', 'base_sha',
|
|
402
|
+
'witness_set_digest', 'compiled_at', 'task_ids', 'task_boundaries', 'conflict_resources', 'conflicts',
|
|
403
|
+
'precedences', 'unknowns', 'wave_plan', 'scope_expanded', 'outcome', 'result_digest',
|
|
404
|
+
])) return false;
|
|
405
|
+
if (value.schema !== TODO_INDEPENDENCE_SCHEMA) return false;
|
|
406
|
+
if (!isTodoIdentifier(value.project_id) || !isTodoIdentifier(value.plan_key)
|
|
407
|
+
|| !isTodoIdentifier(value.plan_version)) return false;
|
|
408
|
+
if (!isTodoDigest(value.topology_digest) || !isGitSha(value.base_sha)
|
|
409
|
+
|| !isTodoDigest(value.witness_set_digest)
|
|
410
|
+
|| !isStrictTodoTimestamp(value.compiled_at)) return false;
|
|
411
|
+
if (!Array.isArray(value.task_ids) || value.task_ids.length < 1
|
|
412
|
+
|| value.task_ids.length > TODO_INDEPENDENCE_TASK_LIMIT
|
|
413
|
+
|| !value.task_ids.every(isTodoIdentifier) || !strictlySorted(value.task_ids)) return false;
|
|
414
|
+
const known = new Set(value.task_ids);
|
|
415
|
+
// 宣言境界はcompile対象taskとちょうど一対一で対応する。欠けたtaskがあると、
|
|
416
|
+
// そのtaskだけ交差判定ができないのに全体はverifiedを名乗れてしまう。
|
|
417
|
+
if (!boundedList(value.task_boundaries, taskBoundaryEntry, TODO_INDEPENDENCE_TASK_LIMIT)
|
|
418
|
+
|| value.task_boundaries.length !== value.task_ids.length
|
|
419
|
+
|| !strictlySorted(value.task_boundaries, (entry) => entry.task_id)
|
|
420
|
+
|| !value.task_boundaries.every((entry, index) => entry.task_id === value.task_ids[index])) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
// scope_expandedはtask_idsとちょうど一対一。欠けたtaskがあると「膨張していない」と
|
|
424
|
+
// 「まだ見ていない」が同じ顔になる。
|
|
425
|
+
if (!boundedList(value.scope_expanded, scopeExpansionEntry, TODO_INDEPENDENCE_TASK_LIMIT)
|
|
426
|
+
|| value.scope_expanded.length !== value.task_ids.length
|
|
427
|
+
|| !strictlySorted(value.scope_expanded, (entry) => entry.task_id)
|
|
428
|
+
|| !value.scope_expanded.every((entry, index) => entry.task_id === value.task_ids[index])) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
if (!boundedList(value.conflict_resources, conflictResourceEntry)
|
|
432
|
+
|| !strictlySorted(value.conflict_resources, (entry) => entry.resource_id)) return false;
|
|
433
|
+
const conflictResourceIds = new Set(value.conflict_resources.map(({ resource_id: id }) => id));
|
|
434
|
+
if (!boundedList(value.conflicts, conflictEntry)
|
|
435
|
+
|| !value.conflicts.every((entry) => entry.task_ids.every((taskId) => known.has(taskId)))
|
|
436
|
+
|| !value.conflicts.every((entry) => conflictResourceIds.has(entry.resource_id))
|
|
437
|
+
|| !strictlySorted(value.conflicts, (entry) => (
|
|
438
|
+
`${entry.task_ids[0]}\0${entry.task_ids[1]}\0${entry.resource_id}`))) return false;
|
|
439
|
+
const referencedResourceIds = new Set(value.conflicts.map(({ resource_id: id }) => id));
|
|
440
|
+
if (referencedResourceIds.size !== value.conflict_resources.length) return false;
|
|
441
|
+
if (!boundedList(value.precedences, precedenceEntry)
|
|
442
|
+
|| !value.precedences.every((entry) => known.has(entry.from_task_id) && known.has(entry.to_task_id))
|
|
443
|
+
|| !strictlySorted(value.precedences, (entry) => (
|
|
444
|
+
`${entry.from_task_id}\0${entry.to_task_id}\0${entry.reason}`))) return false;
|
|
445
|
+
if (!boundedList(value.unknowns, unknownEntry)
|
|
446
|
+
|| !value.unknowns.every((entry) => known.has(entry.task_id))
|
|
447
|
+
|| !sorted(value.unknowns, (entry) => (
|
|
448
|
+
`${entry.task_id}\0${entry.kind}\0${entry.ref}`))) return false;
|
|
449
|
+
if (!['compiled', 'unknown'].includes(value.outcome)) return false;
|
|
450
|
+
// unknownが残る限りwave planは主張しない。compiledならunknownは空でなければならない。
|
|
451
|
+
if (value.outcome === 'compiled' && value.unknowns.length > 0) return false;
|
|
452
|
+
if (value.outcome === 'unknown' && value.wave_plan !== null) return false;
|
|
453
|
+
if (!wavePlan(value.wave_plan, value.task_ids)) return false;
|
|
454
|
+
return isTodoDigest(value.result_digest)
|
|
455
|
+
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
456
|
+
} catch {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function groupEntry(value) {
|
|
462
|
+
return exactRecord(value, ['task_ids'])
|
|
463
|
+
&& Array.isArray(value.task_ids) && value.task_ids.length >= 1
|
|
464
|
+
&& value.task_ids.length <= TODO_INDEPENDENCE_TASK_LIMIT
|
|
465
|
+
&& value.task_ids.every(isTodoIdentifier) && strictlySorted(value.task_ids);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function pairKindAndSeverability(value) {
|
|
469
|
+
// conflictはkindを持ちseverabilityがそこから導かれる。precedenceは順序制約であって
|
|
470
|
+
// resource起因ではないためkindを持たず、常にserialになる。
|
|
471
|
+
if (value.type === 'conflict') {
|
|
472
|
+
return TODO_INDEPENDENCE_CONFLICT_KINDS.includes(value.kind)
|
|
473
|
+
&& value.severability === severabilityOfConflictKind(value.kind);
|
|
474
|
+
}
|
|
475
|
+
return value.kind === null && value.severability === 'serial';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function serializeEntry(value) {
|
|
479
|
+
return exactRecord(value, ['task_ids', 'type', 'detail', 'kind', 'severability'])
|
|
480
|
+
&& Array.isArray(value.task_ids) && value.task_ids.length === 2
|
|
481
|
+
&& value.task_ids.every(isTodoIdentifier)
|
|
482
|
+
&& compareText(value.task_ids[0], value.task_ids[1]) < 0
|
|
483
|
+
&& ['conflict', 'precedence'].includes(value.type) && boundedText(value.detail)
|
|
484
|
+
&& pairKindAndSeverability(value);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 着手候補と進行中ToDoの競合。
|
|
489
|
+
*
|
|
490
|
+
* v1は両端がready集合のペアだけを採っており、片端がactiveのペアを黙って捨てていた。
|
|
491
|
+
* 着手する瞬間に最も危ないのはこの組み合わせなので、独立した面として持つ(ADR 0128 Decision 3)。
|
|
492
|
+
*/
|
|
493
|
+
function activeConflictEntry(value) {
|
|
494
|
+
return exactRecord(value, [
|
|
495
|
+
'ready_task_id', 'active_task_id', 'type', 'detail', 'kind', 'severability',
|
|
496
|
+
]) && isTodoIdentifier(value.ready_task_id) && isTodoIdentifier(value.active_task_id)
|
|
497
|
+
&& value.ready_task_id !== value.active_task_id
|
|
498
|
+
&& ['conflict', 'precedence'].includes(value.type) && boundedText(value.detail)
|
|
499
|
+
&& pairKindAndSeverability(value);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 案内。単一正本のcatalogが返した形をそのまま載せる(ADR 0130 Decision 1・2)。
|
|
504
|
+
* 面ごとに文言を組み立て直さないため、ここではshapeだけを検査する。
|
|
505
|
+
*/
|
|
506
|
+
function guidanceEntry(value) {
|
|
507
|
+
return exactRecord(value, ['code', 'message', 'next_action'])
|
|
508
|
+
&& TODO_INDEPENDENCE_GUIDANCE_CODES.includes(value.code)
|
|
509
|
+
&& boundedText(value.message) && isTodoIdentifier(value.next_action);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* 鮮度の内訳。`coverage`がsha水準の事実を述べるのに対し、こちらは
|
|
514
|
+
* 「そのdiffが宣言境界に触れたか」というtask単位の事実を述べる(ADR 0128 Decision 4)。
|
|
515
|
+
*/
|
|
516
|
+
function driftEntry(value) {
|
|
517
|
+
if (value === null) return true;
|
|
518
|
+
return exactRecord(value, ['base_reachable', 'changed_path_count', 'intersecting_task_ids'])
|
|
519
|
+
&& typeof value.base_reachable === 'boolean'
|
|
520
|
+
&& isNonNegativeSafeInteger(value.changed_path_count)
|
|
521
|
+
&& Array.isArray(value.intersecting_task_ids)
|
|
522
|
+
&& value.intersecting_task_ids.length <= TODO_INDEPENDENCE_TASK_LIMIT
|
|
523
|
+
&& value.intersecting_task_ids.every(isTodoIdentifier)
|
|
524
|
+
&& strictlySorted(value.intersecting_task_ids);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function unknownTaskEntry(value) {
|
|
528
|
+
return exactRecord(value, ['task_id', 'unknowns'])
|
|
529
|
+
&& isTodoIdentifier(value.task_id)
|
|
530
|
+
&& Array.isArray(value.unknowns) && value.unknowns.length >= 1
|
|
531
|
+
&& value.unknowns.length <= TODO_INDEPENDENCE_LIST_LIMIT
|
|
532
|
+
&& value.unknowns.every((entry) => exactRecord(entry, ['kind', 'ref'])
|
|
533
|
+
&& isTodoIdentifier(entry.kind) && boundedText(entry.ref));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* `lattice.todo_independence_projection.v2`。
|
|
538
|
+
*
|
|
539
|
+
* ready frontierを「検証済み並列グループ」「直列化すべき組」「未検査」へ分けた読み出し面。
|
|
540
|
+
* v2は進行中ToDoとの競合(`conflicts_with_active`)と鮮度の内訳(`drift`)を加える。
|
|
541
|
+
* `todo_status_result`と`dispatch_frontier`は変更せず、加算の別面として持つ(ADR 0124の規律)。
|
|
542
|
+
*/
|
|
543
|
+
export function validateTodoIndependenceProjection(value) {
|
|
544
|
+
try {
|
|
545
|
+
if (!exactRecord(value, [
|
|
546
|
+
'schema', 'project_id', 'plan_key', 'coverage', 'compiled_base_sha', 'current_base_sha',
|
|
547
|
+
'plan_version', 'topology_digest', 'active_task_ids', 'uncovered_active_task_ids',
|
|
548
|
+
'drift', 'guidance', 'frontier', 'result_digest',
|
|
549
|
+
])) return false;
|
|
550
|
+
if (value.schema !== TODO_INDEPENDENCE_PROJECTION_SCHEMA) return false;
|
|
551
|
+
if (!isTodoIdentifier(value.project_id)) return false;
|
|
552
|
+
if (value.plan_key !== null && !isTodoIdentifier(value.plan_key)) return false;
|
|
553
|
+
if (!TODO_INDEPENDENCE_COVERAGE.includes(value.coverage)) return false;
|
|
554
|
+
if (value.compiled_base_sha !== null && !isGitSha(value.compiled_base_sha)) return false;
|
|
555
|
+
if (!isGitSha(value.current_base_sha)) return false;
|
|
556
|
+
if (value.plan_version !== null && !isTodoIdentifier(value.plan_version)) return false;
|
|
557
|
+
if (value.topology_digest !== null && !isTodoDigest(value.topology_digest)) return false;
|
|
558
|
+
// 記録が無い状態でcompile済みidentityを名乗らない。
|
|
559
|
+
if (value.coverage === 'missing'
|
|
560
|
+
&& (value.compiled_base_sha !== null || value.topology_digest !== null)) return false;
|
|
561
|
+
if (['verified', 'stale'].includes(value.coverage) && value.compiled_base_sha === null) return false;
|
|
562
|
+
if (value.coverage === 'superseded' && value.compiled_base_sha === null
|
|
563
|
+
&& (value.plan_version !== null || value.topology_digest !== null)) return false;
|
|
564
|
+
for (const key of ['active_task_ids', 'uncovered_active_task_ids']) {
|
|
565
|
+
if (!Array.isArray(value[key]) || value[key].length > TODO_INDEPENDENCE_TASK_LIMIT
|
|
566
|
+
|| !value[key].every(isTodoIdentifier) || !strictlySorted(value[key])) return false;
|
|
567
|
+
}
|
|
568
|
+
// 宣言のないactiveは考慮済みactiveの部分集合でなければならない。
|
|
569
|
+
const activeSet = new Set(value.active_task_ids);
|
|
570
|
+
if (!value.uncovered_active_task_ids.every((taskId) => activeSet.has(taskId))) return false;
|
|
571
|
+
if (!driftEntry(value.drift)) return false;
|
|
572
|
+
if (!guidanceEntry(value.guidance)) return false;
|
|
573
|
+
// driftはstale時の内訳。それ以外で語ると鮮度の事実を二重に主張することになる。
|
|
574
|
+
if (value.coverage !== 'stale' && value.drift !== null) return false;
|
|
575
|
+
if (!exactRecord(value.frontier, [
|
|
576
|
+
'parallel_groups', 'serialize_pairs', 'conflicts_with_active', 'unknown',
|
|
577
|
+
])) return false;
|
|
578
|
+
if (!boundedList(value.frontier.parallel_groups, groupEntry, TODO_INDEPENDENCE_TASK_LIMIT)
|
|
579
|
+
|| !strictlySorted(value.frontier.parallel_groups, (entry) => entry.task_ids[0])) return false;
|
|
580
|
+
if (!boundedList(value.frontier.serialize_pairs, serializeEntry)
|
|
581
|
+
|| !strictlySorted(value.frontier.serialize_pairs, (entry) => (
|
|
582
|
+
`${entry.task_ids[0]}\0${entry.task_ids[1]}\0${entry.type}\0${entry.detail}`))) return false;
|
|
583
|
+
if (!boundedList(value.frontier.conflicts_with_active, activeConflictEntry)
|
|
584
|
+
|| !strictlySorted(value.frontier.conflicts_with_active, (entry) => (
|
|
585
|
+
`${entry.ready_task_id}\0${entry.active_task_id}\0${entry.type}\0${entry.detail}`))) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
if (!boundedList(value.frontier.unknown, unknownTaskEntry, TODO_INDEPENDENCE_TASK_LIMIT)
|
|
589
|
+
|| !strictlySorted(value.frontier.unknown, (entry) => entry.task_id)) return false;
|
|
590
|
+
return isTodoDigest(value.result_digest)
|
|
591
|
+
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
592
|
+
} catch {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
}
|