@quolu/lattice 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/cli-help.mjs +3 -0
- package/src/project-cli.mjs +2 -0
- package/src/seam-proposal-contracts.mjs +427 -0
- package/src/seam-proposal-queries.mjs +484 -0
- package/src/seam-proposal.mjs +1763 -0
- package/src/todo-cli.mjs +285 -10
- package/src/todo-gantt-html.mjs +60 -1
- package/src/todo-gantt-layout.mjs +76 -0
- package/src/todo-independence-contracts.mjs +61 -8
- package/src/todo-independence-guidance.mjs +41 -1
- package/src/todo-independence.mjs +71 -10
- package/src/todo-store.mjs +103 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"test": "node scripts/run-product-tests.mjs",
|
|
61
61
|
"test:sensor": "npm --prefix sensor test",
|
|
62
62
|
"precheck": "node --check src/bridge-launch-agent.mjs",
|
|
63
|
-
"check": "node --check bin/lattice.mjs && node --check bin/lattice-mcp.mjs && node --check bin/lattice-dashboard.mjs && node --check bin/lattice-bridge.mjs && node --check bin/lattice-scripted-adapter.mjs && node --check src/cli-stdio.mjs && node --check src/bridge-address.mjs && node --check src/bridge-registrar.mjs && node --check src/todo-independence-guidance.mjs && node --check src/todo-independence.mjs && node --check src/todo-independence-contracts.mjs && node --check src/todo-gantt-layout.mjs && node --check src/todo-gantt-scope.mjs && node --check src/bridge-config.mjs && node --check src/bridge-server.mjs && node --check src/bridge-daemon.mjs && node --check src/bridge-cli.mjs && node --check src/project-cli.mjs && node --check src/sensor-cli.mjs && node --check src/sensor-runtime.mjs && node --check src/sensor-adapter.mjs && node --check src/factory-diagnostics.mjs && node --check src/runtime-errors.mjs && node --check src/runtime-contracts.mjs && node --check src/runtime-event-store.mjs && node --check src/runtime-adapter-registry.mjs && node --check src/runtime-scripted-adapter-controller.mjs && node --check src/hash-chain.mjs && node --check src/dag-chain.mjs && node --check src/todo-contracts.mjs && node --check src/todo-chain.mjs && node --check src/todo-store.mjs && node --check src/todo-migration.mjs && node --check src/todo-revision.mjs && node --check src/todo-status.mjs && node --check src/todo-cli.mjs && node --check src/todo-dashboard-registry.mjs && node --check src/todo-gantt-presentation.mjs && node --check src/todo-gantt-live.mjs && node --check src/bounded-seam.mjs && node --check src/todo-narrative-anchor.mjs && node --check src/todo-markdown-renderer.mjs && node --check src/todo-gantt-svg.mjs && node --check src/todo-gantt-html.mjs && node --check src/runtime-projection.mjs && node --check src/runtime-decision-verifier.mjs && node --check src/runtime-front-end.mjs && node --check src/runtime-cli.mjs && node --check src/rc3-dogfood-scaffold.mjs && node --check src/runtime-engine.mjs && node --check src/runtime-scripted-executor.mjs && node --check src/runtime-diff-observer.mjs && node --check src/runtime-worktree-executor.mjs && node --check src/runtime-hold-recompile.mjs && node --check src/rc3-scripted-campaign.mjs && node --check src/rc3-actual-dogfood.mjs && node --check src/rc4-stage1-dogfood.mjs && node --check research/fixtures/dispatch-record/src/dispatch-record.mjs && node --check test/research-dispatch-record.test.mjs",
|
|
63
|
+
"check": "node --check bin/lattice.mjs && node --check bin/lattice-mcp.mjs && node --check bin/lattice-dashboard.mjs && node --check bin/lattice-bridge.mjs && node --check bin/lattice-scripted-adapter.mjs && node --check src/cli-stdio.mjs && node --check src/bridge-address.mjs && node --check src/bridge-registrar.mjs && node --check src/todo-independence-guidance.mjs && node --check src/todo-independence.mjs && node --check src/todo-independence-contracts.mjs && node --check src/seam-proposal-contracts.mjs && node --check src/seam-proposal-queries.mjs && node --check src/seam-proposal.mjs && node --check src/todo-gantt-layout.mjs && node --check src/todo-gantt-scope.mjs && node --check src/bridge-config.mjs && node --check src/bridge-server.mjs && node --check src/bridge-daemon.mjs && node --check src/bridge-cli.mjs && node --check src/project-cli.mjs && node --check src/sensor-cli.mjs && node --check src/sensor-runtime.mjs && node --check src/sensor-adapter.mjs && node --check src/factory-diagnostics.mjs && node --check src/runtime-errors.mjs && node --check src/runtime-contracts.mjs && node --check src/runtime-event-store.mjs && node --check src/runtime-adapter-registry.mjs && node --check src/runtime-scripted-adapter-controller.mjs && node --check src/hash-chain.mjs && node --check src/dag-chain.mjs && node --check src/todo-contracts.mjs && node --check src/todo-chain.mjs && node --check src/todo-store.mjs && node --check src/todo-migration.mjs && node --check src/todo-revision.mjs && node --check src/todo-status.mjs && node --check src/todo-cli.mjs && node --check src/todo-dashboard-registry.mjs && node --check src/todo-gantt-presentation.mjs && node --check src/todo-gantt-live.mjs && node --check src/bounded-seam.mjs && node --check src/todo-narrative-anchor.mjs && node --check src/todo-markdown-renderer.mjs && node --check src/todo-gantt-svg.mjs && node --check src/todo-gantt-html.mjs && node --check src/runtime-projection.mjs && node --check src/runtime-decision-verifier.mjs && node --check src/runtime-front-end.mjs && node --check src/runtime-cli.mjs && node --check src/rc3-dogfood-scaffold.mjs && node --check src/runtime-engine.mjs && node --check src/runtime-scripted-executor.mjs && node --check src/runtime-diff-observer.mjs && node --check src/runtime-worktree-executor.mjs && node --check src/runtime-hold-recompile.mjs && node --check src/rc3-scripted-campaign.mjs && node --check src/rc3-actual-dogfood.mjs && node --check src/rc4-stage1-dogfood.mjs && node --check research/fixtures/dispatch-record/src/dispatch-record.mjs && node --check test/research-dispatch-record.test.mjs",
|
|
64
64
|
"check:project-identity": "node --check src/project-identity.mjs",
|
|
65
65
|
"ci": "npm run test && npm run test:sensor && npm run check && npm run check:project-identity"
|
|
66
66
|
}
|
package/src/cli-help.mjs
CHANGED
|
@@ -53,6 +53,7 @@ Read commands:
|
|
|
53
53
|
status [--json]
|
|
54
54
|
bindings [--plan <key>] [--json] # compile_binding付きTaskをTODO identityつきで投影する
|
|
55
55
|
independence [--plan <key>] [--json] # readyを検証済み並列・要直列・未検査へ分けて投影する
|
|
56
|
+
seam-proposal [--plan <key>] [--json] # 記録済みseam提案をsensor無しで投影する
|
|
56
57
|
verify [--plan <key>] [--json]
|
|
57
58
|
snapshot --rebuild --plan <key>
|
|
58
59
|
gantt [--out <file>] [--scope live|all] # 既定live: 完走した工程を図から除く(一覧には残る)
|
|
@@ -69,6 +70,7 @@ Write commands:
|
|
|
69
70
|
evidence promote --plan <key> --task <id> --evidence <file>
|
|
70
71
|
independence compile --plan <key> --input <file> # witness setとsensorから並列可否を記録する
|
|
71
72
|
independence witness migrate --plan <key> # revision後の宣言をtask migrationで写す
|
|
73
|
+
seam-proposal compile --plan <key> # 並列可否記録と実sensorからseam提案を記録する
|
|
72
74
|
revise --plan <key> --input <file>
|
|
73
75
|
revise-phase --plan <key> --input <file>
|
|
74
76
|
revise-set --input <file>
|
|
@@ -130,6 +132,7 @@ const SUBCOMMAND_USAGE = Object.freeze({
|
|
|
130
132
|
'todo status': 'todo status [--json]',
|
|
131
133
|
'todo bindings': 'todo bindings [--plan <key>] [--json]',
|
|
132
134
|
'todo independence': 'todo independence [--plan <key>] [--json] | compile --plan <key> --input <file> | witness migrate --plan <key>',
|
|
135
|
+
'todo seam-proposal': 'todo seam-proposal [--plan <key>] [--json] | compile --plan <key>',
|
|
133
136
|
'todo verify': 'todo verify [--plan <key>] [--json]',
|
|
134
137
|
'todo snapshot': 'todo snapshot --rebuild --plan <key>',
|
|
135
138
|
'todo gantt': 'todo gantt [--out <file>] [--scope live|all] | status [--out <file>] | serve --port <port> [--scope live|all]',
|
package/src/project-cli.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './todo-contracts.mjs';
|
|
15
15
|
import { projectTodoStatus } from './todo-status.mjs';
|
|
16
16
|
import { projectIndependenceFrontier } from './todo-independence.mjs';
|
|
17
|
+
import { isTodoIndependenceLegacyMarker } from './todo-independence-contracts.mjs';
|
|
17
18
|
import { selectIndependenceGuidance } from './todo-independence-guidance.mjs';
|
|
18
19
|
import { ensureTodoDashboardActivity } from './todo-dashboard-registry.mjs';
|
|
19
20
|
import { resolveProjectIdentity } from './project-identity.mjs';
|
|
@@ -161,6 +162,7 @@ async function summarizeIndependence({ repoRoot, store, todo }) {
|
|
|
161
162
|
coverage: projected.coverage,
|
|
162
163
|
guidance: selectIndependenceGuidance({
|
|
163
164
|
coverage: projected.coverage,
|
|
165
|
+
contractSuperseded: isTodoIndependenceLegacyMarker(artifact),
|
|
164
166
|
readyCount: todo.next_ready.filter((task) => task.plan_key === planKey).length,
|
|
165
167
|
taskDeclared: projected.frontier.unknown
|
|
166
168
|
.every(({ unknowns }) => !unknowns.some(({ kind }) => kind === 'witness_missing')),
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import {
|
|
2
|
+
digestTodoArtifact,
|
|
3
|
+
exactRecord,
|
|
4
|
+
isNonNegativeSafeInteger,
|
|
5
|
+
isStrictTodoTimestamp,
|
|
6
|
+
isTodoDigest,
|
|
7
|
+
isTodoIdentifier,
|
|
8
|
+
todoSelfDigest,
|
|
9
|
+
} from './todo-contracts.mjs';
|
|
10
|
+
import {
|
|
11
|
+
TODO_INDEPENDENCE_CONFLICT_KINDS,
|
|
12
|
+
TODO_INDEPENDENCE_COVERAGE,
|
|
13
|
+
TODO_INDEPENDENCE_SCHEMA,
|
|
14
|
+
TODO_INDEPENDENCE_TASK_LIMIT,
|
|
15
|
+
isGitSha,
|
|
16
|
+
severabilityOfConflictKind,
|
|
17
|
+
} from './todo-independence-contracts.mjs';
|
|
18
|
+
import { SEAM_PROPOSAL_GUIDANCE_CODES } from './todo-independence-guidance.mjs';
|
|
19
|
+
|
|
20
|
+
export const SEAM_PROPOSAL_SCHEMA = 'lattice.seam_proposal.v1';
|
|
21
|
+
export const SEAM_PROPOSAL_PROJECTION_SCHEMA = 'lattice.seam_proposal_projection.v1';
|
|
22
|
+
export const SEAM_PROPOSAL_VERDICTS = Object.freeze([
|
|
23
|
+
'seam_candidate',
|
|
24
|
+
'intentional_serial',
|
|
25
|
+
'unknown_requires_evidence',
|
|
26
|
+
]);
|
|
27
|
+
export const SEAM_PROPOSAL_STABLE_SURFACE_ROLES = Object.freeze([
|
|
28
|
+
'composition_test',
|
|
29
|
+
'facade',
|
|
30
|
+
'fixed_oracle',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const LIST_LIMIT = 4_096;
|
|
34
|
+
const CONTROL = /[\u0000-\u001f\u007f]/u;
|
|
35
|
+
const PROPOSAL_ID = /^seam-[0-9a-f]{64}$/u;
|
|
36
|
+
const SURFACE_KINDS = Object.freeze(['path', 'symbol']);
|
|
37
|
+
const STABLE_SURFACE_ROLES = new Set(SEAM_PROPOSAL_STABLE_SURFACE_ROLES);
|
|
38
|
+
|
|
39
|
+
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
40
|
+
|
|
41
|
+
function boundedText(value, maxBytes = 4_096) {
|
|
42
|
+
return typeof value === 'string' && value.length > 0 && value === value.trim()
|
|
43
|
+
&& !CONTROL.test(value) && Buffer.byteLength(value) <= maxBytes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function boundedList(value, validator, { min = 0, max = LIST_LIMIT } = {}) {
|
|
47
|
+
return Array.isArray(value) && value.length >= min && value.length <= max
|
|
48
|
+
&& value.every(validator);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function strictlySorted(values, key = (value) => value) {
|
|
52
|
+
return values.every((value, index) => index === 0
|
|
53
|
+
|| compareText(key(values[index - 1]), key(value)) < 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function repoRelativePathOrPrefix(value) {
|
|
57
|
+
if (!boundedText(value) || value.startsWith('/') || value.includes('\\')
|
|
58
|
+
|| /^[A-Za-z]:/u.test(value)) return false;
|
|
59
|
+
const body = value.endsWith('/') ? value.slice(0, -1) : value;
|
|
60
|
+
return body.length > 0 && body.split('/')
|
|
61
|
+
.every((segment) => segment !== '' && segment !== '.' && segment !== '..');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function repoRelativePath(value) {
|
|
65
|
+
return repoRelativePathOrPrefix(value) && !value.endsWith('/');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sourceBinding(value) {
|
|
69
|
+
return exactRecord(value, [
|
|
70
|
+
'independence_schema', 'independence_result_digest', 'witness_set_digest',
|
|
71
|
+
'plan_version', 'topology_digest', 'base_sha',
|
|
72
|
+
])
|
|
73
|
+
&& value.independence_schema === TODO_INDEPENDENCE_SCHEMA
|
|
74
|
+
&& isTodoDigest(value.independence_result_digest)
|
|
75
|
+
&& isTodoDigest(value.witness_set_digest)
|
|
76
|
+
&& isTodoIdentifier(value.plan_version)
|
|
77
|
+
&& isTodoDigest(value.topology_digest)
|
|
78
|
+
&& isGitSha(value.base_sha);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function taskPair(value, taskIds) {
|
|
82
|
+
return Array.isArray(value) && value.length === 2
|
|
83
|
+
&& value.every(isTodoIdentifier)
|
|
84
|
+
&& compareText(value[0], value[1]) < 0
|
|
85
|
+
&& value.every((taskId) => taskIds.has(taskId));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function conflictEntry(value, taskIds) {
|
|
89
|
+
return exactRecord(value, ['resource_id', 'kind', 'target', 'task_pairs'])
|
|
90
|
+
&& boundedText(value.resource_id)
|
|
91
|
+
&& TODO_INDEPENDENCE_CONFLICT_KINDS.includes(value.kind)
|
|
92
|
+
&& (value.kind === 'path'
|
|
93
|
+
? repoRelativePathOrPrefix(value.target)
|
|
94
|
+
: boundedText(value.target))
|
|
95
|
+
&& boundedList(value.task_pairs, (pair) => taskPair(pair, taskIds), { min: 1 })
|
|
96
|
+
&& strictlySorted(value.task_pairs, (pair) => `${pair[0]}\0${pair[1]}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function conflictsFormConnectedComponent(conflicts, taskIds) {
|
|
100
|
+
const adjacency = new Map([...taskIds].map((taskId) => [taskId, new Set()]));
|
|
101
|
+
for (const { task_pairs: pairs } of conflicts) {
|
|
102
|
+
for (const [left, right] of pairs) {
|
|
103
|
+
adjacency.get(left).add(right);
|
|
104
|
+
adjacency.get(right).add(left);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const first = taskIds.values().next().value;
|
|
108
|
+
const visited = new Set([first]);
|
|
109
|
+
const pending = [first];
|
|
110
|
+
while (pending.length > 0) {
|
|
111
|
+
for (const adjacent of adjacency.get(pending.pop())) {
|
|
112
|
+
if (!visited.has(adjacent)) {
|
|
113
|
+
visited.add(adjacent);
|
|
114
|
+
pending.push(adjacent);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return visited.size === taskIds.size;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const surfaceKey = (value) => `${value.kind}\0${value.target}\0${value.path}\0${value.role}`;
|
|
122
|
+
|
|
123
|
+
function surfaceEntry(value, taskIds, { proposed }) {
|
|
124
|
+
if (!exactRecord(value, ['kind', 'target', 'path', 'role', 'owner_task_ids'])
|
|
125
|
+
|| !SURFACE_KINDS.includes(value.kind)
|
|
126
|
+
|| !repoRelativePathOrPrefix(value.path)
|
|
127
|
+
|| !boundedText(value.target)
|
|
128
|
+
|| !isTodoIdentifier(value.role)
|
|
129
|
+
|| !boundedList(value.owner_task_ids, isTodoIdentifier, {
|
|
130
|
+
max: proposed ? 1 : TODO_INDEPENDENCE_TASK_LIMIT,
|
|
131
|
+
})
|
|
132
|
+
|| !strictlySorted(value.owner_task_ids)
|
|
133
|
+
|| !value.owner_task_ids.every((taskId) => taskIds.has(taskId))) return false;
|
|
134
|
+
if (value.kind === 'path' && value.target !== value.path) return false;
|
|
135
|
+
if (value.kind === 'symbol' && !repoRelativePath(value.path)) return false;
|
|
136
|
+
if (!proposed) return true;
|
|
137
|
+
return STABLE_SURFACE_ROLES.has(value.role)
|
|
138
|
+
? value.owner_task_ids.length === 0
|
|
139
|
+
: value.owner_task_ids.length === 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function queryEntry(value) {
|
|
143
|
+
return exactRecord(value, [
|
|
144
|
+
'query_id', 'operation', 'target', 'outcome', 'resolved_name', 'resolved_path',
|
|
145
|
+
'result_digest',
|
|
146
|
+
])
|
|
147
|
+
&& isTodoIdentifier(value.query_id)
|
|
148
|
+
&& isTodoIdentifier(value.operation)
|
|
149
|
+
&& boundedText(value.target)
|
|
150
|
+
&& isTodoIdentifier(value.outcome)
|
|
151
|
+
&& (value.resolved_name === null || boundedText(value.resolved_name))
|
|
152
|
+
&& (value.resolved_path === null || repoRelativePath(value.resolved_path))
|
|
153
|
+
&& isTodoDigest(value.result_digest);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `query_set_digest` is an external binding to the exact sensor query set input used for
|
|
158
|
+
* collection (the same meaning as `digestArtifact(request.sensor_query_set)` in the runtime
|
|
159
|
+
* front end). That input is not embedded here, so this validator can check only digest shape.
|
|
160
|
+
* `evidence_digest` separately self-seals this record, including that binding and every receipt.
|
|
161
|
+
*/
|
|
162
|
+
function evidenceEntry(value) {
|
|
163
|
+
return exactRecord(value, ['query_set_digest', 'evidence_digest', 'queries'])
|
|
164
|
+
&& isTodoDigest(value.query_set_digest)
|
|
165
|
+
&& isTodoDigest(value.evidence_digest)
|
|
166
|
+
&& boundedList(value.queries, queryEntry)
|
|
167
|
+
&& strictlySorted(value.queries, (query) => query.query_id)
|
|
168
|
+
&& value.evidence_digest === todoSelfDigest(value, 'evidence_digest');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function verificationEntry(value) {
|
|
172
|
+
return exactRecord(value, [
|
|
173
|
+
'virtual_compile_input_digest', 'virtual_compile_result_digest', 'residual_conflicts',
|
|
174
|
+
])
|
|
175
|
+
&& isTodoDigest(value.virtual_compile_input_digest)
|
|
176
|
+
&& isTodoDigest(value.virtual_compile_result_digest)
|
|
177
|
+
&& Array.isArray(value.residual_conflicts)
|
|
178
|
+
&& value.residual_conflicts.length === 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeConflictIdentity(conflicts) {
|
|
182
|
+
return conflicts.map(({ resource_id: resourceId, kind, target, task_pairs: taskPairs }) => ({
|
|
183
|
+
resource_id: resourceId,
|
|
184
|
+
kind,
|
|
185
|
+
target,
|
|
186
|
+
task_pairs: taskPairs.map(([left, right]) => [left, right])
|
|
187
|
+
.sort((left, right) => compareText(`${left[0]}\0${left[1]}`, `${right[0]}\0${right[1]}`)),
|
|
188
|
+
})).sort((left, right) => compareText(left.resource_id, right.resource_id));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeProposedOwnership(surfaces) {
|
|
192
|
+
return surfaces.map(({ kind, target, path, role, owner_task_ids: ownerTaskIds }) => ({
|
|
193
|
+
kind,
|
|
194
|
+
target,
|
|
195
|
+
path,
|
|
196
|
+
role,
|
|
197
|
+
owner_task_ids: [...ownerTaskIds].sort(compareText),
|
|
198
|
+
})).sort((left, right) => compareText(surfaceKey(left), surfaceKey(right)));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Content receipts and source freshness deliberately do not participate in semantic identity.
|
|
203
|
+
* The caller still has to pass the same conflict component and proposed ownership shape.
|
|
204
|
+
*/
|
|
205
|
+
export function deriveSeamProposalId({ conflicts, proposed_surfaces: proposedSurfaces }) {
|
|
206
|
+
const semanticKey = {
|
|
207
|
+
conflicts: normalizeConflictIdentity(conflicts),
|
|
208
|
+
proposed_surfaces: normalizeProposedOwnership(proposedSurfaces),
|
|
209
|
+
};
|
|
210
|
+
return `seam-${digestTodoArtifact(semanticKey)}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function seamCandidate(value, conflicts, taskIds) {
|
|
214
|
+
if (!exactRecord(value, [
|
|
215
|
+
'proposal_id', 'current_surfaces', 'proposed_surfaces', 'affected_tests',
|
|
216
|
+
'verification', 'evidence', 'limits', 'proposal_digest',
|
|
217
|
+
])
|
|
218
|
+
|| !PROPOSAL_ID.test(value.proposal_id)
|
|
219
|
+
|| !boundedList(value.current_surfaces,
|
|
220
|
+
(surface) => surfaceEntry(surface, taskIds, { proposed: false }))
|
|
221
|
+
|| !strictlySorted(value.current_surfaces, surfaceKey)
|
|
222
|
+
|| !boundedList(value.proposed_surfaces,
|
|
223
|
+
(surface) => surfaceEntry(surface, taskIds, { proposed: true }), { min: 1 })
|
|
224
|
+
|| !strictlySorted(value.proposed_surfaces, surfaceKey)
|
|
225
|
+
|| !boundedList(value.affected_tests, repoRelativePath)
|
|
226
|
+
|| !strictlySorted(value.affected_tests)
|
|
227
|
+
|| !verificationEntry(value.verification)
|
|
228
|
+
|| !evidenceEntry(value.evidence)
|
|
229
|
+
|| !boundedList(value.limits, isTodoIdentifier, { min: 1 })
|
|
230
|
+
|| !strictlySorted(value.limits)
|
|
231
|
+
|| !value.limits.includes('structural_only')
|
|
232
|
+
|| !isTodoDigest(value.proposal_digest)) return false;
|
|
233
|
+
const proposedOwnerTaskIds = new Set(value.proposed_surfaces
|
|
234
|
+
.flatMap(({ owner_task_ids: ownerTaskIds }) => ownerTaskIds));
|
|
235
|
+
if (proposedOwnerTaskIds.size !== taskIds.size
|
|
236
|
+
|| ![...taskIds].every((taskId) => proposedOwnerTaskIds.has(taskId))) return false;
|
|
237
|
+
if (value.proposal_id !== deriveSeamProposalId({
|
|
238
|
+
conflicts,
|
|
239
|
+
proposed_surfaces: value.proposed_surfaces,
|
|
240
|
+
})) return false;
|
|
241
|
+
return value.proposal_digest === todoSelfDigest(value, 'proposal_digest');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function reasonEntry(value) {
|
|
245
|
+
return exactRecord(value, ['code', 'detail'])
|
|
246
|
+
&& isTodoIdentifier(value.code) && boundedText(value.detail);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function unknownEntry(value) {
|
|
250
|
+
return exactRecord(value, ['kind', 'ref'])
|
|
251
|
+
&& isTodoIdentifier(value.kind) && boundedText(value.ref);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function decisionEntry(value) {
|
|
255
|
+
if (!exactRecord(value, [
|
|
256
|
+
'component_id', 'task_ids', 'conflicts', 'verdict', 'seam_candidate',
|
|
257
|
+
'reasons', 'unknowns',
|
|
258
|
+
])
|
|
259
|
+
|| !isTodoIdentifier(value.component_id)
|
|
260
|
+
|| !boundedList(value.task_ids, isTodoIdentifier, {
|
|
261
|
+
min: 2, max: TODO_INDEPENDENCE_TASK_LIMIT,
|
|
262
|
+
})
|
|
263
|
+
|| !strictlySorted(value.task_ids)
|
|
264
|
+
|| !SEAM_PROPOSAL_VERDICTS.includes(value.verdict)
|
|
265
|
+
|| !boundedList(value.reasons, reasonEntry)
|
|
266
|
+
|| !strictlySorted(value.reasons, (reason) => `${reason.code}\0${reason.detail}`)
|
|
267
|
+
|| !boundedList(value.unknowns, unknownEntry)
|
|
268
|
+
|| !strictlySorted(value.unknowns, (unknown) => `${unknown.kind}\0${unknown.ref}`)) return false;
|
|
269
|
+
const taskIds = new Set(value.task_ids);
|
|
270
|
+
if (!boundedList(value.conflicts, (conflict) => conflictEntry(conflict, taskIds), { min: 1 })
|
|
271
|
+
|| !strictlySorted(value.conflicts, (conflict) => conflict.resource_id)
|
|
272
|
+
|| !conflictsFormConnectedComponent(value.conflicts, taskIds)) return false;
|
|
273
|
+
if (value.verdict === 'seam_candidate') {
|
|
274
|
+
return value.unknowns.length === 0
|
|
275
|
+
&& value.conflicts.every(({ kind }) => severabilityOfConflictKind(kind) === 'code_seam')
|
|
276
|
+
&& seamCandidate(value.seam_candidate, value.conflicts, taskIds);
|
|
277
|
+
}
|
|
278
|
+
if (value.seam_candidate !== null) return false;
|
|
279
|
+
if (value.verdict === 'intentional_serial') return value.reasons.length > 0;
|
|
280
|
+
return value.unknowns.length > 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Validate the immutable public `lattice.seam_proposal.v1` artifact.
|
|
285
|
+
*
|
|
286
|
+
* This validates the artifact's closed runtime shape and canonical relations. Binding the
|
|
287
|
+
* recorded conflicts back to the referenced independence bytes is a consumer responsibility.
|
|
288
|
+
*/
|
|
289
|
+
export function validateSeamProposal(value) {
|
|
290
|
+
try {
|
|
291
|
+
if (!exactRecord(value, [
|
|
292
|
+
'schema', 'project_id', 'plan_key', 'source_binding', 'compiled_at',
|
|
293
|
+
'decisions', 'result_digest',
|
|
294
|
+
])
|
|
295
|
+
|| value.schema !== SEAM_PROPOSAL_SCHEMA
|
|
296
|
+
|| !isTodoIdentifier(value.project_id)
|
|
297
|
+
|| !isTodoIdentifier(value.plan_key)
|
|
298
|
+
|| !sourceBinding(value.source_binding)
|
|
299
|
+
|| !isStrictTodoTimestamp(value.compiled_at)
|
|
300
|
+
|| !boundedList(value.decisions, decisionEntry)
|
|
301
|
+
|| !strictlySorted(value.decisions, (decision) => decision.component_id)
|
|
302
|
+
|| !isTodoDigest(value.result_digest)) return false;
|
|
303
|
+
const resourceIds = value.decisions
|
|
304
|
+
.flatMap(({ conflicts }) => conflicts.map(({ resource_id: resourceId }) => resourceId));
|
|
305
|
+
if (new Set(resourceIds).size !== resourceIds.length) return false;
|
|
306
|
+
return value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
307
|
+
} catch {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const PROJECTION_GUIDANCE_BY_COVERAGE = Object.freeze({
|
|
313
|
+
missing: 'seam_proposal_unrecorded',
|
|
314
|
+
stale: 'seam_proposal_stale',
|
|
315
|
+
superseded: 'seam_proposal_superseded',
|
|
316
|
+
verified: 'seam_proposal_verified',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
function projectionGuidance(value, coverage) {
|
|
320
|
+
return exactRecord(value, ['code', 'message', 'next_action'])
|
|
321
|
+
&& SEAM_PROPOSAL_GUIDANCE_CODES.includes(value.code)
|
|
322
|
+
&& value.code === PROJECTION_GUIDANCE_BY_COVERAGE[coverage]
|
|
323
|
+
&& boundedText(value.message)
|
|
324
|
+
&& isTodoIdentifier(value.next_action);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function projectionComponent(value) {
|
|
328
|
+
if (!exactRecord(value, [
|
|
329
|
+
'component_id', 'verdict', 'task_ids', 'conflicts', 'proposed_surfaces',
|
|
330
|
+
'affected_tests', 'limits', 'reasons', 'unknowns',
|
|
331
|
+
])
|
|
332
|
+
|| !isTodoIdentifier(value.component_id)
|
|
333
|
+
|| !SEAM_PROPOSAL_VERDICTS.includes(value.verdict)
|
|
334
|
+
|| !boundedList(value.task_ids, isTodoIdentifier, {
|
|
335
|
+
min: 2, max: TODO_INDEPENDENCE_TASK_LIMIT,
|
|
336
|
+
})
|
|
337
|
+
|| !strictlySorted(value.task_ids)
|
|
338
|
+
|| !boundedList(value.reasons, reasonEntry)
|
|
339
|
+
|| !strictlySorted(value.reasons, (reason) => `${reason.code}\0${reason.detail}`)
|
|
340
|
+
|| !boundedList(value.unknowns, unknownEntry)
|
|
341
|
+
|| !strictlySorted(value.unknowns, (unknown) => `${unknown.kind}\0${unknown.ref}`)
|
|
342
|
+
|| !boundedList(value.affected_tests, repoRelativePath)
|
|
343
|
+
|| !strictlySorted(value.affected_tests)
|
|
344
|
+
|| !boundedList(value.limits, isTodoIdentifier)
|
|
345
|
+
|| !strictlySorted(value.limits)) return false;
|
|
346
|
+
const taskIds = new Set(value.task_ids);
|
|
347
|
+
if (!boundedList(value.conflicts, (conflict) => conflictEntry(conflict, taskIds), { min: 1 })
|
|
348
|
+
|| !strictlySorted(value.conflicts, (conflict) => conflict.resource_id)
|
|
349
|
+
|| !conflictsFormConnectedComponent(value.conflicts, taskIds)
|
|
350
|
+
|| !boundedList(value.proposed_surfaces,
|
|
351
|
+
(surface) => surfaceEntry(surface, taskIds, { proposed: true }))
|
|
352
|
+
|| !strictlySorted(value.proposed_surfaces, surfaceKey)) return false;
|
|
353
|
+
if (value.verdict === 'seam_candidate') {
|
|
354
|
+
const ownerTaskIds = new Set(value.proposed_surfaces
|
|
355
|
+
.flatMap(({ owner_task_ids: owners }) => owners));
|
|
356
|
+
return value.unknowns.length === 0
|
|
357
|
+
&& value.proposed_surfaces.length > 0
|
|
358
|
+
&& value.limits.includes('structural_only')
|
|
359
|
+
&& ownerTaskIds.size === taskIds.size
|
|
360
|
+
&& [...taskIds].every((taskId) => ownerTaskIds.has(taskId));
|
|
361
|
+
}
|
|
362
|
+
if (value.proposed_surfaces.length > 0
|
|
363
|
+
|| value.affected_tests.length > 0
|
|
364
|
+
|| value.limits.length > 0) return false;
|
|
365
|
+
if (value.verdict === 'intentional_serial') return value.reasons.length > 0;
|
|
366
|
+
return value.unknowns.length > 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Validate the read-only lattice.seam_proposal_projection.v1 public CLI projection.
|
|
371
|
+
* The immutable seam proposal artifact contract above remains independent from this read model.
|
|
372
|
+
*/
|
|
373
|
+
export function validateSeamProposalProjection(value) {
|
|
374
|
+
try {
|
|
375
|
+
if (!exactRecord(value, [
|
|
376
|
+
'schema', 'project_id', 'plan_key', 'coverage', 'compiled_base_sha',
|
|
377
|
+
'current_base_sha', 'plan_version', 'topology_digest', 'independence_result_digest',
|
|
378
|
+
'compiled_at', 'guidance', 'component_count', 'conflict_resource_count',
|
|
379
|
+
'components', 'result_digest',
|
|
380
|
+
])
|
|
381
|
+
|| value.schema !== SEAM_PROPOSAL_PROJECTION_SCHEMA
|
|
382
|
+
|| !isTodoIdentifier(value.project_id)
|
|
383
|
+
|| !isTodoIdentifier(value.plan_key)
|
|
384
|
+
|| !TODO_INDEPENDENCE_COVERAGE.includes(value.coverage)
|
|
385
|
+
|| !isGitSha(value.current_base_sha)
|
|
386
|
+
|| !projectionGuidance(value.guidance, value.coverage)
|
|
387
|
+
|| !isTodoDigest(value.result_digest)) return false;
|
|
388
|
+
|
|
389
|
+
if (value.coverage === 'missing') {
|
|
390
|
+
return value.compiled_base_sha === null
|
|
391
|
+
&& value.plan_version === null
|
|
392
|
+
&& value.topology_digest === null
|
|
393
|
+
&& value.independence_result_digest === null
|
|
394
|
+
&& value.compiled_at === null
|
|
395
|
+
&& value.component_count === null
|
|
396
|
+
&& value.conflict_resource_count === null
|
|
397
|
+
&& Array.isArray(value.components)
|
|
398
|
+
&& value.components.length === 0
|
|
399
|
+
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!isGitSha(value.compiled_base_sha)
|
|
403
|
+
|| !isTodoIdentifier(value.plan_version)
|
|
404
|
+
|| !isTodoDigest(value.topology_digest)
|
|
405
|
+
|| !isTodoDigest(value.independence_result_digest)
|
|
406
|
+
|| !isStrictTodoTimestamp(value.compiled_at)
|
|
407
|
+
|| !isNonNegativeSafeInteger(value.component_count)
|
|
408
|
+
|| !isNonNegativeSafeInteger(value.conflict_resource_count)
|
|
409
|
+
|| !boundedList(value.components, projectionComponent)
|
|
410
|
+
|| !strictlySorted(value.components, (component) => component.component_id)
|
|
411
|
+
|| value.component_count !== value.components.length
|
|
412
|
+
|| value.conflict_resource_count !== value.components
|
|
413
|
+
.reduce((count, component) => count + component.conflicts.length, 0)) return false;
|
|
414
|
+
if (value.coverage === 'verified' && value.compiled_base_sha !== value.current_base_sha) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
if (value.coverage === 'stale' && value.compiled_base_sha === value.current_base_sha) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
const resourceIds = value.components
|
|
421
|
+
.flatMap(({ conflicts }) => conflicts.map(({ resource_id: resourceId }) => resourceId));
|
|
422
|
+
return new Set(resourceIds).size === resourceIds.length
|
|
423
|
+
&& value.result_digest === todoSelfDigest(value, 'result_digest');
|
|
424
|
+
} catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|