@quolu/lattice 0.15.0 → 0.17.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 +3 -2
- package/src/bridge-daemon.mjs +43 -5
- package/src/seam-proposal-contracts.mjs +20 -15
- package/src/seam-proposal-queries.mjs +25 -1
- package/src/seam-proposal.mjs +230 -11
- package/src/todo-cli.mjs +6 -2
- package/src/todo-independence-contracts.mjs +80 -4
- package/src/todo-independence-guidance.mjs +77 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/lattice",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"precheck": "node --check src/bridge-launch-agent.mjs",
|
|
63
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
|
-
"ci": "npm run test && npm run test:sensor && npm run check && npm run check:project-identity"
|
|
65
|
+
"ci": "npm run test && npm run test:sensor && npm run check && npm run check:project-identity && npm run verify:todo-store",
|
|
66
|
+
"verify:todo-store": "node bin/lattice.mjs todo verify --json"
|
|
66
67
|
}
|
|
67
68
|
}
|
package/src/bridge-daemon.mjs
CHANGED
|
@@ -17,6 +17,9 @@ const STOP_REQUEST_SCHEMA = 'lattice.bridge_stop_request.v1';
|
|
|
17
17
|
const STOP_RECEIPT_SCHEMA = 'lattice.bridge_stop_receipt.v1';
|
|
18
18
|
const ACTIVE_MARKER_SCHEMA = 'lattice.bridge_daemon_active.v1';
|
|
19
19
|
const CONTROL_MAX_BYTES = 65_536;
|
|
20
|
+
/** control fileの差し替え競合だけを吸収する再読の上限と間隔(`readStrictJson`が唯一の使用者)。 */
|
|
21
|
+
const CONTROL_READ_RETRY_LIMIT = 5;
|
|
22
|
+
const CONTROL_READ_RETRY_DELAY_MS = 20;
|
|
20
23
|
|
|
21
24
|
export function bridgeDaemonDescriptorPath(env = process.env) {
|
|
22
25
|
return path.join(bridgeConfigPaths(env).root, 'bridge-daemon.json');
|
|
@@ -66,7 +69,16 @@ function isIsoTimestamp(value) {
|
|
|
66
69
|
try { return new Date(value).toISOString() === value; } catch { return false; }
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
|
|
72
|
+
/** 読み取り中の差し替え・消失につけるmarker。内容の異常と同じ顔をさせない。 */
|
|
73
|
+
const CONTROL_READ_RACE = Symbol('bridge_control_read_race');
|
|
74
|
+
|
|
75
|
+
function controlReadRace(message) {
|
|
76
|
+
const error = new Error(message);
|
|
77
|
+
error[CONTROL_READ_RACE] = true;
|
|
78
|
+
return error;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function readStrictJsonOnce(ref, label) {
|
|
70
82
|
let before;
|
|
71
83
|
let handle;
|
|
72
84
|
try {
|
|
@@ -77,12 +89,12 @@ async function readStrictJson(ref, code, label) {
|
|
|
77
89
|
const opened = await handle.stat();
|
|
78
90
|
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino
|
|
79
91
|
|| opened.size !== before.size || opened.size > CONTROL_MAX_BYTES) {
|
|
80
|
-
throw
|
|
92
|
+
throw controlReadRace(`${label} changed during validation`);
|
|
81
93
|
}
|
|
82
94
|
const text = await handle.readFile('utf8');
|
|
83
95
|
const after = await lstat(ref);
|
|
84
96
|
if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size) {
|
|
85
|
-
throw
|
|
97
|
+
throw controlReadRace(`${label} changed during read`);
|
|
86
98
|
}
|
|
87
99
|
const errors = [];
|
|
88
100
|
const tree = parseTree(text, errors, { allowTrailingComma: false, disallowComments: true });
|
|
@@ -91,11 +103,37 @@ async function readStrictJson(ref, code, label) {
|
|
|
91
103
|
}
|
|
92
104
|
return JSON.parse(text);
|
|
93
105
|
} catch (error) {
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
// 最初のlstatで無い=未起動。読み始めた後に消えたのは停止との競合で、再読でnullへ落ち着く。
|
|
107
|
+
if (error?.code === 'ENOENT') {
|
|
108
|
+
if (before === undefined) return null;
|
|
109
|
+
throw controlReadRace(`${label} removed during read`);
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
96
112
|
} finally { await handle?.close(); }
|
|
97
113
|
}
|
|
98
114
|
|
|
115
|
+
/**
|
|
116
|
+
* 公開はatomic renameなので、読み手はinodeの差し替えに必ず出会う。差し替えを跨いだ読みは
|
|
117
|
+
* 内容の異常ではなく再読で解ける競合であり、壊れたcontrol fileと同じerrorにしてはならない。
|
|
118
|
+
*
|
|
119
|
+
* 再試行するのは`CONTROL_READ_RACE`が付いた3条件だけである——検証中のinode/size変化、
|
|
120
|
+
* 読み取り中のinode/size変化、最初のlstat後の消失。最大`CONTROL_READ_RETRY_LIMIT`回、
|
|
121
|
+
* 間隔`CONTROL_READ_RETRY_DELAY_MS`で再読し、超えたら他と同じtyped errorで落とす。
|
|
122
|
+
* JSON不正・schema不正・mode不正は競合ではないので一度も再試行せず即fail closedにする。
|
|
123
|
+
*/
|
|
124
|
+
async function readStrictJson(ref, code, label) {
|
|
125
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
126
|
+
try {
|
|
127
|
+
return await readStrictJsonOnce(ref, label);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error?.[CONTROL_READ_RACE] !== true || attempt >= CONTROL_READ_RETRY_LIMIT) {
|
|
130
|
+
throw new BridgeConfigError(code, `${label} invalid`, undefined, error);
|
|
131
|
+
}
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, CONTROL_READ_RETRY_DELAY_MS));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
99
137
|
async function readStrictControl(ref, schema, keys) {
|
|
100
138
|
const value = await readStrictJson(ref, 'BRIDGE_STOP_CONTROL_INVALID', 'bridge stop control');
|
|
101
139
|
if (value === null) return null;
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
isGitSha,
|
|
16
16
|
severabilityOfConflictKind,
|
|
17
17
|
} from './todo-independence-contracts.mjs';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
SEAM_PROPOSAL_GUIDANCE_CODES,
|
|
20
|
+
seamProposalGuidanceCode,
|
|
21
|
+
} from './todo-independence-guidance.mjs';
|
|
19
22
|
|
|
20
23
|
export const SEAM_PROPOSAL_SCHEMA = 'lattice.seam_proposal.v1';
|
|
21
24
|
export const SEAM_PROPOSAL_PROJECTION_SCHEMA = 'lattice.seam_proposal_projection.v1';
|
|
@@ -309,19 +312,21 @@ export function validateSeamProposal(value) {
|
|
|
309
312
|
}
|
|
310
313
|
}
|
|
311
314
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
315
|
+
/**
|
|
316
|
+
* 案内は、鮮度と載っているunknownから一意に決まる。生成側の文言をそのまま載せる規約なので
|
|
317
|
+
* shapeだけを見ると、投影が状況と噛み合わない案内を載せても通ってしまう。codeは規則正本へ
|
|
318
|
+
* 引き直して照合する(ADR 0130 Decision 1・2)。
|
|
319
|
+
*/
|
|
320
|
+
function projectionGuidance(value, coverage, components) {
|
|
321
|
+
if (!exactRecord(value, ['code', 'message', 'next_action'])
|
|
322
|
+
|| !SEAM_PROPOSAL_GUIDANCE_CODES.includes(value.code)
|
|
323
|
+
|| !boundedText(value.message)
|
|
324
|
+
|| !isTodoIdentifier(value.next_action)) return false;
|
|
325
|
+
const unknownKinds = Array.isArray(components)
|
|
326
|
+
? components.flatMap((component) => (Array.isArray(component?.unknowns)
|
|
327
|
+
? component.unknowns.map((unknown) => unknown?.kind) : []))
|
|
328
|
+
: [];
|
|
329
|
+
return value.code === seamProposalGuidanceCode({ coverage, unknownKinds });
|
|
325
330
|
}
|
|
326
331
|
|
|
327
332
|
function projectionComponent(value) {
|
|
@@ -383,7 +388,7 @@ export function validateSeamProposalProjection(value) {
|
|
|
383
388
|
|| !isTodoIdentifier(value.plan_key)
|
|
384
389
|
|| !TODO_INDEPENDENCE_COVERAGE.includes(value.coverage)
|
|
385
390
|
|| !isGitSha(value.current_base_sha)
|
|
386
|
-
|| !projectionGuidance(value.guidance, value.coverage)
|
|
391
|
+
|| !projectionGuidance(value.guidance, value.coverage, value.components)
|
|
387
392
|
|| !isTodoDigest(value.result_digest)) return false;
|
|
388
393
|
|
|
389
394
|
if (value.coverage === 'missing') {
|
|
@@ -129,13 +129,30 @@ function assertRuntimeQuerySet(querySet) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function assertConcernSymbols(concernSymbols) {
|
|
133
|
+
if (!Array.isArray(concernSymbols) || concernSymbols.length > QUERY_LIMIT) {
|
|
134
|
+
fail('SEAM_QUERY_CONCERN_SYMBOLS_INVALID', 'concern_symbols_not_bounded_array');
|
|
135
|
+
}
|
|
136
|
+
for (const symbol of concernSymbols) {
|
|
137
|
+
if (!boundedText(symbol, 1_024)) {
|
|
138
|
+
fail('SEAM_QUERY_CONCERN_SYMBOL_INVALID', 'concern_symbol_invalid');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return sortedUnique(concernSymbols);
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
/**
|
|
133
145
|
* Build the schema-less `lattice.run_request.v1.sensor_query_set` vocabulary used by
|
|
134
146
|
* todo-independence. The wrapper records non-code conflicts without putting their targets
|
|
135
147
|
* on the sensor command line.
|
|
148
|
+
*
|
|
149
|
+
* `concernSymbols` are the symbol names declared through witness `concern_anchors`. They only
|
|
150
|
+
* need the `query` operation: the binder asks whether the declared name resolves to exactly one
|
|
151
|
+
* symbol, and where it lives. Graph expansion stays owned by the conflict resources.
|
|
136
152
|
*/
|
|
137
|
-
export function buildSeamProposalQuerySet({ conflictResources } = {}) {
|
|
153
|
+
export function buildSeamProposalQuerySet({ conflictResources, concernSymbols = [] } = {}) {
|
|
138
154
|
const resources = assertConflictResources(conflictResources);
|
|
155
|
+
const concerns = assertConcernSymbols(concernSymbols);
|
|
139
156
|
const queryById = new Map([[
|
|
140
157
|
'seam-00-status',
|
|
141
158
|
{ id: 'seam-00-status', operation: 'status' },
|
|
@@ -168,6 +185,13 @@ export function buildSeamProposalQuerySet({ conflictResources } = {}) {
|
|
|
168
185
|
}
|
|
169
186
|
}
|
|
170
187
|
|
|
188
|
+
// 宣言concern symbolの解決query。conflict symbolと同名なら既存queryがそのまま答えになる。
|
|
189
|
+
for (const symbol of concerns) {
|
|
190
|
+
const id = queryId('query', 'symbol', symbol);
|
|
191
|
+
if (queryById.has(id)) continue;
|
|
192
|
+
queryById.set(id, { id, operation: 'query', target: symbol });
|
|
193
|
+
}
|
|
194
|
+
|
|
171
195
|
const querySet = {
|
|
172
196
|
queries: [...queryById.values()]
|
|
173
197
|
.sort((left, right) => compareText(left.id, right.id)),
|
package/src/seam-proposal.mjs
CHANGED
|
@@ -755,6 +755,122 @@ export function verifyVirtualCompileReceipt({ verification, ...options } = {}) {
|
|
|
755
755
|
};
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
+
/** 宣言されたconcern symbolを、`query` operationの解決receiptから探す。 */
|
|
759
|
+
function resolvedSymbolPath(queries, name) {
|
|
760
|
+
const receipt = queries.find((query) => (
|
|
761
|
+
query.operation === 'query'
|
|
762
|
+
&& query.target === name
|
|
763
|
+
&& query.outcome === 'resolved'
|
|
764
|
+
&& query.resolved_name === name
|
|
765
|
+
&& typeof query.resolved_path === 'string'
|
|
766
|
+
&& query.resolved_path.length > 0
|
|
767
|
+
));
|
|
768
|
+
return receipt === undefined ? null : receipt.resolved_path;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function pathContains(resourcePath, symbolPath) {
|
|
772
|
+
return resourcePath.endsWith('/')
|
|
773
|
+
? symbolPath.startsWith(resourcePath)
|
|
774
|
+
: symbolPath === resourcePath;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Resolve declared concern anchors against fresh sensor evidence.
|
|
779
|
+
*
|
|
780
|
+
* A declaration only becomes a binding anchor when the sensor resolves the exact name to exactly
|
|
781
|
+
* one path and that path lies inside the declared resource. Fuzzy resolution to a neighbouring
|
|
782
|
+
* symbol, an absent name, or a symbol living outside the contested resource yields a typed
|
|
783
|
+
* unknown instead — a wrong declaration must never widen what the binder believes it knows.
|
|
784
|
+
*
|
|
785
|
+
* Two ToDos claiming the same symbol is a contradiction in the declarations themselves, not a cut
|
|
786
|
+
* to be discovered: the anchor is dropped from both and reported, so neither side can be bound by
|
|
787
|
+
* a claim the other also makes.
|
|
788
|
+
*/
|
|
789
|
+
export function resolveConcernAnchors({ manualWitness, taskIds, evidence } = {}) {
|
|
790
|
+
if (!plainRecord(manualWitness) || !Array.isArray(taskIds) || !plainRecord(evidence)) {
|
|
791
|
+
fail('concern anchor resolution input shapeが不正');
|
|
792
|
+
}
|
|
793
|
+
// receiptが1件も無い証拠は「解決しなかった」であって、宣言を素通しさせる理由にはしない。
|
|
794
|
+
const queries = Array.isArray(evidence.queries) ? evidence.queries : [];
|
|
795
|
+
const anchorsByTask = new Map();
|
|
796
|
+
const unknowns = [];
|
|
797
|
+
for (const taskId of taskIds) {
|
|
798
|
+
const anchors = [];
|
|
799
|
+
for (const entry of manualWitness[taskId]?.concern_anchors ?? []) {
|
|
800
|
+
const resourcePath = entry.within.kind === 'path'
|
|
801
|
+
? entry.within.target
|
|
802
|
+
: resolvedSymbolPath(queries, entry.within.target);
|
|
803
|
+
if (resourcePath === null) {
|
|
804
|
+
unknowns.push({
|
|
805
|
+
kind: 'concern_anchor_resource_unresolved',
|
|
806
|
+
ref: `${taskId}:${entry.within.kind}:${entry.within.target}`,
|
|
807
|
+
});
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
for (const symbol of entry.symbols) {
|
|
811
|
+
const symbolPath = resolvedSymbolPath(queries, symbol);
|
|
812
|
+
if (symbolPath === null) {
|
|
813
|
+
unknowns.push({ kind: 'concern_anchor_unresolved', ref: `${taskId}:${symbol}` });
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (!pathContains(resourcePath, symbolPath)) {
|
|
817
|
+
unknowns.push({
|
|
818
|
+
kind: 'concern_anchor_outside_resource',
|
|
819
|
+
ref: `${taskId}:${symbol}:${symbolPath}`,
|
|
820
|
+
});
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
anchors.push(`concern:${symbolPath}\0${symbol}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
anchorsByTask.set(taskId, sortedUnique(anchors));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// 同じsymbolを2 task以上が担当と主張したら、どちらの束縛根拠にもしない。
|
|
830
|
+
const claimantsByAnchor = new Map();
|
|
831
|
+
for (const [taskId, anchors] of anchorsByTask) {
|
|
832
|
+
for (const anchor of anchors) {
|
|
833
|
+
if (!claimantsByAnchor.has(anchor)) claimantsByAnchor.set(anchor, []);
|
|
834
|
+
claimantsByAnchor.get(anchor).push(taskId);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const overlapping = new Set();
|
|
838
|
+
for (const [anchor, claimants] of claimantsByAnchor) {
|
|
839
|
+
if (claimants.length < 2) continue;
|
|
840
|
+
overlapping.add(anchor);
|
|
841
|
+
const [path, symbol] = anchor.slice('concern:'.length).split('\0');
|
|
842
|
+
unknowns.push({
|
|
843
|
+
kind: 'concern_anchor_overlap',
|
|
844
|
+
ref: `${[...claimants].sort(compareText).join(',')}:${path}:${symbol}`,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
if (overlapping.size > 0) {
|
|
848
|
+
for (const [taskId, anchors] of anchorsByTask) {
|
|
849
|
+
anchorsByTask.set(taskId, anchors.filter((anchor) => !overlapping.has(anchor)));
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
anchorsByTask,
|
|
855
|
+
unknowns: unknowns.sort((left, right) => compareText(
|
|
856
|
+
`${left.kind}\0${left.ref}`, `${right.kind}\0${right.ref}`,
|
|
857
|
+
)),
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** witness set全体から、sensorへ問い合わせるconcern symbol名を集める。 */
|
|
862
|
+
export function declaredConcernSymbols(manualWitness) {
|
|
863
|
+
if (!plainRecord(manualWitness)) fail('manual witness shapeが不正');
|
|
864
|
+
const names = [];
|
|
865
|
+
for (const witness of Object.values(manualWitness)) {
|
|
866
|
+
for (const entry of witness?.concern_anchors ?? []) {
|
|
867
|
+
names.push(...entry.symbols);
|
|
868
|
+
if (entry.within.kind === 'symbol') names.push(entry.within.target);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return sortedUnique(names);
|
|
872
|
+
}
|
|
873
|
+
|
|
758
874
|
function uniqueIntentAnchors(manualWitness, taskIds) {
|
|
759
875
|
const anchorsByTask = new Map();
|
|
760
876
|
const counts = new Map();
|
|
@@ -1008,6 +1124,12 @@ function canonicalPartitions(partitions) {
|
|
|
1008
1124
|
}
|
|
1009
1125
|
|
|
1010
1126
|
function anchorMatchesNode(anchor, node) {
|
|
1127
|
+
if (anchor.startsWith('concern:')) {
|
|
1128
|
+
const separator = anchor.indexOf('\0');
|
|
1129
|
+
return node.kind === 'symbol'
|
|
1130
|
+
&& node.path === anchor.slice('concern:'.length, separator)
|
|
1131
|
+
&& node.target === anchor.slice(separator + 1);
|
|
1132
|
+
}
|
|
1011
1133
|
if (anchor.startsWith('owns:symbol\0')) {
|
|
1012
1134
|
return node.kind === 'symbol' && node.target === anchor.slice('owns:symbol\0'.length);
|
|
1013
1135
|
}
|
|
@@ -1021,18 +1143,32 @@ function anchorMatchesNode(anchor, node) {
|
|
|
1021
1143
|
return false;
|
|
1022
1144
|
}
|
|
1023
1145
|
|
|
1024
|
-
|
|
1146
|
+
/**
|
|
1147
|
+
* Bind tasks to partitions.
|
|
1148
|
+
*
|
|
1149
|
+
* Declared concern anchors take precedence over the coarse owns/writes/test anchors within one
|
|
1150
|
+
* skeleton: a ToDo that named the symbols it touches inside the contested resource has given
|
|
1151
|
+
* strictly more specific evidence than "it owns some file". The coarse anchors stay as the
|
|
1152
|
+
* fallback for skeletons the declaration says nothing about, so declaring a concern for one
|
|
1153
|
+
* conflict never blinds the binder to another.
|
|
1154
|
+
*/
|
|
1155
|
+
function bindSkeleton({ skeleton, graph, intentAnchors, concernAnchors, taskIds }) {
|
|
1025
1156
|
const nodeByKey = new Map(graph.nodes.map((node) => [graphNodeKey(node), node]));
|
|
1026
1157
|
const taskBindings = [];
|
|
1027
1158
|
const unknowns = [];
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1159
|
+
const matchesFor = (anchors) => {
|
|
1160
|
+
const matched = [];
|
|
1030
1161
|
skeleton.partitions.forEach((partition, index) => {
|
|
1031
|
-
const
|
|
1162
|
+
const hits = anchors.filter((anchor) => (
|
|
1032
1163
|
partition.some((key) => anchorMatchesNode(anchor, nodeByKey.get(key)))
|
|
1033
1164
|
));
|
|
1034
|
-
if (
|
|
1165
|
+
if (hits.length > 0) matched.push({ index, anchors: sortedUnique(hits) });
|
|
1035
1166
|
});
|
|
1167
|
+
return matched;
|
|
1168
|
+
};
|
|
1169
|
+
for (const taskId of taskIds) {
|
|
1170
|
+
const declared = matchesFor(concernAnchors.get(taskId) ?? []);
|
|
1171
|
+
const matches = declared.length > 0 ? declared : matchesFor(intentAnchors.get(taskId));
|
|
1036
1172
|
if (matches.length === 0) {
|
|
1037
1173
|
unknowns.push({
|
|
1038
1174
|
kind: 'semantic_owner_binding_missing',
|
|
@@ -1068,15 +1204,54 @@ function bindSkeleton({ skeleton, graph, intentAnchors, taskIds }) {
|
|
|
1068
1204
|
};
|
|
1069
1205
|
}
|
|
1070
1206
|
|
|
1207
|
+
/**
|
|
1208
|
+
* Build a cut skeleton for a contested repo path out of the declared concern anchors alone.
|
|
1209
|
+
*
|
|
1210
|
+
* A path conflict has no call graph to partition — the sensor was only asked which tests the file
|
|
1211
|
+
* affects. What the declarations do give is a partition of the file's symbols by owner, which is
|
|
1212
|
+
* exactly the shape a cut needs. Every task in the component must have named at least one symbol
|
|
1213
|
+
* inside the path; otherwise there is nothing to say about who owns which half and the caller
|
|
1214
|
+
* keeps reporting the resource as unavailable rather than guessing.
|
|
1215
|
+
*/
|
|
1216
|
+
function declaredPartitionSkeleton({ conflict, concernAnchors, taskIds }) {
|
|
1217
|
+
const rootPath = conflict.target;
|
|
1218
|
+
const root = { kind: 'path', target: rootPath, path: rootPath };
|
|
1219
|
+
const nodes = [root];
|
|
1220
|
+
const partitions = [];
|
|
1221
|
+
for (const taskId of taskIds) {
|
|
1222
|
+
const owned = (concernAnchors.get(taskId) ?? [])
|
|
1223
|
+
.map((anchor) => {
|
|
1224
|
+
const separator = anchor.indexOf('\0');
|
|
1225
|
+
return {
|
|
1226
|
+
path: anchor.slice('concern:'.length, separator),
|
|
1227
|
+
name: anchor.slice(separator + 1),
|
|
1228
|
+
};
|
|
1229
|
+
})
|
|
1230
|
+
.filter(({ path }) => pathContains(rootPath, path));
|
|
1231
|
+
if (owned.length === 0) return null;
|
|
1232
|
+
for (const { path, name } of owned) nodes.push({ kind: 'symbol', target: name, path });
|
|
1233
|
+
partitions.push(owned.map(({ path, name }) => graphNodeKey({
|
|
1234
|
+
kind: 'symbol', target: name, path,
|
|
1235
|
+
})));
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
root,
|
|
1239
|
+
nodes,
|
|
1240
|
+
partitions: canonicalPartitions(partitions),
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1071
1244
|
/**
|
|
1072
1245
|
* Enumerate structural cut skeletons from the in-memory sensor outcomes. Graph edges only shape
|
|
1073
|
-
* SCC/closure/frontier partitions; task ownership is bound exclusively by unique witness anchors
|
|
1246
|
+
* SCC/closure/frontier partitions; task ownership is bound exclusively by unique witness anchors
|
|
1247
|
+
* and by declared concern anchors, never by the edges themselves.
|
|
1074
1248
|
*/
|
|
1075
1249
|
export function enumerateCutSkeletons({
|
|
1076
1250
|
component,
|
|
1077
1251
|
request,
|
|
1078
1252
|
evidence,
|
|
1079
1253
|
rawCollected,
|
|
1254
|
+
concernAnchors = new Map(),
|
|
1080
1255
|
} = {}) {
|
|
1081
1256
|
if (!plainRecord(component)
|
|
1082
1257
|
|| !Array.isArray(component.task_ids)
|
|
@@ -1087,7 +1262,9 @@ export function enumerateCutSkeletons({
|
|
|
1087
1262
|
}
|
|
1088
1263
|
const taskIds = [...component.task_ids].sort(compareText);
|
|
1089
1264
|
const intentAnchors = uniqueIntentAnchors(request.manual_witness, taskIds);
|
|
1090
|
-
const missingIntent = taskIds.filter((taskId) =>
|
|
1265
|
+
const missingIntent = taskIds.filter((taskId) => (
|
|
1266
|
+
intentAnchors.get(taskId).length === 0 && (concernAnchors.get(taskId) ?? []).length === 0
|
|
1267
|
+
));
|
|
1091
1268
|
if (missingIntent.length > 0) {
|
|
1092
1269
|
return {
|
|
1093
1270
|
skeletons: [],
|
|
@@ -1102,6 +1279,26 @@ export function enumerateCutSkeletons({
|
|
|
1102
1279
|
const skeletonByLayout = new Map();
|
|
1103
1280
|
const unknowns = [];
|
|
1104
1281
|
for (const conflict of component.conflicts) {
|
|
1282
|
+
if (conflict.kind === 'path') {
|
|
1283
|
+
const declared = declaredPartitionSkeleton({ conflict, concernAnchors, taskIds });
|
|
1284
|
+
if (declared === null || declared.partitions.length < 2) {
|
|
1285
|
+
unknowns.push({ kind: 'raw_graph_unavailable', ref: conflict.resource_id });
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
const layoutKey = digestArtifact({
|
|
1289
|
+
conflict_resource_id: conflict.resource_id,
|
|
1290
|
+
partitions: declared.partitions,
|
|
1291
|
+
});
|
|
1292
|
+
skeletonByLayout.set(layoutKey, {
|
|
1293
|
+
skeleton_id: `cut-${sha16(layoutKey)}`,
|
|
1294
|
+
conflict_resource_id: conflict.resource_id,
|
|
1295
|
+
cut_kinds: ['declared_partition'],
|
|
1296
|
+
root_surface: structuredClone(declared.root),
|
|
1297
|
+
partitions: declared.partitions,
|
|
1298
|
+
raw_graph: { nodes: structuredClone(declared.nodes), edges: [] },
|
|
1299
|
+
});
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1105
1302
|
if (conflict.kind !== 'symbol') {
|
|
1106
1303
|
unknowns.push({ kind: 'raw_graph_unavailable', ref: conflict.resource_id });
|
|
1107
1304
|
continue;
|
|
@@ -1154,6 +1351,7 @@ export function enumerateCutSkeletons({
|
|
|
1154
1351
|
skeleton,
|
|
1155
1352
|
graph: skeleton.raw_graph,
|
|
1156
1353
|
intentAnchors,
|
|
1354
|
+
concernAnchors,
|
|
1157
1355
|
taskIds,
|
|
1158
1356
|
}));
|
|
1159
1357
|
for (const skeleton of skeletons) unknowns.push(...skeleton.binding_unknowns);
|
|
@@ -1251,6 +1449,7 @@ export function evaluateSeamProposalCandidates({
|
|
|
1251
1449
|
evidence,
|
|
1252
1450
|
candidateSpecs,
|
|
1253
1451
|
explorationComplete = false,
|
|
1452
|
+
concernAnchors = new Map(),
|
|
1254
1453
|
} = {}) {
|
|
1255
1454
|
if (!plainRecord(component)
|
|
1256
1455
|
|| !Array.isArray(component.task_ids)
|
|
@@ -1265,7 +1464,9 @@ export function evaluateSeamProposalCandidates({
|
|
|
1265
1464
|
}
|
|
1266
1465
|
const taskSet = new Set(taskIds);
|
|
1267
1466
|
const intentAnchors = uniqueIntentAnchors(request.manual_witness, taskIds);
|
|
1268
|
-
const missingIntent = taskIds.filter((taskId) =>
|
|
1467
|
+
const missingIntent = taskIds.filter((taskId) => (
|
|
1468
|
+
intentAnchors.get(taskId).length === 0 && (concernAnchors.get(taskId) ?? []).length === 0
|
|
1469
|
+
));
|
|
1269
1470
|
if (missingIntent.length > 0) {
|
|
1270
1471
|
return decisionUnknown(component, missingIntent.map((taskId) => ({
|
|
1271
1472
|
kind: 'semantic_owner_binding_missing',
|
|
@@ -1556,15 +1757,20 @@ export function compileSeamProposalDecision({
|
|
|
1556
1757
|
sensorEvidence,
|
|
1557
1758
|
evidence,
|
|
1558
1759
|
rawCollected,
|
|
1760
|
+
concernAnchors = new Map(),
|
|
1761
|
+
concernUnknowns = [],
|
|
1559
1762
|
} = {}) {
|
|
1560
1763
|
const enumeration = enumerateCutSkeletons({
|
|
1561
1764
|
component,
|
|
1562
1765
|
request,
|
|
1563
1766
|
evidence,
|
|
1564
1767
|
rawCollected,
|
|
1768
|
+
concernAnchors,
|
|
1565
1769
|
});
|
|
1566
|
-
|
|
1567
|
-
|
|
1770
|
+
// 宣言が解決しなかった事実は、束縛が成功したかに関わらず記録から消さない。
|
|
1771
|
+
const unknowns = [...concernUnknowns, ...enumeration.unknowns];
|
|
1772
|
+
if (unknowns.length > 0) {
|
|
1773
|
+
return decisionUnknown(component, unknowns);
|
|
1568
1774
|
}
|
|
1569
1775
|
const candidateSpecs = enumeration.skeletons.map((skeleton) => (
|
|
1570
1776
|
skeletonCandidateSpec({ skeleton, component, request, evidence })
|
|
@@ -1582,6 +1788,7 @@ export function compileSeamProposalDecision({
|
|
|
1582
1788
|
evidence,
|
|
1583
1789
|
candidateSpecs,
|
|
1584
1790
|
explorationComplete: enumeration.exploration_complete,
|
|
1791
|
+
concernAnchors,
|
|
1585
1792
|
});
|
|
1586
1793
|
}
|
|
1587
1794
|
|
|
@@ -1730,13 +1937,25 @@ export function compileSeamProposalArtifact({
|
|
|
1730
1937
|
baseSha: independenceArtifact.base_sha,
|
|
1731
1938
|
requestId: `seam-proposal-${independenceArtifact.result_digest.slice(0, 24)}`,
|
|
1732
1939
|
});
|
|
1733
|
-
const
|
|
1940
|
+
const components = conflictComponents(independenceArtifact);
|
|
1941
|
+
const concern = resolveConcernAnchors({
|
|
1942
|
+
manualWitness: witnessSet.manual_witness,
|
|
1943
|
+
taskIds: [...new Set(components.flatMap(({ task_ids: ids }) => ids))].sort(compareText),
|
|
1944
|
+
evidence,
|
|
1945
|
+
});
|
|
1946
|
+
const decisions = components.map((component) => (
|
|
1734
1947
|
compileSeamProposalDecision({
|
|
1735
1948
|
component,
|
|
1736
1949
|
request,
|
|
1737
1950
|
sensorEvidence,
|
|
1738
1951
|
evidence,
|
|
1739
1952
|
rawCollected,
|
|
1953
|
+
concernAnchors: concern.anchorsByTask,
|
|
1954
|
+
// このcomponentのtaskに関する解決失敗だけを持ち込む。
|
|
1955
|
+
concernUnknowns: concern.unknowns.filter((unknown) => (
|
|
1956
|
+
component.task_ids.some((taskId) => unknown.ref.startsWith(`${taskId}:`)
|
|
1957
|
+
|| unknown.ref.split(':')[0].split(',').includes(taskId))
|
|
1958
|
+
)),
|
|
1740
1959
|
})
|
|
1741
1960
|
)).sort((left, right) => compareText(left.component_id, right.component_id));
|
|
1742
1961
|
const artifact = {
|
package/src/todo-cli.mjs
CHANGED
|
@@ -83,7 +83,7 @@ import {
|
|
|
83
83
|
SEAM_PROPOSAL_PROJECTION_SCHEMA,
|
|
84
84
|
validateSeamProposalProjection,
|
|
85
85
|
} from './seam-proposal-contracts.mjs';
|
|
86
|
-
import { compileSeamProposalArtifact } from './seam-proposal.mjs';
|
|
86
|
+
import { compileSeamProposalArtifact, declaredConcernSymbols } from './seam-proposal.mjs';
|
|
87
87
|
import {
|
|
88
88
|
parseTodoSourceRef, todoLegacyReconciliationDigest, validatePhaseTodoRevision,
|
|
89
89
|
validateTodoRevision, validateTodoRevisionSet,
|
|
@@ -893,6 +893,7 @@ async function seamProposalCompile({ repoRoot, planKey }) {
|
|
|
893
893
|
|
|
894
894
|
const { query_set: querySet } = buildSeamProposalQuerySet({
|
|
895
895
|
conflictResources: independenceArtifact.conflict_resources,
|
|
896
|
+
concernSymbols: declaredConcernSymbols(witnessSet.manual_witness),
|
|
896
897
|
});
|
|
897
898
|
const [sensorEvidence, proposalEvidence] = await Promise.all([
|
|
898
899
|
collectWitnessSensorEvidence({ cwd: repoRoot, witnessSet }),
|
|
@@ -1012,7 +1013,10 @@ async function seamProposal({ repoRoot, requestedPlanKey }) {
|
|
|
1012
1013
|
topology_digest: artifact?.source_binding.topology_digest ?? null,
|
|
1013
1014
|
independence_result_digest: artifact?.source_binding.independence_result_digest ?? null,
|
|
1014
1015
|
compiled_at: artifact?.compiled_at ?? null,
|
|
1015
|
-
guidance: selectSeamProposalGuidance({
|
|
1016
|
+
guidance: selectSeamProposalGuidance({
|
|
1017
|
+
coverage,
|
|
1018
|
+
unknownKinds: components.flatMap(({ unknowns }) => unknowns.map(({ kind }) => kind)),
|
|
1019
|
+
}),
|
|
1016
1020
|
component_count: artifact === null ? null : components.length,
|
|
1017
1021
|
conflict_resource_count: artifact === null ? null : components
|
|
1018
1022
|
.reduce((count, component) => count + component.conflicts.length, 0),
|
|
@@ -13,7 +13,19 @@ import {
|
|
|
13
13
|
} from './runtime-contracts.mjs';
|
|
14
14
|
import { TODO_INDEPENDENCE_GUIDANCE_CODES } from './todo-independence-guidance.mjs';
|
|
15
15
|
|
|
16
|
-
export const TODO_WITNESS_SET_SCHEMA = 'lattice.todo_witness_set.
|
|
16
|
+
export const TODO_WITNESS_SET_SCHEMA = 'lattice.todo_witness_set.v2';
|
|
17
|
+
/**
|
|
18
|
+
* まだ受理する旧witness set契約。v1はconcern anchorを持てないだけで、境界宣言としては
|
|
19
|
+
* v2と同値である。既存宣言を書き換えさせないために読み口を残す。
|
|
20
|
+
*/
|
|
21
|
+
export const TODO_WITNESS_SET_LEGACY_SCHEMAS = Object.freeze(['lattice.todo_witness_set.v1']);
|
|
22
|
+
export const TODO_WITNESS_SET_SCHEMAS = Object.freeze([
|
|
23
|
+
TODO_WITNESS_SET_SCHEMA,
|
|
24
|
+
...TODO_WITNESS_SET_LEGACY_SCHEMAS,
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/** 1 taskが宣言できるconcern anchorの資源数と、資源あたりのsymbol数の上限。 */
|
|
28
|
+
export const TODO_CONCERN_ANCHOR_LIMIT = 256;
|
|
17
29
|
export const TODO_INDEPENDENCE_SCHEMA = 'lattice.todo_independence.v3';
|
|
18
30
|
export const TODO_INDEPENDENCE_PROJECTION_SCHEMA = 'lattice.todo_independence_projection.v2';
|
|
19
31
|
export const TODO_INDEPENDENCE_LEGACY_MARKER_SCHEMA = 'lattice.todo_independence_legacy_marker.v1';
|
|
@@ -97,6 +109,10 @@ function boundedText(value, maximumBytes = 4_096) {
|
|
|
97
109
|
* manual witnessとsensor query setの判定正本は`explainRunRequest`だけが持つ(ADR 0123)。
|
|
98
110
|
* ここで同じ規則を書き直すと契約が二箇所へ分裂するため、検証もcompileもこの合成を通す。
|
|
99
111
|
* `requestId`はplan identityとwitness digestから導出され、run lifecycleへは登録されない。
|
|
112
|
+
*
|
|
113
|
+
* `concern_anchors`はここで落とす。並列可否の判定はこの合成requestだけを入力にするので、
|
|
114
|
+
* 落としておけばconcern宣言が判定へ影響しないことが構造で保証される(testの主張ではない)。
|
|
115
|
+
* 宣言はseam束縛の入力であり、witness setから直接読む。
|
|
100
116
|
*/
|
|
101
117
|
export function synthesizeWitnessRunRequest(witnessSet, { baseSha, requestId }) {
|
|
102
118
|
const taskIds = Object.keys(witnessSet.manual_witness).sort(compareText);
|
|
@@ -106,7 +122,10 @@ export function synthesizeWitnessRunRequest(witnessSet, { baseSha, requestId })
|
|
|
106
122
|
repo: { base_sha: baseSha, root_kind: 'git' },
|
|
107
123
|
capacity: witnessSet.capacity,
|
|
108
124
|
todos: taskIds.map((taskId) => ({ todo_id: taskId })),
|
|
109
|
-
manual_witness:
|
|
125
|
+
manual_witness: Object.fromEntries(taskIds.map((taskId) => {
|
|
126
|
+
const { concern_anchors: _concernAnchors, ...boundary } = witnessSet.manual_witness[taskId];
|
|
127
|
+
return [taskId, boundary];
|
|
128
|
+
})),
|
|
110
129
|
sensor_query_set: witnessSet.sensor_query_set,
|
|
111
130
|
executor_capability: { adapters: ['todo-independence'] },
|
|
112
131
|
claim_mode: RUN_REQUEST_CLAIM_MODE,
|
|
@@ -117,10 +136,57 @@ export function synthesizeWitnessRunRequest(witnessSet, { baseSha, requestId })
|
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
/**
|
|
120
|
-
*
|
|
139
|
+
* 1 taskのconcern anchor宣言を検査する。
|
|
140
|
+
*
|
|
141
|
+
* `within`はそのtask自身が`owns`で主張している資源に限る。所有していない資源の内側に
|
|
142
|
+
* 担当を主張させない。symbol名の実在・資源内包含・task間排他はsensorとcompile側が見る。
|
|
143
|
+
*/
|
|
144
|
+
function explainConcernAnchors(anchors, owns, at) {
|
|
145
|
+
const reject = (reason, path) => ({ valid: false, reason, path });
|
|
146
|
+
if (!Array.isArray(anchors) || anchors.length > TODO_CONCERN_ANCHOR_LIMIT) {
|
|
147
|
+
return reject('bounded_collection_violation', at);
|
|
148
|
+
}
|
|
149
|
+
const ownedKeys = new Set(owns.map((own) => `${own.kind}\0${own.target}`));
|
|
150
|
+
for (const [index, entry] of anchors.entries()) {
|
|
151
|
+
const entryAt = `${at}/${index}`;
|
|
152
|
+
if (!exactRecord(entry, ['within', 'symbols'])) {
|
|
153
|
+
return reject('unexpected_or_missing_keys', entryAt);
|
|
154
|
+
}
|
|
155
|
+
if (!exactRecord(entry.within, ['kind', 'target'])
|
|
156
|
+
|| !['symbol', 'path'].includes(entry.within.kind)) {
|
|
157
|
+
return reject('invalid_concern_anchor_resource', `${entryAt}/within`);
|
|
158
|
+
}
|
|
159
|
+
const targetValid = entry.within.kind === 'path'
|
|
160
|
+
? repoRelativeResourceTarget(entry.within.target)
|
|
161
|
+
: boundedText(entry.within.target, 1_024);
|
|
162
|
+
if (!targetValid) return reject('invalid_concern_anchor_resource', `${entryAt}/within`);
|
|
163
|
+
if (!ownedKeys.has(`${entry.within.kind}\0${entry.within.target}`)) {
|
|
164
|
+
return reject('concern_anchor_resource_not_owned', `${entryAt}/within`);
|
|
165
|
+
}
|
|
166
|
+
if (!Array.isArray(entry.symbols) || entry.symbols.length < 1
|
|
167
|
+
|| entry.symbols.length > TODO_CONCERN_ANCHOR_LIMIT) {
|
|
168
|
+
return reject('bounded_collection_violation', `${entryAt}/symbols`);
|
|
169
|
+
}
|
|
170
|
+
if (!entry.symbols.every((symbol) => boundedText(symbol, 1_024))) {
|
|
171
|
+
return reject('invalid_concern_anchor_symbol', `${entryAt}/symbols`);
|
|
172
|
+
}
|
|
173
|
+
if (!strictlySorted(entry.symbols)) {
|
|
174
|
+
return reject('unsorted_or_duplicate_collection', `${entryAt}/symbols`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!strictlySorted(anchors, (entry) => `${entry.within.kind}\0${entry.within.target}`)) {
|
|
178
|
+
return reject('unsorted_or_duplicate_collection', at);
|
|
179
|
+
}
|
|
180
|
+
return { valid: true };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `lattice.todo_witness_set.v2`(およびconcern anchorを持たない旧v1)を検証し、
|
|
185
|
+
* 拒否理由とpathを返す。
|
|
121
186
|
*
|
|
122
187
|
* 自分のshapeだけをここで見て、witness本体はprobe requestへ合成して
|
|
123
188
|
* `explainRunRequest`へ委譲する。probeのbase_shaは検証専用の定数であり永続化しない。
|
|
189
|
+
* `concern_anchors`はprobeから落ちるので、ここだけが判定正本になる。
|
|
124
190
|
*/
|
|
125
191
|
export function explainTodoWitnessSet(value) {
|
|
126
192
|
const reject = (reason, at = '') => ({ valid: false, reason, path: at });
|
|
@@ -129,7 +195,7 @@ export function explainTodoWitnessSet(value) {
|
|
|
129
195
|
'schema', 'project_id', 'plan_key', 'capacity', 'sensor_query_set',
|
|
130
196
|
'manual_witness', 'witness_set_digest',
|
|
131
197
|
])) return reject('unexpected_or_missing_top_level_keys');
|
|
132
|
-
if (value.schema
|
|
198
|
+
if (!TODO_WITNESS_SET_SCHEMAS.includes(value.schema)) return reject('schema_mismatch', '/schema');
|
|
133
199
|
if (!isTodoIdentifier(value.project_id)) return reject('invalid_identifier', '/project_id');
|
|
134
200
|
if (!isTodoIdentifier(value.plan_key)) return reject('invalid_identifier', '/plan_key');
|
|
135
201
|
if (!plain(value.manual_witness)) return reject('not_an_object', '/manual_witness');
|
|
@@ -146,6 +212,16 @@ export function explainTodoWitnessSet(value) {
|
|
|
146
212
|
});
|
|
147
213
|
const explained = explainRunRequest(probe);
|
|
148
214
|
if (!explained.valid) return reject(explained.reason, explained.path);
|
|
215
|
+
// concern anchorはprobeへ写していないので、ここが唯一の判定正本になる。
|
|
216
|
+
const legacy = TODO_WITNESS_SET_LEGACY_SCHEMAS.includes(value.schema);
|
|
217
|
+
for (const taskId of taskIds) {
|
|
218
|
+
const witness = value.manual_witness[taskId];
|
|
219
|
+
const at = `/manual_witness/${taskId}/concern_anchors`;
|
|
220
|
+
if (!Object.hasOwn(witness, 'concern_anchors')) continue;
|
|
221
|
+
if (legacy) return reject('concern_anchors_require_witness_set_v2', at);
|
|
222
|
+
const anchors = explainConcernAnchors(witness.concern_anchors, witness.owns, at);
|
|
223
|
+
if (!anchors.valid) return reject(anchors.reason, anchors.path);
|
|
224
|
+
}
|
|
149
225
|
return { valid: true };
|
|
150
226
|
} catch {
|
|
151
227
|
return reject('non_canonical_witness_set_bytes');
|
|
@@ -24,9 +24,39 @@ export const SEAM_PROPOSAL_GUIDANCE_CODES = Object.freeze([
|
|
|
24
24
|
'seam_proposal_unrecorded',
|
|
25
25
|
'seam_proposal_superseded',
|
|
26
26
|
'seam_proposal_stale',
|
|
27
|
+
'seam_proposal_binding_overlap',
|
|
28
|
+
'seam_proposal_binding_outside_resource',
|
|
29
|
+
'seam_proposal_binding_symbol_unresolved',
|
|
30
|
+
'seam_proposal_binding_resource_unresolved',
|
|
31
|
+
'seam_proposal_binding_ambiguous',
|
|
32
|
+
'seam_proposal_binding_missing',
|
|
27
33
|
'seam_proposal_verified',
|
|
28
34
|
]);
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 束縛失敗のunknown種別から案内codeを引く表。並びは優先順で、先頭ほど次の一歩が具体的である。
|
|
38
|
+
*
|
|
39
|
+
* 宣言が壊れている状況を、宣言が無い状況より上に置く。壊れている方は直す対象が
|
|
40
|
+
* 一意に決まるのに対し、無い方は何をどう宣言するかから決めることになるからである。
|
|
41
|
+
*/
|
|
42
|
+
const SEAM_PROPOSAL_BINDING_GUIDANCE = Object.freeze([
|
|
43
|
+
Object.freeze(['concern_anchor_overlap', 'seam_proposal_binding_overlap']),
|
|
44
|
+
Object.freeze(['concern_anchor_outside_resource', 'seam_proposal_binding_outside_resource']),
|
|
45
|
+
Object.freeze(['concern_anchor_unresolved', 'seam_proposal_binding_symbol_unresolved']),
|
|
46
|
+
Object.freeze(['concern_anchor_resource_unresolved', 'seam_proposal_binding_resource_unresolved']),
|
|
47
|
+
Object.freeze(['semantic_owner_binding_ambiguous', 'seam_proposal_binding_ambiguous']),
|
|
48
|
+
Object.freeze(['semantic_owner_binding_missing', 'seam_proposal_binding_missing']),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 束縛の案内へ共通で添える、記録が更新される条件。
|
|
53
|
+
*
|
|
54
|
+
* concern_anchorsはwitness setにあり、seam提案はindependence記録のwitness_set_digestと
|
|
55
|
+
* 一致する宣言しか読まない。宣言を直しただけでは提案は変わらないので、そこまで述べないと
|
|
56
|
+
* 「直したのに同じunknownが出る」で止まる。
|
|
57
|
+
*/
|
|
58
|
+
const BINDING_RECOMPILE_HINT = '宣言はwitness setにあり、independence compileとseam-proposal compileを通し直すまで提案へ写らない。';
|
|
59
|
+
|
|
30
60
|
const CATALOG = Object.freeze({
|
|
31
61
|
independence_no_ready_frontier: Object.freeze({
|
|
32
62
|
message: '着手候補が無いため、並列可否を述べる対象が無い。',
|
|
@@ -76,6 +106,30 @@ const CATALOG = Object.freeze({
|
|
|
76
106
|
message: 'seam提案の生成後にHEADが進み、記録時点の構造証拠は現在のcodeを指していない。',
|
|
77
107
|
next_action: 'compile_seam_proposal',
|
|
78
108
|
}),
|
|
109
|
+
seam_proposal_binding_overlap: Object.freeze({
|
|
110
|
+
message: `同じ資源について、複数のToDoが同じsymbolを自分の担当として宣言している。どちら側へ切るか決まらないため、切断候補を束縛できない。${BINDING_RECOMPILE_HINT}`,
|
|
111
|
+
next_action: 'split_overlapping_concern_anchors_then_recompile',
|
|
112
|
+
}),
|
|
113
|
+
seam_proposal_binding_outside_resource: Object.freeze({
|
|
114
|
+
message: `concern_anchorsが挙げたsymbolが、withinで指した資源の外にある。その資源の分割を説明しないので束縛へ使えない。${BINDING_RECOMPILE_HINT}`,
|
|
115
|
+
next_action: 'correct_concern_anchors_then_recompile',
|
|
116
|
+
}),
|
|
117
|
+
seam_proposal_binding_symbol_unresolved: Object.freeze({
|
|
118
|
+
message: `concern_anchorsが挙げたsymbolを、sensorが同名同pathで解決できなかった。近い別symbolへは寄せないため、束縛は成立していない。${BINDING_RECOMPILE_HINT}`,
|
|
119
|
+
next_action: 'correct_concern_anchors_then_recompile',
|
|
120
|
+
}),
|
|
121
|
+
seam_proposal_binding_resource_unresolved: Object.freeze({
|
|
122
|
+
message: `concern_anchorsのwithinが指す資源をsensorで解決できなかった。宣言がどの資源についてのものか確定しない。${BINDING_RECOMPILE_HINT}`,
|
|
123
|
+
next_action: 'correct_concern_anchors_then_recompile',
|
|
124
|
+
}),
|
|
125
|
+
seam_proposal_binding_ambiguous: Object.freeze({
|
|
126
|
+
message: `宣言したsymbolが切断候補の複数側へ当たるため、どのToDoがどちらを所有するか一意に決まらない。${BINDING_RECOMPILE_HINT}`,
|
|
127
|
+
next_action: 'narrow_concern_anchors_then_recompile',
|
|
128
|
+
}),
|
|
129
|
+
seam_proposal_binding_missing: Object.freeze({
|
|
130
|
+
message: `係争資源の中でどのToDoが何を触るかが宣言されていないため、切断候補を所有者へ束縛できない。依存線でも呼び出し辺でも所有は決まらない。${BINDING_RECOMPILE_HINT}`,
|
|
131
|
+
next_action: 'declare_concern_anchors_then_recompile',
|
|
132
|
+
}),
|
|
79
133
|
seam_proposal_verified: Object.freeze({
|
|
80
134
|
message: 'seam提案の記録は現在のplan、並列可否記録、HEADと一致している。',
|
|
81
135
|
next_action: 'none',
|
|
@@ -135,21 +189,37 @@ export function selectIndependenceGuidance({
|
|
|
135
189
|
return todoIndependenceGuidance('independence_verified');
|
|
136
190
|
}
|
|
137
191
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
192
|
+
/**
|
|
193
|
+
* seam提案の状況から案内codeを引く。投影の生成側と契約の検査側が同じ規則を読むための正本。
|
|
194
|
+
*
|
|
195
|
+
* 鮮度(coverage)を束縛失敗より先に見る。記録が古い・別topologyについてのものである時、
|
|
196
|
+
* そこに載っているunknownは現在のcodeについての事実ではないため、先に再compileが要る。
|
|
197
|
+
* 記録が現在と一致している時だけ、束縛失敗が「今直せる状況」になる。
|
|
198
|
+
*/
|
|
199
|
+
export function seamProposalGuidanceCode({ coverage, unknownKinds = [] }) {
|
|
200
|
+
if (coverage === 'missing') return 'seam_proposal_unrecorded';
|
|
201
|
+
if (coverage === 'superseded') return 'seam_proposal_superseded';
|
|
202
|
+
if (coverage === 'stale') return 'seam_proposal_stale';
|
|
203
|
+
if (coverage !== 'verified') throw new TypeError(`unknown seam proposal coverage: ${coverage}`);
|
|
204
|
+
const kinds = new Set(unknownKinds);
|
|
205
|
+
const binding = SEAM_PROPOSAL_BINDING_GUIDANCE.find(([kind]) => kinds.has(kind));
|
|
206
|
+
return binding === undefined ? 'seam_proposal_verified' : binding[1];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function selectSeamProposalGuidance({ coverage, unknownKinds = [] }) {
|
|
210
|
+
return todoIndependenceGuidance(seamProposalGuidanceCode({ coverage, unknownKinds }));
|
|
145
211
|
}
|
|
146
212
|
|
|
147
213
|
/**
|
|
148
214
|
* 宣言からcompileを経て読むまでの順序。helpとMCP instructionsが同じ手順を語るための正本。
|
|
149
215
|
* 面ごとに手順を書き直すと、片方だけが古くなる。
|
|
216
|
+
*
|
|
217
|
+
* 宣言できる欄を挙げる面はここだけなので、witness契約へ欄を足したらここへも足す。
|
|
218
|
+
* 足さないと、能力はあるのに機械が黙る面が残る(ADR 0130)。
|
|
150
219
|
*/
|
|
151
220
|
export const TODO_INDEPENDENCE_WORKFLOW = Object.freeze([
|
|
152
221
|
'1. 宣言する: .lattice/todo/witness/<plan_key>.json へ、ToDoごとのowns/reads/writes/affected_testsを書く',
|
|
222
|
+
' 係争資源しか所有していないToDoは、その資源の中で自分が触るsymbolをconcern_anchorsへ宣言できる(witness set v2)。並列可否の判定には写らず、切断候補の束縛だけに効く',
|
|
153
223
|
'2. 判定する: lattice todo independence compile --plan <key> --input <ref>(実sensorを引き、clean worktreeが要る)',
|
|
154
224
|
'3. 読む: lattice todo independence --plan <key> --json(sensorを引かず、記録とHEAD照合だけで返る)',
|
|
155
225
|
'4. 追従する: plan改訂後は lattice todo independence witness migrate --plan <key> で宣言を写してから再compileする',
|