@quolu/lattice 0.12.20 → 0.12.22
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/README.md +6 -0
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -0
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -0
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -0
- package/package.json +4 -1
- package/src/cli-help.mjs +6 -2
- package/src/runtime-cli.mjs +32 -2
- package/src/runtime-contracts.mjs +134 -48
- package/src/runtime-front-end.mjs +5 -9
- package/src/todo-cli.mjs +12 -1
- package/src/todo-status.mjs +60 -0
package/README.md
CHANGED
|
@@ -95,6 +95,12 @@ LANや外部reverse proxyから閲覧するoptional bridgeは既定で無効で
|
|
|
95
95
|
凡例の件数バッジを押すと全工程を描いた図へ切り替わり、`lattice todo gantt --scope all`は最初から全件を
|
|
96
96
|
描きます。表示規約は[ADR 0066](docs/adr/0066-gantt-live-scope-drops-finished-work.md)が正です。
|
|
97
97
|
|
|
98
|
+
右ペインは概要・選択工程・全工程の3面で、いずれもToDo storeを表示します(元plan Markdown本文は
|
|
99
|
+
再表示しません。元文書へは各工程の詳細が持つ行対応から辿ります)。全工程一覧は動いているplanを
|
|
100
|
+
最終活動の新しい順で上に、全工程が図から外れた完走planを古い順で下にまとめ、plan内は登録順です。
|
|
101
|
+
決着済みPhaseと図から外した工程は既定で畳み、開けば読めます。規約は
|
|
102
|
+
[ADR 0067](docs/adr/0067-right-pane-shows-the-store-and-orders-by-activity.md)が正です。
|
|
103
|
+
|
|
98
104
|
静的工程表は`lattice todo gantt status`で`current / stale / missing`を確認でき、HTMLまたは
|
|
99
105
|
digest付きsidecarの欠落・改ざんはtyped failureになります。
|
|
100
106
|
dashboard daemonは起動時に読み込んだ版数をhealthで名乗り、installされた版と食い違えば`lattice status`の
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/kitepon-rgb/Lattice/blob/main/docs/schemas/lattice.executor_packet.v1.schema.json",
|
|
4
|
+
"title": "lattice.executor_packet.v1",
|
|
5
|
+
"description": "The single context packet issued at dispatch. It is the machine record of the context an executor received.",
|
|
6
|
+
"$comment": "Runtime validation additionally enforces canonical JSON bytes, the self-digest rule for `packet_digest`, and that `context_content_digest` equals the SHA-256 of the canonical JSON projection of exactly {todo_id, task_ref, scope, base_sha, verifier_refs, forbidden_operations} — plan attribution fields are excluded so that an epoch rebind is provably content-preserving.",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"required": [
|
|
10
|
+
"schema",
|
|
11
|
+
"packet_id",
|
|
12
|
+
"todo_id",
|
|
13
|
+
"task_ref",
|
|
14
|
+
"scope",
|
|
15
|
+
"base_sha",
|
|
16
|
+
"plan_ref",
|
|
17
|
+
"plan_epoch",
|
|
18
|
+
"verifier_refs",
|
|
19
|
+
"forbidden_operations",
|
|
20
|
+
"context_content_digest",
|
|
21
|
+
"packet_digest"
|
|
22
|
+
],
|
|
23
|
+
"properties": {
|
|
24
|
+
"schema": { "const": "lattice.executor_packet.v1" },
|
|
25
|
+
"packet_id": { "$ref": "#/$defs/identifier" },
|
|
26
|
+
"todo_id": {
|
|
27
|
+
"$ref": "#/$defs/identifier",
|
|
28
|
+
"$comment": "Carried through from `run_request.v1`. It is not qualified by any TODO store project/plan/revision identity."
|
|
29
|
+
},
|
|
30
|
+
"task_ref": { "$ref": "#/$defs/identifier" },
|
|
31
|
+
"scope": { "type": "object" },
|
|
32
|
+
"base_sha": { "$ref": "#/$defs/gitSha" },
|
|
33
|
+
"plan_ref": { "$ref": "#/$defs/identifier" },
|
|
34
|
+
"plan_epoch": { "type": "integer", "minimum": 0 },
|
|
35
|
+
"verifier_refs": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"maxItems": 256,
|
|
38
|
+
"items": { "type": "string" }
|
|
39
|
+
},
|
|
40
|
+
"forbidden_operations": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"minItems": 1,
|
|
43
|
+
"maxItems": 256,
|
|
44
|
+
"items": { "type": "string" }
|
|
45
|
+
},
|
|
46
|
+
"context_content_digest": { "$ref": "#/$defs/digest" },
|
|
47
|
+
"packet_digest": { "$ref": "#/$defs/digest" }
|
|
48
|
+
},
|
|
49
|
+
"$defs": {
|
|
50
|
+
"identifier": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"pattern": "^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$"
|
|
53
|
+
},
|
|
54
|
+
"digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
55
|
+
"gitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/kitepon-rgb/Lattice/blob/main/docs/schemas/lattice.executor_receipt.v1.schema.json",
|
|
4
|
+
"title": "lattice.executor_receipt.v1",
|
|
5
|
+
"description": "The receipt an executor returns for one dispatched packet. `packet_digest` is what attributes it to the packet it received.",
|
|
6
|
+
"$comment": "Runtime validation additionally enforces canonical JSON bytes and the self-digest rule for `receipt_digest`. Acceptance is decided by event order, not by the receipt's own claims: the binding is checked against the `executor_dispatched` / `epoch_rebound` event that recorded the handle, worktree and packet digest for that TODO.",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"required": [
|
|
10
|
+
"schema",
|
|
11
|
+
"receipt_id",
|
|
12
|
+
"executor_handle",
|
|
13
|
+
"worktree_id",
|
|
14
|
+
"base_sha",
|
|
15
|
+
"plan_epoch",
|
|
16
|
+
"packet_digest",
|
|
17
|
+
"todo_id",
|
|
18
|
+
"checkpoint_digest",
|
|
19
|
+
"observed_diff",
|
|
20
|
+
"receipt_digest"
|
|
21
|
+
],
|
|
22
|
+
"properties": {
|
|
23
|
+
"schema": { "const": "lattice.executor_receipt.v1" },
|
|
24
|
+
"receipt_id": { "$ref": "#/$defs/identifier" },
|
|
25
|
+
"executor_handle": { "$ref": "#/$defs/identifier" },
|
|
26
|
+
"worktree_id": { "$ref": "#/$defs/identifier" },
|
|
27
|
+
"base_sha": { "$ref": "#/$defs/gitSha" },
|
|
28
|
+
"plan_epoch": { "type": "integer", "minimum": 0 },
|
|
29
|
+
"packet_digest": {
|
|
30
|
+
"$ref": "#/$defs/digest",
|
|
31
|
+
"$comment": "Attribution to the received `executor_packet.v1`. This is the only link back to the dispatch."
|
|
32
|
+
},
|
|
33
|
+
"todo_id": {
|
|
34
|
+
"$ref": "#/$defs/identifier",
|
|
35
|
+
"$comment": "Carried through from `run_request.v1`. It is not qualified by any TODO store project/plan/revision identity, so a receipt alone does not prove which TODO store task it belongs to."
|
|
36
|
+
},
|
|
37
|
+
"checkpoint_digest": { "$ref": "#/$defs/digest" },
|
|
38
|
+
"observed_diff": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"maxItems": 256,
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": ["path", "change"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"path": { "$ref": "#/$defs/repoRelativePath" },
|
|
47
|
+
"change": { "enum": ["added", "modified", "deleted"] }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"receipt_digest": { "$ref": "#/$defs/digest" }
|
|
52
|
+
},
|
|
53
|
+
"$defs": {
|
|
54
|
+
"identifier": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"pattern": "^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$"
|
|
57
|
+
},
|
|
58
|
+
"digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
59
|
+
"gitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
|
|
60
|
+
"repoRelativePath": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"minLength": 1,
|
|
63
|
+
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?(?:/|$))[^\\u0000-\\u001f\\u007f\\\\]+$"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/kitepon-rgb/Lattice/blob/main/docs/schemas/lattice.run_request.v1.schema.json",
|
|
4
|
+
"title": "lattice.run_request.v1",
|
|
5
|
+
"description": "Canonical run request accepted by `lattice plan compile --request` and `lattice run start --request`.",
|
|
6
|
+
"$comment": "Runtime validation additionally enforces canonical JSON bytes and the self-digest rule: `request_digest` is the SHA-256 of the canonical JSON of the request with `request_digest` removed. Rejections are reported as `lattice.cli_error.v2` with `detail.reason` and `detail.path`.",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"required": [
|
|
10
|
+
"schema",
|
|
11
|
+
"request_id",
|
|
12
|
+
"repo",
|
|
13
|
+
"capacity",
|
|
14
|
+
"todos",
|
|
15
|
+
"manual_witness",
|
|
16
|
+
"sensor_query_set",
|
|
17
|
+
"executor_capability",
|
|
18
|
+
"claim_mode",
|
|
19
|
+
"request_digest"
|
|
20
|
+
],
|
|
21
|
+
"properties": {
|
|
22
|
+
"schema": { "const": "lattice.run_request.v1" },
|
|
23
|
+
"request_id": { "$ref": "#/$defs/identifier" },
|
|
24
|
+
"repo": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"additionalProperties": false,
|
|
27
|
+
"required": ["base_sha", "root_kind"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"base_sha": { "$ref": "#/$defs/gitSha" },
|
|
30
|
+
"root_kind": { "$ref": "#/$defs/identifier" }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"capacity": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"required": ["executors"],
|
|
37
|
+
"properties": { "executors": { "type": "integer", "minimum": 1 } }
|
|
38
|
+
},
|
|
39
|
+
"todos": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"minItems": 1,
|
|
42
|
+
"maxItems": 256,
|
|
43
|
+
"items": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"required": ["todo_id"],
|
|
47
|
+
"properties": { "todo_id": { "$ref": "#/$defs/identifier" } }
|
|
48
|
+
},
|
|
49
|
+
"$comment": "`todo_id` is chosen by the host. It is not qualified by any TODO store project/plan/revision identity."
|
|
50
|
+
},
|
|
51
|
+
"manual_witness": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"description": "Keys must be exactly the `todo_id` values listed in `todos`.",
|
|
54
|
+
"additionalProperties": { "$ref": "#/$defs/manualWitness" }
|
|
55
|
+
},
|
|
56
|
+
"sensor_query_set": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"required": ["queries"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"queries": {
|
|
62
|
+
"type": "array",
|
|
63
|
+
"maxItems": 256,
|
|
64
|
+
"items": { "$ref": "#/$defs/sensorQuery" }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"executor_capability": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"required": ["adapters"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"adapters": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"minItems": 1,
|
|
76
|
+
"maxItems": 256,
|
|
77
|
+
"uniqueItems": true,
|
|
78
|
+
"items": { "$ref": "#/$defs/identifier" }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"claim_mode": { "const": "exact_minimum" },
|
|
83
|
+
"request_digest": { "$ref": "#/$defs/digest" }
|
|
84
|
+
},
|
|
85
|
+
"$defs": {
|
|
86
|
+
"identifier": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"pattern": "^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$"
|
|
89
|
+
},
|
|
90
|
+
"digest": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
91
|
+
"gitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
|
|
92
|
+
"repoRelativePath": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"minLength": 1,
|
|
95
|
+
"description": "Repo-relative path. Absolute paths, `..` traversal, empty segments, control characters and `\\` separators are rejected.",
|
|
96
|
+
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?(?:/|$))[^\\u0000-\\u001f\\u007f\\\\]+$"
|
|
97
|
+
},
|
|
98
|
+
"repoRelativePathOrPrefix": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"minLength": 1,
|
|
101
|
+
"description": "Same as `repoRelativePath` but a trailing `/` is allowed to declare a directory write prefix.",
|
|
102
|
+
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.?(?:/|$))[^\\u0000-\\u001f\\u007f\\\\]+/?$"
|
|
103
|
+
},
|
|
104
|
+
"sensorQuery": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"additionalProperties": false,
|
|
107
|
+
"required": ["id", "operation"],
|
|
108
|
+
"properties": {
|
|
109
|
+
"id": { "$ref": "#/$defs/identifier" },
|
|
110
|
+
"operation": {
|
|
111
|
+
"enum": ["status", "query", "callers", "callees", "impact", "affected"]
|
|
112
|
+
},
|
|
113
|
+
"target": { "type": "string", "minLength": 1 }
|
|
114
|
+
},
|
|
115
|
+
"$comment": "`id` must be unique across the query set."
|
|
116
|
+
},
|
|
117
|
+
"sensorExpect": {
|
|
118
|
+
"oneOf": [
|
|
119
|
+
{
|
|
120
|
+
"type": "object",
|
|
121
|
+
"additionalProperties": false,
|
|
122
|
+
"required": ["kind", "name", "path"],
|
|
123
|
+
"properties": {
|
|
124
|
+
"kind": { "const": "symbol" },
|
|
125
|
+
"name": { "type": "string", "minLength": 1 },
|
|
126
|
+
"path": { "$ref": "#/$defs/repoRelativePath" }
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"type": "object",
|
|
131
|
+
"additionalProperties": false,
|
|
132
|
+
"required": ["kind", "path"],
|
|
133
|
+
"properties": {
|
|
134
|
+
"kind": { "enum": ["path", "affected"] },
|
|
135
|
+
"path": { "$ref": "#/$defs/repoRelativePath" }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
"manualWitness": {
|
|
141
|
+
"type": "object",
|
|
142
|
+
"additionalProperties": false,
|
|
143
|
+
"required": [
|
|
144
|
+
"owns",
|
|
145
|
+
"reads",
|
|
146
|
+
"writes",
|
|
147
|
+
"resources",
|
|
148
|
+
"state_effects",
|
|
149
|
+
"sensor_provenance",
|
|
150
|
+
"affected_tests",
|
|
151
|
+
"unknowns"
|
|
152
|
+
],
|
|
153
|
+
"properties": {
|
|
154
|
+
"owns": {
|
|
155
|
+
"type": "array",
|
|
156
|
+
"maxItems": 256,
|
|
157
|
+
"items": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"additionalProperties": false,
|
|
160
|
+
"required": ["kind", "target"],
|
|
161
|
+
"properties": {
|
|
162
|
+
"kind": { "enum": ["symbol", "path"] },
|
|
163
|
+
"target": { "type": "string", "minLength": 1, "maxLength": 1024 }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"reads": {
|
|
168
|
+
"type": "array",
|
|
169
|
+
"maxItems": 256,
|
|
170
|
+
"items": { "$ref": "#/$defs/repoRelativePath" }
|
|
171
|
+
},
|
|
172
|
+
"writes": {
|
|
173
|
+
"type": "array",
|
|
174
|
+
"maxItems": 256,
|
|
175
|
+
"items": { "$ref": "#/$defs/repoRelativePathOrPrefix" }
|
|
176
|
+
},
|
|
177
|
+
"resources": {
|
|
178
|
+
"type": "array",
|
|
179
|
+
"maxItems": 256,
|
|
180
|
+
"items": { "$ref": "#/$defs/identifier" }
|
|
181
|
+
},
|
|
182
|
+
"state_effects": {
|
|
183
|
+
"type": "array",
|
|
184
|
+
"maxItems": 256,
|
|
185
|
+
"items": {
|
|
186
|
+
"type": "object",
|
|
187
|
+
"additionalProperties": false,
|
|
188
|
+
"required": ["resource_id", "kind"],
|
|
189
|
+
"properties": {
|
|
190
|
+
"resource_id": { "$ref": "#/$defs/identifier" },
|
|
191
|
+
"kind": {
|
|
192
|
+
"enum": ["state", "schema", "invariant", "effect", "external_effect"]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"sensor_provenance": {
|
|
198
|
+
"type": "object",
|
|
199
|
+
"additionalProperties": false,
|
|
200
|
+
"required": ["queries"],
|
|
201
|
+
"properties": {
|
|
202
|
+
"queries": {
|
|
203
|
+
"type": "array",
|
|
204
|
+
"maxItems": 256,
|
|
205
|
+
"items": {
|
|
206
|
+
"type": "object",
|
|
207
|
+
"additionalProperties": false,
|
|
208
|
+
"required": ["query_id", "expect"],
|
|
209
|
+
"properties": {
|
|
210
|
+
"query_id": { "$ref": "#/$defs/identifier" },
|
|
211
|
+
"expect": { "$ref": "#/$defs/sensorExpect" }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
"affected_tests": {
|
|
218
|
+
"type": "array",
|
|
219
|
+
"maxItems": 256,
|
|
220
|
+
"items": { "$ref": "#/$defs/repoRelativePath" }
|
|
221
|
+
},
|
|
222
|
+
"unknowns": {
|
|
223
|
+
"type": "array",
|
|
224
|
+
"maxItems": 256,
|
|
225
|
+
"items": {
|
|
226
|
+
"type": "object",
|
|
227
|
+
"additionalProperties": false,
|
|
228
|
+
"required": ["kind", "ref"],
|
|
229
|
+
"properties": {
|
|
230
|
+
"kind": { "$ref": "#/$defs/identifier" },
|
|
231
|
+
"ref": { "type": "string", "minLength": 1 }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.22",
|
|
4
4
|
"description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"docs/schemas/lattice.plan_create_input.v1.schema.json",
|
|
22
22
|
"docs/schemas/lattice.plan_create_input.v2.schema.json",
|
|
23
23
|
"docs/schemas/lattice.plan_create_input.v3.schema.json",
|
|
24
|
+
"docs/schemas/lattice.run_request.v1.schema.json",
|
|
25
|
+
"docs/schemas/lattice.executor_packet.v1.schema.json",
|
|
26
|
+
"docs/schemas/lattice.executor_receipt.v1.schema.json",
|
|
24
27
|
"docs/bridge-setup.md",
|
|
25
28
|
"sensor/dist",
|
|
26
29
|
"!sensor/dist/bin/lattice-sensor.*",
|
package/src/cli-help.mjs
CHANGED
|
@@ -24,12 +24,14 @@ Commands:
|
|
|
24
24
|
create --schema --json
|
|
25
25
|
create --schema-version <2|3> --json
|
|
26
26
|
compile --request <request.json>
|
|
27
|
+
compile --schema --json # lattice.run_request.v1 の JSON Schema を出す
|
|
27
28
|
verify --request <request.json> --plan <plan.json>
|
|
28
29
|
`,
|
|
29
30
|
run: `Usage: lattice run <command> [options]
|
|
30
31
|
|
|
31
32
|
Commands:
|
|
32
33
|
start --request <request.json> --executor <adapter>
|
|
34
|
+
start --schema --json # lattice.run_request.v1 の JSON Schema を出す
|
|
33
35
|
observe --run .lattice/runs/<id>
|
|
34
36
|
status --run .lattice/runs/<id>
|
|
35
37
|
resume --run .lattice/runs/<id>
|
|
@@ -43,6 +45,7 @@ Commands:
|
|
|
43
45
|
|
|
44
46
|
Read commands:
|
|
45
47
|
status [--json]
|
|
48
|
+
bindings [--plan <key>] [--json] # compile_binding付きTaskをTODO identityつきで投影する
|
|
46
49
|
verify [--plan <key>] [--json]
|
|
47
50
|
snapshot --rebuild --plan <key>
|
|
48
51
|
gantt [--out <file>] [--scope live|all] # 既定live: 完走した工程を図から除く(一覧には残る)
|
|
@@ -97,9 +100,9 @@ registerはLATTICE_BRIDGE_REGISTRAR_SSH_HOSTとLATTICE_BRIDGE_REGISTRAR_SCRIPT
|
|
|
97
100
|
|
|
98
101
|
const SUBCOMMAND_USAGE = Object.freeze({
|
|
99
102
|
'plan create': 'plan create --input <file> | --schema --json | --schema-version <2|3> --json',
|
|
100
|
-
'plan compile': 'plan compile --request <request.json>',
|
|
103
|
+
'plan compile': 'plan compile --request <request.json> | --schema --json',
|
|
101
104
|
'plan verify': 'plan verify --request <request.json> --plan <plan.json>',
|
|
102
|
-
'run start': 'run start --request <request.json> --executor <adapter>',
|
|
105
|
+
'run start': 'run start --request <request.json> --executor <adapter> | --schema --json',
|
|
103
106
|
'run observe': 'run observe --run .lattice/runs/<id>',
|
|
104
107
|
'run status': 'run status --run .lattice/runs/<id>',
|
|
105
108
|
'run resume': 'run resume --run .lattice/runs/<id>',
|
|
@@ -108,6 +111,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
|
|
|
108
111
|
'run list': 'run list --json',
|
|
109
112
|
'event verify': 'event verify --run .lattice/runs/<id>',
|
|
110
113
|
'todo status': 'todo status [--json]',
|
|
114
|
+
'todo bindings': 'todo bindings [--plan <key>] [--json]',
|
|
111
115
|
'todo verify': 'todo verify [--plan <key>] [--json]',
|
|
112
116
|
'todo snapshot': 'todo snapshot --rebuild --plan <key>',
|
|
113
117
|
'todo gantt': 'todo gantt [--out <file>] [--scope live|all] | status [--out <file>] | serve --port <port> [--scope live|all]',
|
package/src/runtime-cli.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
evidenceFromCollectedOutcomes,
|
|
25
25
|
} from './runtime-front-end.mjs';
|
|
26
26
|
import {
|
|
27
|
+
explainRunRequest,
|
|
27
28
|
validateRunRequest,
|
|
28
29
|
validateRuntimeBoundaryManifest,
|
|
29
30
|
validateRuntimePlan,
|
|
@@ -247,10 +248,31 @@ function canonicalNow() {
|
|
|
247
248
|
return new Date().toISOString();
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
/**
|
|
252
|
+
* 同梱の`lattice.run_request.v1` JSON Schemaをstdoutへ出す(ADR 0123)。
|
|
253
|
+
* hostがrequestを推測で組まずに済むよう、契約を配布物から直接取れるようにする。
|
|
254
|
+
*/
|
|
255
|
+
async function runRequestSchema({ stdout }) {
|
|
256
|
+
const schemaUrl = new URL('../docs/schemas/lattice.run_request.v1.schema.json', import.meta.url);
|
|
257
|
+
const schema = JSON.parse(await readFile(schemaUrl, 'utf8'));
|
|
258
|
+
if (schema?.title !== 'lattice.run_request.v1') {
|
|
259
|
+
throw new CliContractError('CONTRACT_VIOLATION', '同梱run_request schemaが不正');
|
|
260
|
+
}
|
|
261
|
+
stdout.write(`${JSON.stringify(schema)}\n`);
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
250
265
|
async function loadRequest(requestPath) {
|
|
251
266
|
const request = await readBoundedJson(requestPath, 'run request');
|
|
252
|
-
|
|
253
|
-
|
|
267
|
+
const verdict = explainRunRequest(request);
|
|
268
|
+
if (!verdict.valid) {
|
|
269
|
+
// 拒否理由と違反箇所を返す(ADR 0123)。TODO面と同じdiagnosabilityへ揃え、
|
|
270
|
+
// hostがschemaを推測せずにrequestを直せるようにする。
|
|
271
|
+
throw new CliContractError(
|
|
272
|
+
'INVALID_RUN_REQUEST',
|
|
273
|
+
'run_request.v1 contractを満たさない',
|
|
274
|
+
{ reason: verdict.reason, path: verdict.path },
|
|
275
|
+
);
|
|
254
276
|
}
|
|
255
277
|
return request;
|
|
256
278
|
}
|
|
@@ -2753,6 +2775,14 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
|
|
|
2753
2775
|
}
|
|
2754
2776
|
let action = null;
|
|
2755
2777
|
if (argv.length === 4
|
|
2778
|
+
&& argv[0] === 'plan' && argv[1] === 'compile'
|
|
2779
|
+
&& argv[2] === '--schema' && argv[3] === '--json') {
|
|
2780
|
+
action = () => runRequestSchema({ stdout });
|
|
2781
|
+
} else if (argv.length === 4
|
|
2782
|
+
&& argv[0] === 'run' && argv[1] === 'start'
|
|
2783
|
+
&& argv[2] === '--schema' && argv[3] === '--json') {
|
|
2784
|
+
action = () => runRequestSchema({ stdout });
|
|
2785
|
+
} else if (argv.length === 4
|
|
2756
2786
|
&& argv[0] === 'plan' && argv[1] === 'compile' && argv[2] === '--request'
|
|
2757
2787
|
&& typeof argv[3] === 'string' && argv[3].length > 0) {
|
|
2758
2788
|
action = () => planCompile({ requestPath: path.resolve(cwd, argv[3]), cwd, stdout });
|
|
@@ -164,7 +164,7 @@ function unknownEntry(value) {
|
|
|
164
164
|
&& value.ref.length > 0;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
const MANUAL_WITNESS_FIELDS = Object.freeze([
|
|
167
|
+
export const MANUAL_WITNESS_FIELDS = Object.freeze([
|
|
168
168
|
'owns',
|
|
169
169
|
'reads',
|
|
170
170
|
'writes',
|
|
@@ -175,18 +175,8 @@ const MANUAL_WITNESS_FIELDS = Object.freeze([
|
|
|
175
175
|
'unknowns',
|
|
176
176
|
]);
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
&& exactRecord(value, MANUAL_WITNESS_FIELDS)
|
|
181
|
-
&& boundedArray(value.owns, ownEntry)
|
|
182
|
-
&& repoPathArray(value.reads)
|
|
183
|
-
&& repoPathArray(value.writes, { allowPrefix: true })
|
|
184
|
-
&& boundedArray(value.resources, identifier)
|
|
185
|
-
&& boundedArray(value.state_effects, stateEffectEntry)
|
|
186
|
-
&& plainObject(value.sensor_provenance)
|
|
187
|
-
&& repoPathArray(value.affected_tests)
|
|
188
|
-
&& boundedArray(value.unknowns, unknownEntry);
|
|
189
|
-
}
|
|
178
|
+
// manual witness entryの判定は`explainRunRequest`だけが所有する。
|
|
179
|
+
// 同じ規則をbooleanと診断の二箇所へ持たないための単一正本化(ADR 0123)。
|
|
190
180
|
|
|
191
181
|
function boundedArray(value, predicate, { min = 0, max = MAX_COLLECTION } = {}) {
|
|
192
182
|
return Array.isArray(value)
|
|
@@ -231,43 +221,139 @@ const WITNESS_PROVENANCE = Object.freeze([
|
|
|
231
221
|
'manual_state_effect',
|
|
232
222
|
]);
|
|
233
223
|
|
|
224
|
+
export const RUN_REQUEST_FIELDS = Object.freeze([
|
|
225
|
+
'schema',
|
|
226
|
+
'request_id',
|
|
227
|
+
'repo',
|
|
228
|
+
'capacity',
|
|
229
|
+
'todos',
|
|
230
|
+
'manual_witness',
|
|
231
|
+
'sensor_query_set',
|
|
232
|
+
'executor_capability',
|
|
233
|
+
'claim_mode',
|
|
234
|
+
'request_digest',
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
export const RUN_REQUEST_CLAIM_MODE = 'exact_minimum';
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* `sensor_query_set.queries[].operation`のclosed set。
|
|
241
|
+
* runtime front-endはこの定数を輸入して使う(同じ閉集合を二箇所へ持たない)。
|
|
242
|
+
*/
|
|
243
|
+
export const SENSOR_QUERY_OPERATIONS = Object.freeze([
|
|
244
|
+
'status',
|
|
245
|
+
'query',
|
|
246
|
+
'callers',
|
|
247
|
+
'callees',
|
|
248
|
+
'impact',
|
|
249
|
+
'affected',
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
/** `manual_witness[].sensor_provenance.queries[].expect.kind`のclosed set。 */
|
|
253
|
+
export const SENSOR_EXPECT_KINDS = Object.freeze(['symbol', 'path', 'affected']);
|
|
254
|
+
|
|
255
|
+
/** `sensor_provenance` entryのexpectを検査する(front-end adapter契約と同一規則)。 */
|
|
256
|
+
function sensorExpect(value) {
|
|
257
|
+
if (!plainObject(value) || !SENSOR_EXPECT_KINDS.includes(value.kind)) return false;
|
|
258
|
+
if (value.kind === 'symbol') {
|
|
259
|
+
return exactRecord(value, ['kind', 'name', 'path'])
|
|
260
|
+
&& typeof value.name === 'string' && value.name.length > 0
|
|
261
|
+
&& repoRelativePath(value.path);
|
|
262
|
+
}
|
|
263
|
+
return exactRecord(value, ['kind', 'path']) && repoRelativePath(value.path);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* `lattice.run_request.v1`の唯一の判定正本(ADR 0123)。
|
|
268
|
+
* 受理は`{ valid: true }`、拒否は最初の違反の`reason`と`path`を返す。
|
|
269
|
+
* `validateRunRequest`は本関数へ委譲するため、boolean判定と診断が乖離しない。
|
|
270
|
+
*/
|
|
271
|
+
export function explainRunRequest(value) {
|
|
272
|
+
const reject = (reason, at) => ({ valid: false, reason, path: at });
|
|
273
|
+
try {
|
|
274
|
+
canonicalizeArtifact(value);
|
|
275
|
+
} catch {
|
|
276
|
+
return reject('non_canonical_request_bytes', '');
|
|
277
|
+
}
|
|
278
|
+
if (!exactRecord(value, RUN_REQUEST_FIELDS)) return reject('unexpected_or_missing_top_level_keys', '');
|
|
279
|
+
if (value.schema !== 'lattice.run_request.v1') return reject('schema_mismatch', '/schema');
|
|
280
|
+
if (!identifier(value.request_id)) return reject('invalid_identifier', '/request_id');
|
|
281
|
+
if (!exactRecord(value.repo, ['base_sha', 'root_kind'])) return reject('unexpected_or_missing_keys', '/repo');
|
|
282
|
+
if (!gitSha(value.repo.base_sha)) return reject('invalid_git_sha', '/repo/base_sha');
|
|
283
|
+
if (!identifier(value.repo.root_kind)) return reject('invalid_identifier', '/repo/root_kind');
|
|
284
|
+
if (!exactRecord(value.capacity, ['executors'])) return reject('unexpected_or_missing_keys', '/capacity');
|
|
285
|
+
if (!positiveInteger(value.capacity.executors)) return reject('not_a_positive_integer', '/capacity/executors');
|
|
286
|
+
if (!Array.isArray(value.todos) || value.todos.length < 1 || value.todos.length > MAX_COLLECTION) {
|
|
287
|
+
return reject('bounded_collection_violation', '/todos');
|
|
288
|
+
}
|
|
289
|
+
for (const [index, todo] of value.todos.entries()) {
|
|
290
|
+
if (!exactRecord(todo, ['todo_id'])) return reject('unexpected_or_missing_keys', `/todos/${index}`);
|
|
291
|
+
if (!identifier(todo.todo_id)) return reject('invalid_identifier', `/todos/${index}/todo_id`);
|
|
292
|
+
}
|
|
293
|
+
const todoIds = value.todos.map((todo) => todo.todo_id);
|
|
294
|
+
if (new Set(todoIds).size !== todoIds.length) return reject('duplicate_todo_id', '/todos');
|
|
295
|
+
if (!plainObject(value.manual_witness)) return reject('not_an_object', '/manual_witness');
|
|
296
|
+
if (!exactRecord(value.manual_witness, todoIds)) {
|
|
297
|
+
return reject('manual_witness_keys_must_equal_todo_ids', '/manual_witness');
|
|
298
|
+
}
|
|
299
|
+
for (const todoId of todoIds) {
|
|
300
|
+
const witness = value.manual_witness[todoId];
|
|
301
|
+
const at = `/manual_witness/${todoId}`;
|
|
302
|
+
if (!plainObject(witness)) return reject('not_an_object', at);
|
|
303
|
+
if (!exactRecord(witness, MANUAL_WITNESS_FIELDS)) return reject('unexpected_or_missing_keys', at);
|
|
304
|
+
if (!boundedArray(witness.owns, ownEntry)) return reject('invalid_own_entries', `${at}/owns`);
|
|
305
|
+
if (!repoPathArray(witness.reads)) return reject('invalid_repo_relative_paths', `${at}/reads`);
|
|
306
|
+
if (!repoPathArray(witness.writes, { allowPrefix: true })) return reject('invalid_repo_relative_paths', `${at}/writes`);
|
|
307
|
+
if (!boundedArray(witness.resources, identifier)) return reject('invalid_identifier', `${at}/resources`);
|
|
308
|
+
if (!boundedArray(witness.state_effects, stateEffectEntry)) return reject('invalid_state_effect_entries', `${at}/state_effects`);
|
|
309
|
+
// sensor_provenanceもfront-end adapter契約のshapeまで検査する(ADR 0123)。
|
|
310
|
+
const provenanceAt = `${at}/sensor_provenance`;
|
|
311
|
+
if (!exactRecord(witness.sensor_provenance, ['queries'])) return reject('unexpected_or_missing_keys', provenanceAt);
|
|
312
|
+
if (!Array.isArray(witness.sensor_provenance.queries)
|
|
313
|
+
|| witness.sensor_provenance.queries.length > MAX_COLLECTION) {
|
|
314
|
+
return reject('bounded_collection_violation', `${provenanceAt}/queries`);
|
|
315
|
+
}
|
|
316
|
+
for (const [index, entry] of witness.sensor_provenance.queries.entries()) {
|
|
317
|
+
const entryAt = `${provenanceAt}/queries/${index}`;
|
|
318
|
+
if (!exactRecord(entry, ['query_id', 'expect'])) return reject('unexpected_or_missing_keys', entryAt);
|
|
319
|
+
if (!identifier(entry.query_id)) return reject('invalid_identifier', `${entryAt}/query_id`);
|
|
320
|
+
if (!sensorExpect(entry.expect)) return reject('invalid_sensor_expect', `${entryAt}/expect`);
|
|
321
|
+
}
|
|
322
|
+
if (!repoPathArray(witness.affected_tests)) return reject('invalid_repo_relative_paths', `${at}/affected_tests`);
|
|
323
|
+
if (!boundedArray(witness.unknowns, unknownEntry)) return reject('invalid_unknown_entries', `${at}/unknowns`);
|
|
324
|
+
}
|
|
325
|
+
// sensor_query_set/executor_capabilityは、runtime front-endとrun startが実際に要求する
|
|
326
|
+
// shapeまで検査する。schemaで通ってから後段で落ちる契約分裂を残さない(ADR 0123)。
|
|
327
|
+
if (!exactRecord(value.sensor_query_set, ['queries'])) return reject('unexpected_or_missing_keys', '/sensor_query_set');
|
|
328
|
+
if (!Array.isArray(value.sensor_query_set.queries) || value.sensor_query_set.queries.length > MAX_COLLECTION) {
|
|
329
|
+
return reject('bounded_collection_violation', '/sensor_query_set/queries');
|
|
330
|
+
}
|
|
331
|
+
const queryIds = new Set();
|
|
332
|
+
for (const [index, query] of value.sensor_query_set.queries.entries()) {
|
|
333
|
+
const at = `/sensor_query_set/queries/${index}`;
|
|
334
|
+
const keys = plainObject(query) && Object.hasOwn(query, 'target')
|
|
335
|
+
? ['id', 'operation', 'target'] : ['id', 'operation'];
|
|
336
|
+
if (!exactRecord(query, keys)) return reject('unexpected_or_missing_keys', at);
|
|
337
|
+
if (!identifier(query.id)) return reject('invalid_identifier', `${at}/id`);
|
|
338
|
+
if (!SENSOR_QUERY_OPERATIONS.includes(query.operation)) return reject('unknown_sensor_query_operation', `${at}/operation`);
|
|
339
|
+
if (keys.includes('target') && (typeof query.target !== 'string' || query.target.length === 0)) {
|
|
340
|
+
return reject('empty_query_target', `${at}/target`);
|
|
341
|
+
}
|
|
342
|
+
if (queryIds.has(query.id)) return reject('duplicate_query_id', `${at}/id`);
|
|
343
|
+
queryIds.add(query.id);
|
|
344
|
+
}
|
|
345
|
+
if (!exactRecord(value.executor_capability, ['adapters'])) return reject('unexpected_or_missing_keys', '/executor_capability');
|
|
346
|
+
if (!uniqueIdentifierArray(value.executor_capability.adapters, { min: 1 })) {
|
|
347
|
+
return reject('invalid_adapter_identifiers', '/executor_capability/adapters');
|
|
348
|
+
}
|
|
349
|
+
if (value.claim_mode !== RUN_REQUEST_CLAIM_MODE) return reject('claim_mode_must_be_exact_minimum', '/claim_mode');
|
|
350
|
+
if (!selfDigestValid(value, 'request_digest')) return reject('request_digest_mismatch', '/request_digest');
|
|
351
|
+
return { valid: true };
|
|
352
|
+
}
|
|
353
|
+
|
|
234
354
|
/** `lattice.run_request.v1`。manual witnessはTODOごとに完備でなければならない。 */
|
|
235
355
|
export function validateRunRequest(value) {
|
|
236
|
-
return
|
|
237
|
-
if (!exactRecord(request, [
|
|
238
|
-
'schema',
|
|
239
|
-
'request_id',
|
|
240
|
-
'repo',
|
|
241
|
-
'capacity',
|
|
242
|
-
'todos',
|
|
243
|
-
'manual_witness',
|
|
244
|
-
'sensor_query_set',
|
|
245
|
-
'executor_capability',
|
|
246
|
-
'claim_mode',
|
|
247
|
-
'request_digest',
|
|
248
|
-
])
|
|
249
|
-
|| request.schema !== 'lattice.run_request.v1'
|
|
250
|
-
|| !identifier(request.request_id)
|
|
251
|
-
|| !exactRecord(request.repo, ['base_sha', 'root_kind'])
|
|
252
|
-
|| !gitSha(request.repo.base_sha)
|
|
253
|
-
|| !identifier(request.repo.root_kind)
|
|
254
|
-
|| !exactRecord(request.capacity, ['executors'])
|
|
255
|
-
|| !positiveInteger(request.capacity.executors)
|
|
256
|
-
|| !boundedArray(request.todos, (todo) => (
|
|
257
|
-
exactRecord(todo, ['todo_id']) && identifier(todo.todo_id)
|
|
258
|
-
), { min: 1 })) {
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
const todoIds = request.todos.map((todo) => todo.todo_id);
|
|
262
|
-
if (new Set(todoIds).size !== todoIds.length) return false;
|
|
263
|
-
return plainObject(request.manual_witness)
|
|
264
|
-
&& exactRecord(request.manual_witness, todoIds)
|
|
265
|
-
&& Object.values(request.manual_witness).every(manualWitnessEntry)
|
|
266
|
-
&& plainObject(request.sensor_query_set)
|
|
267
|
-
&& plainObject(request.executor_capability)
|
|
268
|
-
&& request.claim_mode === 'exact_minimum'
|
|
269
|
-
&& selfDigestValid(request, 'request_digest');
|
|
270
|
-
});
|
|
356
|
+
return explainRunRequest(value).valid;
|
|
271
357
|
}
|
|
272
358
|
|
|
273
359
|
/**
|
|
@@ -6,6 +6,8 @@ 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
|
+
SENSOR_EXPECT_KINDS,
|
|
10
|
+
SENSOR_QUERY_OPERATIONS,
|
|
9
11
|
selfDigest,
|
|
10
12
|
validateRunRequest,
|
|
11
13
|
validateRuntimeBoundaryManifest,
|
|
@@ -36,16 +38,10 @@ import {
|
|
|
36
38
|
* portable outcome projectionのcanonical digestだけを入れる(Decision 10.4)。
|
|
37
39
|
*/
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'query',
|
|
42
|
-
'callers',
|
|
43
|
-
'callees',
|
|
44
|
-
'impact',
|
|
45
|
-
'affected',
|
|
46
|
-
]);
|
|
41
|
+
// 閉集合の正本はruntime-contracts。ここでは輸入して使う。
|
|
42
|
+
const QUERY_OPERATIONS = SENSOR_QUERY_OPERATIONS;
|
|
47
43
|
const STRUCTURE_OPERATIONS = new Set(['query', 'callers', 'callees', 'impact']);
|
|
48
|
-
const EXPECT_KINDS = new Set(
|
|
44
|
+
const EXPECT_KINDS = new Set(SENSOR_EXPECT_KINDS);
|
|
49
45
|
const IDENTIFIER = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/;
|
|
50
46
|
const STATE_KIND_MAP = Object.freeze({
|
|
51
47
|
state: 'state',
|
package/src/todo-cli.mjs
CHANGED
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
appendTodoExtraction,
|
|
47
47
|
validateTodoExtraction,
|
|
48
48
|
} from './todo-migration.mjs';
|
|
49
|
-
import { projectTodoStatus } from './todo-status.mjs';
|
|
49
|
+
import { projectTodoBindings, projectTodoStatus } from './todo-status.mjs';
|
|
50
50
|
import {
|
|
51
51
|
parseTodoSourceRef, todoLegacyReconciliationDigest, validatePhaseTodoRevision,
|
|
52
52
|
validateTodoRevision, validateTodoRevisionSet,
|
|
@@ -474,6 +474,10 @@ async function status({ repoRoot }) {
|
|
|
474
474
|
return projectTodoStatus(await readTodoStore({ repoRoot }));
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
+
async function bindings({ repoRoot, requestedPlanKey }) {
|
|
478
|
+
return projectTodoBindings(await readTodoStore({ repoRoot }), { requestedPlanKey });
|
|
479
|
+
}
|
|
480
|
+
|
|
477
481
|
async function readNarrative(repoRoot, ref) {
|
|
478
482
|
const canonicalRoot = await realpath(repoRoot);
|
|
479
483
|
const source = parseTodoSourceRef(ref);
|
|
@@ -926,6 +930,13 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
926
930
|
if ((argv.length === 1 && argv[0] === 'status')
|
|
927
931
|
|| (argv.length === 2 && argv[0] === 'status' && argv[1] === '--json')) {
|
|
928
932
|
action = (repoRoot) => status({ repoRoot });
|
|
933
|
+
} else if ((argv.length === 1 && argv[0] === 'bindings')
|
|
934
|
+
|| (argv.length === 2 && argv[0] === 'bindings' && argv[1] === '--json')) {
|
|
935
|
+
action = (repoRoot) => bindings({ repoRoot, requestedPlanKey: null });
|
|
936
|
+
} else if ((argv.length === 3 || argv.length === 4) && argv[0] === 'bindings'
|
|
937
|
+
&& argv[1] === '--plan' && isTodoIdentifier(argv[2])
|
|
938
|
+
&& (argv.length === 3 || argv[3] === '--json')) {
|
|
939
|
+
action = (repoRoot) => bindings({ repoRoot, requestedPlanKey: argv[2] });
|
|
929
940
|
} else if ((argv.length === 1 && argv[0] === 'verify')
|
|
930
941
|
|| (argv.length === 2 && argv[0] === 'verify' && argv[1] === '--json')) {
|
|
931
942
|
action = (repoRoot) => verify({ repoRoot, requestedPlanKey: null });
|
package/src/todo-status.mjs
CHANGED
|
@@ -338,3 +338,63 @@ export function projectTodoStatus(readModel) {
|
|
|
338
338
|
}
|
|
339
339
|
return result;
|
|
340
340
|
}
|
|
341
|
+
|
|
342
|
+
export const TODO_BINDING_PROJECTION_SCHEMA = 'lattice.todo_binding_projection.v1';
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* `compile_binding`が設定されたTaskだけを、TODO正本のidentityつきで投影する(ADR 0124)。
|
|
346
|
+
*
|
|
347
|
+
* これはTODO工程storeとruntime実行を結ぶ唯一の公開読み取り面である。host は
|
|
348
|
+
* `compiled_plan_digest`で`runtime_plan.v1`を、`base_sha`でrun requestのbaseを照合し、
|
|
349
|
+
* plan→`executor_packet.v1`→`executor_receipt.v1`(`packet_digest`帰属)まで辿れる。
|
|
350
|
+
*
|
|
351
|
+
* `todo_status_result.v4`は変更しない。binding投影は加算の別面とし、v4を受理する
|
|
352
|
+
* 既存hostを壊さない。
|
|
353
|
+
*/
|
|
354
|
+
export function projectTodoBindings(readModel, { requestedPlanKey = null } = {}) {
|
|
355
|
+
if (!plain(readModel) || readModel.schema !== 'lattice.todo_store_read.v1'
|
|
356
|
+
|| !isTodoIdentifier(readModel.project_id) || !Array.isArray(readModel.members)) {
|
|
357
|
+
fail('TODO_STATUS_INVALID_INPUT', 'todo_status_read_model_invalid');
|
|
358
|
+
}
|
|
359
|
+
if (requestedPlanKey !== null && !isTodoIdentifier(requestedPlanKey)) {
|
|
360
|
+
fail('TODO_STATUS_INVALID_INPUT', 'todo_binding_plan_key_invalid');
|
|
361
|
+
}
|
|
362
|
+
const members = [...readModel.members].sort((left, right) => {
|
|
363
|
+
const leftKey = left?.plan?.plan_key ?? '';
|
|
364
|
+
const rightKey = right?.plan?.plan_key ?? '';
|
|
365
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
366
|
+
});
|
|
367
|
+
const bindings = [];
|
|
368
|
+
let matchedPlan = requestedPlanKey === null;
|
|
369
|
+
for (const member of members) {
|
|
370
|
+
const plan = member?.plan;
|
|
371
|
+
if (!plain(plan) || !Array.isArray(plan.tasks)) continue;
|
|
372
|
+
if (requestedPlanKey !== null && plan.plan_key !== requestedPlanKey) continue;
|
|
373
|
+
if (requestedPlanKey !== null) matchedPlan = true;
|
|
374
|
+
for (const task of plan.tasks) {
|
|
375
|
+
if (!plain(task) || task.compile_binding === null || task.compile_binding === undefined) continue;
|
|
376
|
+
bindings.push({
|
|
377
|
+
project_id: plan.project_id,
|
|
378
|
+
plan_key: plan.plan_key,
|
|
379
|
+
plan_version: plan.plan_version,
|
|
380
|
+
task_id: task.task_id,
|
|
381
|
+
compile_binding: task.compile_binding,
|
|
382
|
+
});
|
|
383
|
+
if (bindings.length > TODO_STATUS_LIST_LIMIT) {
|
|
384
|
+
fail('TODO_SCALE_EXCEEDED', 'todo_binding_projection_limit_exceeded', {
|
|
385
|
+
binding_limit: TODO_STATUS_LIST_LIMIT,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!matchedPlan) fail('TODO_STATUS_INVALID_INPUT', 'todo_binding_plan_not_found');
|
|
391
|
+
const result = {
|
|
392
|
+
schema: TODO_BINDING_PROJECTION_SCHEMA,
|
|
393
|
+
project_id: readModel.project_id,
|
|
394
|
+
plan_key: requestedPlanKey,
|
|
395
|
+
bindings,
|
|
396
|
+
result_digest: '',
|
|
397
|
+
};
|
|
398
|
+
result.result_digest = todoSelfDigest(result, 'result_digest');
|
|
399
|
+
return result;
|
|
400
|
+
}
|