@quolu/lattice 0.32.0 → 0.34.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/README.ja.md +11 -1
- package/README.md +11 -2
- package/bin/lattice.mjs +12 -1
- package/package.json +1 -1
- package/sensor/dist/bin/lattice-sensor.js +72 -4
- package/sensor/dist/db/migrations.d.ts +1 -1
- package/sensor/dist/db/migrations.d.ts.map +1 -1
- package/sensor/dist/db/migrations.js +42 -2
- package/sensor/dist/db/migrations.js.map +1 -1
- package/sensor/dist/db/queries.d.ts +18 -0
- package/sensor/dist/db/queries.d.ts.map +1 -1
- package/sensor/dist/db/queries.js +76 -9
- package/sensor/dist/db/queries.js.map +1 -1
- package/sensor/dist/db/schema.sql +10 -1
- package/sensor/dist/extraction/extraction-version.d.ts +1 -1
- package/sensor/dist/extraction/extraction-version.d.ts.map +1 -1
- package/sensor/dist/extraction/extraction-version.js +5 -1
- package/sensor/dist/extraction/extraction-version.js.map +1 -1
- package/sensor/dist/extraction/index.d.ts +2 -0
- package/sensor/dist/extraction/index.d.ts.map +1 -1
- package/sensor/dist/extraction/index.js +38 -3
- package/sensor/dist/extraction/index.js.map +1 -1
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +89 -16
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/sensor/dist/index.d.ts +8 -1
- package/sensor/dist/index.d.ts.map +1 -1
- package/sensor/dist/index.js +9 -0
- package/sensor/dist/index.js.map +1 -1
- package/sensor/dist/resolution/import-resolver.d.ts +11 -0
- package/sensor/dist/resolution/import-resolver.d.ts.map +1 -1
- package/sensor/dist/resolution/import-resolver.js +13 -5
- package/sensor/dist/resolution/import-resolver.js.map +1 -1
- package/sensor/dist/resolution/index.d.ts.map +1 -1
- package/sensor/dist/resolution/index.js +11 -0
- package/sensor/dist/resolution/index.js.map +1 -1
- package/sensor/dist/resolution/types.d.ts +4 -0
- package/sensor/dist/resolution/types.d.ts.map +1 -1
- package/sensor/dist/types.d.ts +22 -0
- package/sensor/dist/types.d.ts.map +1 -1
- package/src/artifact-contracts.mjs +3 -0
- package/src/cli-help.mjs +6 -0
- package/src/rc3-scripted-campaign.mjs +3 -1
- package/src/runtime-cli.mjs +280 -36
- package/src/runtime-contracts.mjs +1 -1
- package/src/runtime-control-store.mjs +24 -1
- package/src/runtime-decision-verifier.mjs +1 -1
- package/src/runtime-diff-observer.mjs +2 -2
- package/src/runtime-front-end.mjs +47 -13
- package/src/runtime-hold-recompile.mjs +31 -2
- package/src/runtime-io-sentinel.mjs +16 -9
- package/src/runtime-managed-supervisor.mjs +32 -3
- package/src/runtime-multi-epoch-store.mjs +2 -2
- package/src/runtime-projection.mjs +27 -0
- package/src/runtime-scripted-adapter-controller.mjs +22 -1
- package/src/runtime-seam-resolve.mjs +156 -13
- package/src/runtime-seam-treatment.mjs +2 -1
- package/src/seam-apply.mjs +101 -4
- package/src/seam-cost.mjs +322 -0
- package/src/seam-gate.mjs +146 -0
- package/src/seam-rewrite.mjs +102 -16
- package/src/seam-verification.mjs +26 -3
- package/src/sensor-adapter.mjs +6 -1
- package/src/todo-cli.mjs +55 -0
|
@@ -28,7 +28,7 @@ import { selfDigest } from './runtime-contracts.mjs';
|
|
|
28
28
|
import { coveredBy } from './runtime-diff-observer.mjs';
|
|
29
29
|
|
|
30
30
|
/** 警報の種別。findingのkindとは別空間にする——findingへ昇格するのはprobeを通った後だけである。 */
|
|
31
|
-
export const IO_WARNING_KINDS = Object.freeze(['io_overlap_warning', '
|
|
31
|
+
export const IO_WARNING_KINDS = Object.freeze(['io_overlap_warning', 'io_undeclared_write_warning']);
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* 監視から外すrepo相対prefix。
|
|
@@ -65,7 +65,7 @@ export function isExcludedPath(relativePath, excludes = DEFAULT_IO_EXCLUDES) {
|
|
|
65
65
|
*
|
|
66
66
|
* checkpoint findingの2述語をそのまま1 pathへ適用する:
|
|
67
67
|
* - 他のrunning TODOの宣言scopeに入るpathへ書いた → `io_overlap_warning`
|
|
68
|
-
* - 自分の宣言scopeの外へ書いた → `
|
|
68
|
+
* - 自分の宣言scopeの外へ書いた → `io_undeclared_write_warning`
|
|
69
69
|
*
|
|
70
70
|
* @returns {{warnings: Array<{kind: string, todo_ids: string[], path: string}>}}
|
|
71
71
|
*/
|
|
@@ -84,7 +84,7 @@ export function classifyIoObservation(options = {}) {
|
|
|
84
84
|
|
|
85
85
|
const warnings = [];
|
|
86
86
|
if (!coveredBy(packet.scope.writes, relativePath)) {
|
|
87
|
-
warnings.push({ kind: '
|
|
87
|
+
warnings.push({ kind: 'io_undeclared_write_warning', todo_ids: [todoId], path: relativePath });
|
|
88
88
|
}
|
|
89
89
|
for (const otherId of [...runningTodoIds].sort(compareText)) {
|
|
90
90
|
if (otherId === todoId) continue;
|
|
@@ -298,15 +298,22 @@ export function probeIoWarning({ warning, checkpointsByTodo } = {}) {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/**
|
|
301
|
-
* 警報kind → finding kind
|
|
301
|
+
* 警報kind → finding kind。**重なりだけがhold経路へ乗る。**
|
|
302
302
|
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
303
|
+
* 宣言境界は計画時の**予測**であって、workerを閉じ込める制約ではない。範囲内へ無理に
|
|
304
|
+
* 押し込めるとworkerの自由度が落ち、成果の品質が下がる。だから自由に書かせ、**実際の足跡が
|
|
305
|
+
* 他の走行中TODOとぶつかった時にだけ**止めて処置する——請求項7(片方を停止し他方を確定して
|
|
306
|
+
* 再開する)と請求項8(限定的な変換を施して双方再開する)はそのための構成である。
|
|
307
|
+
*
|
|
308
|
+
* よって単独のscope警報——誰の領分とも重なっていない宣言外の書き込み——はhold経路へ運ばない。
|
|
309
|
+
* それは競合ではなく、**予測が実態より狭かったという情報**であり、止める理由が無い。記録は
|
|
310
|
+
* 残る(`io_warning_observed`)ので、再計画の材料としては失われない。
|
|
311
|
+
*
|
|
312
|
+
* 止めるべきでないものを止めると、処置の当てようが無い停止が生まれる。scope違反に処置が
|
|
313
|
+
* 無いのは欠落ではなく、処置すべき事象ではないことの現れである。
|
|
306
314
|
*/
|
|
307
315
|
const WARNING_FINDING_KIND = Object.freeze({
|
|
308
316
|
io_overlap_warning: 'observed_write_conflict',
|
|
309
|
-
io_scope_warning: 'scope_violation',
|
|
310
317
|
});
|
|
311
318
|
|
|
312
319
|
/**
|
|
@@ -320,7 +327,7 @@ function selectEscalationAnchor({ warning, writers, packets }) {
|
|
|
320
327
|
const wrote = new Set(writers);
|
|
321
328
|
const qualified = [...warning.todo_ids].sort(compareText).filter((todoId) => {
|
|
322
329
|
if (!wrote.has(todoId)) return false;
|
|
323
|
-
if (warning.kind === '
|
|
330
|
+
if (warning.kind === 'io_undeclared_write_warning') return true;
|
|
324
331
|
return warning.todo_ids.some((otherId) => {
|
|
325
332
|
if (otherId === todoId) return false;
|
|
326
333
|
const other = packets[otherId];
|
|
@@ -92,6 +92,28 @@ export async function observeMacosBinaryIdentity(binaryPath) {
|
|
|
92
92
|
return identity;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* 起動済みprocessが実際に実行しているimageのcanonical pathをOSから観測する。
|
|
97
|
+
* bytesの同一性はcallerがlaunch descriptorのSHA-256と照合する。
|
|
98
|
+
*/
|
|
99
|
+
export async function observeProcessExecutablePath(pid) {
|
|
100
|
+
if (!Number.isSafeInteger(pid) || pid < 1) {
|
|
101
|
+
fail('ADAPTER_BINARY_IDENTITY_MISMATCH', 'exec後image PID不正');
|
|
102
|
+
}
|
|
103
|
+
if (process.platform === 'linux') {
|
|
104
|
+
return realpath(`/proc/${pid}/exe`);
|
|
105
|
+
}
|
|
106
|
+
if (process.platform === 'darwin') {
|
|
107
|
+
const { stdout } = await execFileAsync(
|
|
108
|
+
'/bin/ps',
|
|
109
|
+
['-o', 'comm=', '-p', String(pid)],
|
|
110
|
+
{ encoding: 'utf8' },
|
|
111
|
+
);
|
|
112
|
+
return realpath(stdout.trim());
|
|
113
|
+
}
|
|
114
|
+
fail('ADAPTER_BINARY_IDENTITY_MISMATCH', `exec後image path観測未対応platform: ${process.platform}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
95
117
|
/** storeが解決したimmutable bindingからprocess/worktree/checkpointをDirect OSで再観測する。 */
|
|
96
118
|
/**
|
|
97
119
|
* 本repositoryの現在状態を1つのdigestへ畳む。
|
|
@@ -862,8 +884,7 @@ async function activateManagedSupervisorController({ repoRoot, runDir, runId, ad
|
|
|
862
884
|
try { process.kill(child.pid, 0); } catch { fail('ADAPTER_CONTROLLER_UNAVAILABLE', 'controller process不達'); }
|
|
863
885
|
let executedImage;
|
|
864
886
|
try {
|
|
865
|
-
|
|
866
|
-
executedImage = await realpath(stdout.trim());
|
|
887
|
+
executedImage = await observeProcessExecutablePath(child.pid);
|
|
867
888
|
} catch { fail('ADAPTER_BINARY_IDENTITY_MISMATCH', 'exec後image path観測失敗'); }
|
|
868
889
|
if (executedImage !== binaryReal || sha256Bytes(await readFile(executedImage)) !== launch.binary_digest) {
|
|
869
890
|
fail('ADAPTER_BINARY_IDENTITY_MISMATCH', 'exec後image path/bytes不一致');
|
|
@@ -1340,7 +1361,15 @@ if (isDirectDaemon) {
|
|
|
1340
1361
|
commitPhaseRevision: (revision) => applyPhaseTodoRevision({
|
|
1341
1362
|
repoRoot: path.resolve(runDir, '..', '..', '..'),
|
|
1342
1363
|
writer: createTodoStoreWriter({ caller: 'g5-authoring' }), revision,
|
|
1343
|
-
actor
|
|
1364
|
+
// actorは`{host, session, agent}`のexact recordである(`lattice.todo_event.v4`)。
|
|
1365
|
+
// 文字列を渡していた間、seam_splitの再計画はphase revisionをcommitできなかった
|
|
1366
|
+
// ——工程storeへ書く直前で必ず落ちるので、請求項8の再開まで一度も届いていない。
|
|
1367
|
+
actor: {
|
|
1368
|
+
host: 'lattice-runtime',
|
|
1369
|
+
session: sessionNonce.slice(0, 32),
|
|
1370
|
+
agent: 'lattice-runtime-supervisor',
|
|
1371
|
+
},
|
|
1372
|
+
recordedAt: new Date().toISOString(),
|
|
1344
1373
|
}),
|
|
1345
1374
|
});
|
|
1346
1375
|
})
|
|
@@ -83,7 +83,7 @@ export function validateRuntimeFindingRecord(value) {
|
|
|
83
83
|
|| new Set(finding.evidence_digests).size !== finding.evidence_digests.length
|
|
84
84
|
|| finding.evidence_digests.some((entry, index) => index > 0 && finding.evidence_digests[index - 1] >= entry)
|
|
85
85
|
|| !selfDigestValid(finding, 'finding_digest')) return false;
|
|
86
|
-
const pathKind = ['observed_write_conflict', '
|
|
86
|
+
const pathKind = ['observed_write_conflict', 'undeclared_write', 'stale_context'].includes(finding.kind);
|
|
87
87
|
if (pathKind !== (finding.path !== null) || pathKind === (finding.resource_id !== null)) return false;
|
|
88
88
|
const observer = value.recorded_by;
|
|
89
89
|
return exactRecord(observer, ['schema', 'kind', 'controller_registration_digest',
|
|
@@ -109,7 +109,7 @@ export function validateRuntimeFindingCandidate(value) {
|
|
|
109
109
|
|| value.evidence_digests.some((digest, index) => index > 0
|
|
110
110
|
&& value.evidence_digests[index - 1] >= digest)
|
|
111
111
|
|| !selfDigestValid(value, 'candidate_digest')) return false;
|
|
112
|
-
const pathKind = ['observed_write_conflict', '
|
|
112
|
+
const pathKind = ['observed_write_conflict', 'undeclared_write', 'stale_context']
|
|
113
113
|
.includes(value.proposed_kind);
|
|
114
114
|
return pathKind
|
|
115
115
|
? typeof value.path === 'string' && value.path.length > 0 && value.resource_id === null
|
|
@@ -238,5 +238,32 @@ export function projectRuntimeStatusOverlays(options = {}) {
|
|
|
238
238
|
carry_over: members('carry_over'),
|
|
239
239
|
redispatch: members('redispatch'),
|
|
240
240
|
intake_frozen: runtimeState.freeze !== null,
|
|
241
|
+
treatment_advice: treatmentAdvice(events),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 止まっているrunに対して、請求項8(変換)を試せるかを助言する。
|
|
247
|
+
*
|
|
248
|
+
* 拒否ではなく助言である(ADR 0128と同じ規律)。既定の再計画modeが`intentional_serial`だから
|
|
249
|
+
* といって「変換が不可能」ではない——事前宣言が無いだけである。実行時に初めて見つかった競合には
|
|
250
|
+
* 事前宣言が無いので、既定だけを見ると請求項8へ行く道が無いように見える。
|
|
251
|
+
*
|
|
252
|
+
* 装置が言えるのは**係争資源が切れる種類か**までとする。共有stateや外部effectは面を分けても
|
|
253
|
+
* 同じ資源に触り続けるので切れない。pathは分けられる**かもしれない**——実際に切れるかと、
|
|
254
|
+
* それが割に合うかは、隔離worktreeで変換して五条件で測って初めて分かる(`run seam resolve`)。
|
|
255
|
+
* 試すかどうかは費用のかかる判断なので、装置は決めずに材料だけ渡す。
|
|
256
|
+
*/
|
|
257
|
+
function treatmentAdvice(events) {
|
|
258
|
+
const conflict = [...events].reverse()
|
|
259
|
+
.find((event) => event.kind === 'conflict_found'
|
|
260
|
+
&& event.payload !== null && typeof event.payload === 'object');
|
|
261
|
+
if (conflict === undefined) return null;
|
|
262
|
+
const severability = typeof conflict.payload.path === 'string' ? 'code_seam' : 'serial';
|
|
263
|
+
return {
|
|
264
|
+
finding_digest: typeof conflict.payload.finding_digest === 'string'
|
|
265
|
+
? conflict.payload.finding_digest : null,
|
|
266
|
+
severability,
|
|
267
|
+
transform_attemptable: severability === 'code_seam',
|
|
241
268
|
};
|
|
242
269
|
}
|
|
@@ -511,6 +511,21 @@ export async function createScriptedAdapterController({
|
|
|
511
511
|
let registrationDigest = null;
|
|
512
512
|
|
|
513
513
|
/** dispatch応答が運ぶworker process。誰を止めればよいかをsupervisorへ名指しする。 */
|
|
514
|
+
/**
|
|
515
|
+
* 起こしたworker processを畳む。
|
|
516
|
+
*
|
|
517
|
+
* **SIGKILLでなければ届かない。** barrierで止めたworkerはSIGSTOP状態にあり、
|
|
518
|
+
* SIGTERMは継続されるまで配送されない。放置すると、runもcontrollerもrepoも消えた後まで
|
|
519
|
+
* 停止したままのprocessが残り続ける(自分では終われない)。
|
|
520
|
+
*/
|
|
521
|
+
const reapWorkers = () => {
|
|
522
|
+
for (const task of tasks.values()) {
|
|
523
|
+
const pid = task.worker?.pid;
|
|
524
|
+
if (!Number.isSafeInteger(pid)) continue;
|
|
525
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* 既に終わっている */ }
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
514
529
|
const workerProcessOf = (task) => ({
|
|
515
530
|
pid: task.worker.pid,
|
|
516
531
|
process_group_id: task.worker.process_group_id,
|
|
@@ -904,7 +919,9 @@ export async function createScriptedAdapterController({
|
|
|
904
919
|
worktree_id: rebind.worktree_id,
|
|
905
920
|
predecessor_epoch: rebind.new_plan_epoch - 1,
|
|
906
921
|
successor_epoch: rebind.new_plan_epoch,
|
|
907
|
-
|
|
922
|
+
// **holdされたworkerにreceiptは無い。** 作業を終えていないから止められているので、
|
|
923
|
+
// 完了の記録を前提にできない。前任packetのdigestはdispatch時のpacketが持っている。
|
|
924
|
+
predecessor_packet_digest: task.packet.packet_digest,
|
|
908
925
|
rebind_packet_digest: rebind.packet_digest,
|
|
909
926
|
new_write_lease_id: staged.lease_id,
|
|
910
927
|
supervisor_session_nonce_digest: sessionNonceDigest,
|
|
@@ -947,6 +964,7 @@ export async function createScriptedAdapterController({
|
|
|
947
964
|
controllerSessionNonce,
|
|
948
965
|
descriptor: structuredClone(descriptor),
|
|
949
966
|
heartbeat,
|
|
967
|
+
reapWorkers,
|
|
950
968
|
leaseSetDigest() {
|
|
951
969
|
return digestArtifact([...stagedLeases.keys(), ...armedLeases.keys()].sort());
|
|
952
970
|
},
|
|
@@ -1102,6 +1120,9 @@ export async function runScriptedAdapterController({
|
|
|
1102
1120
|
const close = async () => {
|
|
1103
1121
|
if (closed) return;
|
|
1104
1122
|
closed = true;
|
|
1123
|
+
// 起こしたworkerを道連れにする。停止中のworkerは自分では終われないので、
|
|
1124
|
+
// ここで畳まないとrunもrepoも消えた後まで残り続ける。
|
|
1125
|
+
controller.reapWorkers();
|
|
1105
1126
|
clearInterval(heartbeatTimer);
|
|
1106
1127
|
persistentSocket?.destroy();
|
|
1107
1128
|
await new Promise((resolve) => server.close(resolve));
|
|
@@ -19,11 +19,16 @@ import { digestArtifact } from './artifact-contracts.mjs';
|
|
|
19
19
|
import { commitSeamTransform } from './seam-commit.mjs';
|
|
20
20
|
import { applySeamConflict } from './seam-apply.mjs';
|
|
21
21
|
import { resolveRuntimeSeamTreatment } from './runtime-seam-treatment.mjs';
|
|
22
|
+
import { affectedTestsFromEvidence } from './runtime-front-end.mjs';
|
|
23
|
+
import { explainSeamGate } from './seam-gate.mjs';
|
|
22
24
|
import { collectWitnessSensorEvidence, compileTodoIndependence } from './todo-independence.mjs';
|
|
23
25
|
import { todoSelfDigest } from './todo-contracts.mjs';
|
|
24
26
|
|
|
25
27
|
export const RUNTIME_SEAM_REQUEST_SCHEMA = 'lattice.runtime_seam_request.v1';
|
|
26
|
-
|
|
28
|
+
// v2は翻訳段(`reconciled`)の追加である。宣言を観測へ合わせてから判定するようになったので、
|
|
29
|
+
// どの宣言の上で五条件を見たかが決着の一部になった。v1の形のまま中身を変えると、記録が何に
|
|
30
|
+
// ついてのものか確定しなくなる。
|
|
31
|
+
export const RUNTIME_SEAM_RESOLUTION_SCHEMA = 'lattice.runtime_seam_resolution.v2';
|
|
27
32
|
|
|
28
33
|
/** 合成するtodo planのkey。実行時planとは別空間なので固定でよい。 */
|
|
29
34
|
const SYNTHETIC_PLAN_KEY = 'runtime';
|
|
@@ -77,21 +82,130 @@ export function validateRuntimeSeamRequest(value) {
|
|
|
77
82
|
return value.request_digest === todoSelfDigest(value, 'request_digest');
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
/**
|
|
86
|
+
* 観測された実態へ宣言を合わせる(翻訳段)。
|
|
87
|
+
*
|
|
88
|
+
* 実行時のpath競合は、**片方がその資源を所有していないから起きる**。所有していない資源の内側に
|
|
89
|
+
* 担当は主張できない(`concern_anchor_resource_not_owned`)ので、宣言のままでは変換の入力が
|
|
90
|
+
* 組めない——請求項8は、実行時に見つかった競合の形をそのままでは受け取れなかった。
|
|
91
|
+
*
|
|
92
|
+
* 境界は計画時の**予測**であって、workerを閉じ込める制約ではない。予測を超えたのは予測が狭かった
|
|
93
|
+
* からであり、作業が不正だったからではない。したがって操作は「破った側を直す」ではなく、観測が
|
|
94
|
+
* 示した実態へ宣言を合わせることであり、**対称**である——どちらの足跡が予測を超えたかは観測が
|
|
95
|
+
* 決めるので、ここでは関与TODOを同じ規則で扱う。
|
|
96
|
+
*
|
|
97
|
+
* これは3つ目の処置ではない。翻訳を通ると計画時競合の形(`owns`の交差)になり、そこから先は
|
|
98
|
+
* 既存の請求項7/8がそのまま適用できる。競合辺が立つのは翻訳の副産物ではなく目的である——
|
|
99
|
+
* 立たないままだと`overlap_reduced`が最初から満たされたことになり、変換の検証が無意味になる。
|
|
100
|
+
*
|
|
101
|
+
* 広げるのは観測が示した資源だけとする。findingが持つ係争pathも関与TODOも観測であって推定では
|
|
102
|
+
* ない。広げた事実は呼び出し側へ返す。黙って広げると、予測が外れたことも、判定がどの宣言の上で
|
|
103
|
+
* 行われたかも残らない。
|
|
104
|
+
*
|
|
105
|
+
* **所有の宣言は裏取りと対で広げる。** `owns`だけ足すとその資源は`sensor_unbound`になり、compileは
|
|
106
|
+
* 非dispatchable(`BOUNDARY_UNKNOWN`)へ落ちる。そこでは競合が投影されないので、翻訳したのに
|
|
107
|
+
* 競合辺が立たず、変換の便益が測れない。裏取りに使えるqueryがrun のquery setに無ければ、
|
|
108
|
+
* 広げずに理由を返す——証明できない宣言を作らない。
|
|
109
|
+
*/
|
|
110
|
+
export function reconcileWitnessToObservation({
|
|
111
|
+
manualWitness, contestedPath, todoIds, sensorQuerySet = null, observedAffectedTests = null,
|
|
112
|
+
} = {}) {
|
|
113
|
+
const reconciled = {};
|
|
114
|
+
const widened = [];
|
|
115
|
+
const reject = (reason) => ({ manualWitness: null, widened: [], reasons: [reason] });
|
|
116
|
+
// 係争pathを覆えるqueryを、run のquery setから拾う。無ければ翻訳しない。
|
|
117
|
+
//
|
|
118
|
+
// `affected`だけを見る。path所有を裏取りするのはこのoperationであり、構造query(query/callers/
|
|
119
|
+
// callees/impact)はsymbolを的にする。構造queryがたまたま同じ文字列をtargetに持つからといって
|
|
120
|
+
// 所有の裏取りへ流用すると、束縛の意味が変わる。
|
|
121
|
+
const covering = (sensorQuerySet?.queries ?? [])
|
|
122
|
+
.filter((query) => query.operation === 'affected' && query.target === contestedPath)
|
|
123
|
+
.map((query) => query.id)
|
|
124
|
+
.sort(compareText);
|
|
125
|
+
|
|
126
|
+
for (const todoId of todoIds) {
|
|
127
|
+
const witness = manualWitness?.[todoId];
|
|
128
|
+
if (!plainObject(witness)) return reject(`witness_absent:${todoId}`);
|
|
129
|
+
const next = structuredClone(witness);
|
|
130
|
+
const ownsPath = (next.owns ?? [])
|
|
131
|
+
.some((own) => own.kind === 'path' && own.target === contestedPath);
|
|
132
|
+
const writesPath = (next.writes ?? []).includes(contestedPath);
|
|
133
|
+
const boundPath = (next.sensor_provenance?.queries ?? [])
|
|
134
|
+
.some((entry) => (entry.expect?.kind === 'affected' || entry.expect?.kind === 'path')
|
|
135
|
+
&& entry.expect?.path === contestedPath);
|
|
136
|
+
// 既に所有と書き込みを宣言しているなら、観測は予測の内側にある。合わせるものが無いので
|
|
137
|
+
// 触らない——裏取りが足りているかどうかは、その宣言を書いた側の問題であり、compileが見る。
|
|
138
|
+
// 翻訳が手を入れてよいのは、観測が予測を超えた分だけである。
|
|
139
|
+
if (ownsPath && writesPath) {
|
|
140
|
+
reconciled[todoId] = next;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!boundPath) {
|
|
144
|
+
if (covering.length === 0) return reject(`observation_unbacked:${todoId}:${contestedPath}`);
|
|
145
|
+
// 同じ資源は同じqueryで裏取りする。TODOごとに別のqueryを選ぶと、front-endが被覆の
|
|
146
|
+
// 曖昧さとして弾く。
|
|
147
|
+
if (covering.length > 1) return reject(`observation_binding_ambiguous:${contestedPath}`);
|
|
148
|
+
// 観測できていないaffectedを推測で埋めない。
|
|
149
|
+
if (!Array.isArray(observedAffectedTests)) {
|
|
150
|
+
return reject(`observation_affected_unread:${contestedPath}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// 所有・書き込み・裏取りをまとめて足す。観測されたのは「このpathへの書き込み」であり、
|
|
154
|
+
// 宣言の一部だけを合わせると、宣言が実態からずれたまま次の判定の前提になる。`creates`は
|
|
155
|
+
// 付けない——観測できたのはpathが既に在るからである。
|
|
156
|
+
if (!ownsPath) next.owns = [...next.owns, { kind: 'path', target: contestedPath }]
|
|
157
|
+
.sort((left, right) => compareText(`${left.kind}\0${left.target}`, `${right.kind}\0${right.target}`));
|
|
158
|
+
if (!writesPath) next.writes = [...next.writes, contestedPath].sort(compareText);
|
|
159
|
+
if (!boundPath) {
|
|
160
|
+
next.sensor_provenance = {
|
|
161
|
+
...next.sensor_provenance,
|
|
162
|
+
queries: [...next.sensor_provenance.queries,
|
|
163
|
+
{ query_id: covering[0], expect: { kind: 'affected', path: contestedPath } }],
|
|
164
|
+
};
|
|
165
|
+
// 面を1つ引き受けたら、その面のaffected testも引き受ける。宣言と観測はTODO単位で
|
|
166
|
+
// exact一致を要求されるので、片方だけ広げるとdriftになる。
|
|
167
|
+
next.affected_tests = [...new Set([...next.affected_tests, ...observedAffectedTests])]
|
|
168
|
+
.sort(compareText);
|
|
169
|
+
}
|
|
170
|
+
reconciled[todoId] = next;
|
|
171
|
+
widened.push({
|
|
172
|
+
todo_id: todoId,
|
|
173
|
+
resource: { kind: 'path', target: contestedPath },
|
|
174
|
+
fields: [
|
|
175
|
+
...(ownsPath ? [] : ['owns']),
|
|
176
|
+
...(writesPath ? [] : ['writes']),
|
|
177
|
+
...(boundPath ? [] : ['affected_tests', 'sensor_provenance']),
|
|
178
|
+
].sort(compareText),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return { manualWitness: reconciled, widened, reasons: [] };
|
|
182
|
+
}
|
|
183
|
+
|
|
80
184
|
/**
|
|
81
185
|
* 実行時witnessへconcern anchorを足してtodo witness setにする。
|
|
82
186
|
*
|
|
83
187
|
* 実行時のmanual_witnessはconcern_anchorsを持たない(`lattice.run_request.v3`)。持たせるのでなく、
|
|
84
188
|
* 宣言から足す——係争資源の中のどのsymbolを触るかは実行時に確定する情報であり、run開始時点の
|
|
85
189
|
* 契約に書けるものではないからである。
|
|
190
|
+
*
|
|
191
|
+
* anchorを足す前に宣言を観測へ合わせる(`reconcileWitnessToObservation`)。この順序でないと、
|
|
192
|
+
* 係争資源を所有していないTODOのanchorが必ず不正になる。
|
|
86
193
|
*/
|
|
87
|
-
export function buildRuntimeSeamWitnessSet({
|
|
194
|
+
export function buildRuntimeSeamWitnessSet({
|
|
195
|
+
request, declaration, contestedPath, executors, observedAffectedTests = null,
|
|
196
|
+
}) {
|
|
88
197
|
const todoIds = Object.keys(declaration.concern_symbols).sort(compareText);
|
|
198
|
+
const translated = reconcileWitnessToObservation({
|
|
199
|
+
manualWitness: request.manual_witness, contestedPath, todoIds,
|
|
200
|
+
sensorQuerySet: request.sensor_query_set, observedAffectedTests,
|
|
201
|
+
});
|
|
202
|
+
if (translated.manualWitness === null) {
|
|
203
|
+
return { witnessSet: null, widened: [], reasons: translated.reasons };
|
|
204
|
+
}
|
|
89
205
|
const manual = {};
|
|
90
206
|
for (const todoId of todoIds) {
|
|
91
|
-
const witness = request.manual_witness?.[todoId];
|
|
92
|
-
if (!plainObject(witness)) return { witnessSet: null, reasons: [`witness_absent:${todoId}`] };
|
|
93
207
|
manual[todoId] = {
|
|
94
|
-
...
|
|
208
|
+
...translated.manualWitness[todoId],
|
|
95
209
|
concern_anchors: [{
|
|
96
210
|
within: { kind: 'path', target: contestedPath },
|
|
97
211
|
symbols: [...declaration.concern_symbols[todoId]].sort(compareText),
|
|
@@ -108,7 +222,7 @@ export function buildRuntimeSeamWitnessSet({ request, declaration, contestedPath
|
|
|
108
222
|
witness_set_digest: '',
|
|
109
223
|
};
|
|
110
224
|
witnessSet.witness_set_digest = todoSelfDigest(witnessSet, 'witness_set_digest');
|
|
111
|
-
return { witnessSet, reasons: [] };
|
|
225
|
+
return { witnessSet, widened: translated.widened, reasons: [] };
|
|
112
226
|
}
|
|
113
227
|
|
|
114
228
|
function syntheticTodoPlan(todoIds) {
|
|
@@ -137,7 +251,7 @@ export async function resolveRuntimeSeam({
|
|
|
137
251
|
const todoIds = Object.keys(declaration.concern_symbols).sort(compareText);
|
|
138
252
|
|
|
139
253
|
if (finding.kind !== 'observed_write_conflict' || typeof finding.path !== 'string') {
|
|
140
|
-
return { lane: 'intentional_serial', reasons: ['finding_not_write_conflict'], split: null };
|
|
254
|
+
return { lane: 'intentional_serial', reasons: ['finding_not_write_conflict'], split: null, widened: [] };
|
|
141
255
|
}
|
|
142
256
|
const findingTodoIds = [...finding.todo_ids].sort(compareText);
|
|
143
257
|
if (findingTodoIds.join('\0') !== todoIds.join('\0')) {
|
|
@@ -145,30 +259,41 @@ export async function resolveRuntimeSeam({
|
|
|
145
259
|
lane: 'intentional_serial',
|
|
146
260
|
reasons: ['declared_todos_differ_from_finding'],
|
|
147
261
|
split: null,
|
|
262
|
+
widened: [],
|
|
148
263
|
};
|
|
149
264
|
}
|
|
150
265
|
|
|
266
|
+
// sensorは1回だけ引く。翻訳(宣言を観測へ合わせる)とcompileは同じ観測の上で行う——
|
|
267
|
+
// 別々に引くと、翻訳が見た実態とcompileが見た実態がずれうる。
|
|
268
|
+
const sensorEvidence = await collectWitnessSensorEvidence({
|
|
269
|
+
cwd: repoRoot, witnessSet: { sensor_query_set: request.sensor_query_set },
|
|
270
|
+
});
|
|
151
271
|
const built = buildRuntimeSeamWitnessSet({
|
|
152
272
|
request, declaration, contestedPath: finding.path,
|
|
153
273
|
executors: request.capacity.executors,
|
|
274
|
+
observedAffectedTests: affectedTestsFromEvidence({
|
|
275
|
+
sensorEvidence, querySet: request.sensor_query_set, path: finding.path,
|
|
276
|
+
}),
|
|
154
277
|
});
|
|
155
278
|
if (built.witnessSet === null) {
|
|
156
|
-
return { lane: 'intentional_serial', reasons: built.reasons, split: null };
|
|
279
|
+
return { lane: 'intentional_serial', reasons: built.reasons, split: null, widened: [] };
|
|
157
280
|
}
|
|
158
281
|
const witnessSet = built.witnessSet;
|
|
159
282
|
const plan = syntheticTodoPlan(todoIds);
|
|
160
283
|
|
|
161
|
-
// 観測したaffected tests
|
|
284
|
+
// 観測したaffected testsだけを検証に使う。宣言から発明しない。翻訳後の宣言から採る——
|
|
285
|
+
// 所有が広がったTODOは、その面のtestも自分のaffectedとして引き受けている。
|
|
162
286
|
const affectedTests = [...new Set(todoIds
|
|
163
|
-
.flatMap((todoId) =>
|
|
287
|
+
.flatMap((todoId) => witnessSet.manual_witness[todoId].affected_tests))].sort(compareText);
|
|
164
288
|
|
|
165
289
|
const baseArtifact = compileTodoIndependence({
|
|
166
|
-
witnessSet, plan, baseSha, compiledAt,
|
|
167
|
-
sensorEvidence: await collectWitnessSensorEvidence({ cwd: repoRoot, witnessSet }),
|
|
290
|
+
witnessSet, plan, baseSha, compiledAt, sensorEvidence,
|
|
168
291
|
});
|
|
169
292
|
|
|
170
293
|
const pathNames = { ...declaration.path_names };
|
|
171
|
-
|
|
294
|
+
// 翻訳で広げた宣言は決着へ載せる。どの宣言の上で五条件を判定したかが残らないと、
|
|
295
|
+
// 「変換して通った」という記録が何についてのものか確定しない。
|
|
296
|
+
const resolved = await resolveRuntimeSeamTreatment({
|
|
172
297
|
finding,
|
|
173
298
|
witnessSet,
|
|
174
299
|
pathNames,
|
|
@@ -176,6 +301,8 @@ export async function resolveRuntimeSeam({
|
|
|
176
301
|
manifestDigest: baseArtifact.result_digest,
|
|
177
302
|
affectedTests,
|
|
178
303
|
taskMigrationDigest: declaration.task_migration_digest,
|
|
304
|
+
// storeへ記録されたfindingのidで縛る。再計画側はこのidでfinding recordを読む。
|
|
305
|
+
recordedFindingDigest: findingRecord.finding_digest,
|
|
179
306
|
commitTransform: async ({ files, candidateId }) => commitSeamTransform({
|
|
180
307
|
repoRoot, baseSha, files, candidateId,
|
|
181
308
|
}),
|
|
@@ -205,6 +332,7 @@ export async function resolveRuntimeSeam({
|
|
|
205
332
|
return { ...applied, candidate: applied.candidate ?? null };
|
|
206
333
|
},
|
|
207
334
|
});
|
|
335
|
+
return { ...resolved, widened: built.widened };
|
|
208
336
|
}
|
|
209
337
|
|
|
210
338
|
/** 決着をartifactにする。branchを動かすのは操作するAIなので、行き先を明示して返す。 */
|
|
@@ -215,6 +343,21 @@ export function buildRuntimeSeamResolution({ runId, findingDigest, resolved }) {
|
|
|
215
343
|
finding_digest: findingDigest,
|
|
216
344
|
lane: resolved.lane,
|
|
217
345
|
reasons: [...resolved.reasons].sort(compareText),
|
|
346
|
+
// 確実の門(sc-012)。拒否理由を「宣言を直せば機械で通る」「AIが変換すべき」へ分類し、
|
|
347
|
+
// 次に誰が動くべきかを事実として返す。可否は決めない。
|
|
348
|
+
gate: explainSeamGate(resolved.reasons ?? []),
|
|
349
|
+
// 判定の前に宣言をどれだけ観測へ合わせたか。空配列は「予測が実態を覆っていた」という
|
|
350
|
+
// 意味であり、翻訳しなかったことと区別できる。
|
|
351
|
+
reconciled: [...(resolved.widened ?? [])]
|
|
352
|
+
.map((entry) => ({
|
|
353
|
+
todo_id: entry.todo_id,
|
|
354
|
+
resource: { kind: entry.resource.kind, target: entry.resource.target },
|
|
355
|
+
fields: [...entry.fields].sort(compareText),
|
|
356
|
+
}))
|
|
357
|
+
.sort((left, right) => compareText(
|
|
358
|
+
`${left.todo_id}\0${left.resource.kind}\0${left.resource.target}`,
|
|
359
|
+
`${right.todo_id}\0${right.resource.kind}\0${right.resource.target}`,
|
|
360
|
+
)),
|
|
218
361
|
split: resolved.split ?? null,
|
|
219
362
|
successor_base_sha: resolved.successor_base_sha ?? null,
|
|
220
363
|
successor_base_ref: resolved.successor_base_ref ?? null,
|
|
@@ -99,7 +99,7 @@ export async function resolveRuntimeSeamTreatment(options = {}) {
|
|
|
99
99
|
const {
|
|
100
100
|
finding, witnessSet, pathNames = {}, predeclaredTreatments = [],
|
|
101
101
|
applyConflict, commitTransform, baseSha, manifestDigest, affectedTests = [],
|
|
102
|
-
taskMigrationDigest,
|
|
102
|
+
taskMigrationDigest, recordedFindingDigest = null,
|
|
103
103
|
} = options;
|
|
104
104
|
|
|
105
105
|
if (finding?.kind === 'observed_write_conflict' && typeof finding.path === 'string') {
|
|
@@ -112,6 +112,7 @@ export async function resolveRuntimeSeamTreatment(options = {}) {
|
|
|
112
112
|
|
|
113
113
|
const { conflict, reasons } = seamConflictFromFinding({
|
|
114
114
|
finding, witnessSet, pathNames, affectedTests, baseSha, manifestDigest,
|
|
115
|
+
recordedFindingDigest,
|
|
115
116
|
});
|
|
116
117
|
if (conflict === null) {
|
|
117
118
|
return { lane: 'intentional_serial', treatment: null, split: null, reasons };
|