@quolu/lattice 0.12.19 → 0.12.21
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 +4 -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-gantt-html.mjs +36 -11
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.21",
|
|
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>
|
|
@@ -97,9 +99,9 @@ registerはLATTICE_BRIDGE_REGISTRAR_SSH_HOSTとLATTICE_BRIDGE_REGISTRAR_SCRIPT
|
|
|
97
99
|
|
|
98
100
|
const SUBCOMMAND_USAGE = Object.freeze({
|
|
99
101
|
'plan create': 'plan create --input <file> | --schema --json | --schema-version <2|3> --json',
|
|
100
|
-
'plan compile': 'plan compile --request <request.json>',
|
|
102
|
+
'plan compile': 'plan compile --request <request.json> | --schema --json',
|
|
101
103
|
'plan verify': 'plan verify --request <request.json> --plan <plan.json>',
|
|
102
|
-
'run start': 'run start --request <request.json> --executor <adapter>',
|
|
104
|
+
'run start': 'run start --request <request.json> --executor <adapter> | --schema --json',
|
|
103
105
|
'run observe': 'run observe --run .lattice/runs/<id>',
|
|
104
106
|
'run status': 'run status --run .lattice/runs/<id>',
|
|
105
107
|
'run resume': 'run resume --run .lattice/runs/<id>',
|
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-gantt-html.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { serializeJsonForScript } from './todo-markdown-renderer.mjs';
|
|
4
4
|
import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
|
|
5
5
|
|
|
6
|
-
export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.
|
|
6
|
+
export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v16';
|
|
7
7
|
export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
8
8
|
export const TODO_GANTT_HTML_MAX_BYTES = 24 * 1024 * 1024;
|
|
9
9
|
|
|
@@ -43,6 +43,10 @@ function refKey(ref) {
|
|
|
43
43
|
return JSON.stringify([ref.project_id, ref.plan_key, ref.task_id]);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function compareText(left, right) {
|
|
47
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
function digest(value) {
|
|
47
51
|
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
48
52
|
}
|
|
@@ -108,16 +112,37 @@ function renderTaskIndexEntry(section, lookup) {
|
|
|
108
112
|
return `<li><button type="button" data-select-node-key="${escapeHtmlAttribute(selectKey)}"><span class="task-index-status status-${escapeHtmlAttribute(section.state.status)}" role="img" aria-label="${escapeHtmlAttribute(status.label)}">${escapeHtmlText(status.mark)}</span><span class="task-index-reference">${escapeHtmlText(taskReference(section, lookup))}</span><strong>${escapeHtmlText(section.task.title)}</strong>${blockedReason}</button></li>`;
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
/** plan_key -> 最終活動時刻。journalの末尾eventが、そのplanが最後に動いた時点。 */
|
|
116
|
+
function planActivity(readModel) {
|
|
117
|
+
return new Map((readModel?.members ?? []).map((member) => {
|
|
118
|
+
const events = member.journal?.events ?? [];
|
|
119
|
+
const last = events.at(-1)?.recorded_at ?? events[0]?.recorded_at ?? '';
|
|
120
|
+
return [member.plan.plan_key, last];
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderTaskIndex(sections, lookup, folds = new Set(), activity = new Map()) {
|
|
125
|
+
const byPlan = new Map();
|
|
113
126
|
for (const section of sections) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
plan = { planKey: section.ref.plan_key, tasks: [] };
|
|
117
|
-
plans.push(plan);
|
|
118
|
-
}
|
|
119
|
-
plan.tasks.push(section);
|
|
127
|
+
if (!byPlan.has(section.ref.plan_key)) byPlan.set(section.ref.plan_key, []);
|
|
128
|
+
byPlan.get(section.ref.plan_key).push(section);
|
|
120
129
|
}
|
|
130
|
+
// 全ToDoが図から外れたplanは終わった仕事。読む側が先に見たいのは動いているplanなので、
|
|
131
|
+
// 動いているものを最終活動の新しい順で上へ、終わったものを古い順で下へまとめる。
|
|
132
|
+
// plan内のToDo順は登録順のまま触らない。
|
|
133
|
+
const plans = [...byPlan.entries()].map(([planKey, tasks]) => ({
|
|
134
|
+
planKey,
|
|
135
|
+
tasks,
|
|
136
|
+
settled: tasks.every((section) => folds.has(refKey(section.ref))),
|
|
137
|
+
lastActivity: activity.get(planKey) ?? '',
|
|
138
|
+
}));
|
|
139
|
+
plans.sort((left, right) => {
|
|
140
|
+
if (left.settled !== right.settled) return left.settled ? 1 : -1;
|
|
141
|
+
const order = left.settled
|
|
142
|
+
? compareText(left.lastActivity, right.lastActivity)
|
|
143
|
+
: compareText(right.lastActivity, left.lastActivity);
|
|
144
|
+
return order !== 0 ? order : compareText(left.planKey, right.planKey);
|
|
145
|
+
});
|
|
121
146
|
return plans.map((plan) => {
|
|
122
147
|
const drawn = plan.tasks.filter((section) => !folds.has(refKey(section.ref)));
|
|
123
148
|
const folded = plan.tasks.filter((section) => folds.has(refKey(section.ref)));
|
|
@@ -247,8 +272,8 @@ function renderRightPane(sections, layout, presentation, readModel) {
|
|
|
247
272
|
: '<p class="fold-note">完走済みのため図には描いていません。図に出すには <code>lattice todo gantt --scope all</code> を実行してください。</p>';
|
|
248
273
|
return `<article class="task-detail" data-detail-key="${escapeHtmlAttribute(key)}" hidden><header><span class="detail-status status-${escapeHtmlAttribute(section.state.status)}">${escapeHtmlText(status.mark)} ${escapeHtmlText(status.label)}</span><span class="detail-reference">${escapeHtmlText(taskReference(section, lookup))}</span></header><h1>${escapeHtmlText(section.task.title)}</h1><p class="detail-category"><strong>カテゴリ:</strong> ${escapeHtmlText(category)}</p>${categoryDescription}<p><strong>正規ID:</strong> <code>${escapeHtmlText(`${section.ref.plan_key}/${section.task.task_id}`)}</code></p>${blockedReason}${readiness}${foldedNote}<section><h2>前提工程</h2>${renderRelationList(incoming.get(key), sectionByKey, lookup, '登録済みの前提工程はありません。', folds)}</section><section><h2>後続工程</h2>${renderRelationList(outgoing.get(key), sectionByKey, lookup, '登録済みの後続工程はありません。', folds)}</section><p class="anchor-status">${escapeHtmlText(anchorText)}</p><details class="task-diagnostics"><summary>開発者向け診断</summary><dl><dt>canonical ref</dt><dd><code>${escapeHtmlText(`${section.ref.project_id}/${section.ref.plan_key}/${section.task.task_id}`)}</code></dd><dt>anchor</dt><dd>${escapeHtmlText(section.anchorOutcome.anchored ? 'verified' : section.anchorOutcome.reason)}</dd></dl></details></article>`;
|
|
249
274
|
}).join('');
|
|
250
|
-
const taskIndex = renderTaskIndex(sections, lookup, folds);
|
|
251
|
-
return `<div class="right-toolbar"><button type="button" data-show-overview>概要</button><button type="button" data-show-selected hidden>選択工程へ戻る</button><button type="button" data-show-task-index>全工程一覧</button></div><div class="right-content">${overview}<div data-right-panel="details" hidden>${details}</div><section class="task-index" data-right-panel="task-index" hidden><h1>全工程</h1><p>Lattice
|
|
275
|
+
const taskIndex = renderTaskIndex(sections, lookup, folds, planActivity(readModel));
|
|
276
|
+
return `<div class="right-toolbar"><button type="button" data-show-overview>概要</button><button type="button" data-show-selected hidden>選択工程へ戻る</button><button type="button" data-show-task-index>全工程一覧</button></div><div class="right-content">${overview}<div data-right-panel="details" hidden>${details}</div><section class="task-index" data-right-panel="task-index" hidden><h1>全工程</h1><p>Latticeに登録された全工程を現在の状態とともに表示しています。planは動いているものを最終活動の新しい順で上に、完走したものを古い順で下にまとめ、plan内は登録順です。</p>${taskIndex}</section></div>`;
|
|
252
277
|
}
|
|
253
278
|
|
|
254
279
|
function renderDiagramLegend(presentation, layout = null, expandable = false) {
|