@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
package/src/runtime-cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { constants as fsConstants } from 'node:fs';
|
|
4
4
|
import {
|
|
5
5
|
lstat,
|
|
@@ -139,6 +139,20 @@ const KNOWN_ADAPTERS = Object.freeze(['scripted', 'isolated-worktree', 'actual-a
|
|
|
139
139
|
* 超えたら`rejected`として理由ごとjournalへ残す(黙って見送らない)。
|
|
140
140
|
*/
|
|
141
141
|
const IO_ESCALATION_LOCK_TIMEOUT_MS = 15_000;
|
|
142
|
+
/**
|
|
143
|
+
* 係争pathから資源idを導出する(front-endと同じ形)。
|
|
144
|
+
*
|
|
145
|
+
* front-endは`owns`から`own-<kind>-<sha256(target)先頭16>`を作る。path競合の直列化資源は
|
|
146
|
+
* その形でなければ、後継planの競合辺が実ownershipの資源と別物になってしまう。
|
|
147
|
+
* **形を1箇所で決める。** 別の形で作ると、同じpathが別の資源に見える。
|
|
148
|
+
*/
|
|
149
|
+
function ownedResourceId(kind, target) {
|
|
150
|
+
return `own-${kind}-${createHash('sha256').update(target, 'utf8').digest('hex').slice(0, 16)}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ownedPathResourceId(pathValue) {
|
|
154
|
+
return ownedResourceId('path', pathValue);
|
|
155
|
+
}
|
|
142
156
|
/** 走行中workerの完了を待つ上限と間隔。待てないと走行中の観測が成立しない。 */
|
|
143
157
|
const SCRIPTED_OBSERVE_TIMEOUT_MS = 120_000;
|
|
144
158
|
const SCRIPTED_OBSERVE_POLL_MS = 20;
|
|
@@ -743,6 +757,80 @@ async function readScriptedControllerReceipt({
|
|
|
743
757
|
return receipt;
|
|
744
758
|
}
|
|
745
759
|
|
|
760
|
+
/**
|
|
761
|
+
* hold裁定をworker processへ反映する(請求項7・8の再開側)。
|
|
762
|
+
*
|
|
763
|
+
* **barrierは全workerを止める。** 静止の証明はrun全体に対して要るからである。hold裁定は
|
|
764
|
+
* 止めた相手を`hold_set`と`continue_set`へ分け、後者は「影響閉包の外なので作業を捨てない」
|
|
765
|
+
* と判定されたものである。
|
|
766
|
+
*
|
|
767
|
+
* **carry-overは「一度も止まらない」ではない。** rebindは静止を要求する
|
|
768
|
+
* (`write_enabled === false`)ので、holdの直後に再開すると後継epochへ束ね直せず
|
|
769
|
+
* `EPOCH_REBIND_INCOMPLETE`で落ちる。止まり、繋がれ、そこで動き出すのが正しい順序である。
|
|
770
|
+
* よってこの関数を呼ぶのはrebindが済んだ後だけとする。
|
|
771
|
+
*
|
|
772
|
+
* pidだけで再開しない。pidは再利用されるので、start identityを照合して記録と同じprocessで
|
|
773
|
+
* あることを確かめる——別のprocessをSIGCONTするのは、止めるのと同じくらい危険である。
|
|
774
|
+
*/
|
|
775
|
+
async function resumeContinuedWorkers({ events, todoIds }) {
|
|
776
|
+
if (!Array.isArray(todoIds) || todoIds.length === 0) return { resumed: [], skipped: [] };
|
|
777
|
+
const resumed = [];
|
|
778
|
+
const skipped = [];
|
|
779
|
+
for (const todoId of todoIds) {
|
|
780
|
+
const dispatch = events.findLast((event) => event.kind === 'executor_dispatched'
|
|
781
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === todoId);
|
|
782
|
+
const binding = dispatch?.payload?.direct_os_observation_binding;
|
|
783
|
+
const pid = binding?.process_pid;
|
|
784
|
+
const recorded = binding?.process_start_identity?.identity_digest;
|
|
785
|
+
if (!Number.isSafeInteger(pid) || typeof recorded !== 'string') continue;
|
|
786
|
+
// pidは再利用される。記録と同じprocessであることを確かめてから再開する——
|
|
787
|
+
// 別のprocessをSIGCONTするのは、止めるのと同じくらい危険である。
|
|
788
|
+
const observed = await observeManagedProcessStartIdentity(pid).catch(() => null);
|
|
789
|
+
if (observed === null || observed.identity_digest !== recorded) {
|
|
790
|
+
skipped.push({ todo_id: todoId, reason: observed === null ? 'process不在' : 'start identity不一致' });
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
try { process.kill(pid, 'SIGCONT'); resumed.push(todoId); } catch {
|
|
794
|
+
skipped.push({ todo_id: todoId, reason: 'SIGCONT失敗' });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return { resumed, skipped };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* runが起こしたworker processを、耐久記録から回収する。
|
|
802
|
+
*
|
|
803
|
+
* **記録が正本である。** 誰を起こしたかは`executor_dispatched`の
|
|
804
|
+
* `direct_os_observation_binding`が持っており、controllerが落ちていても残る。
|
|
805
|
+
*
|
|
806
|
+
* pidだけで殺さない。pidは再利用されるので、start identityを取り直して記録と一致した
|
|
807
|
+
* ものだけを止める——一致しなければ、それは別のprocessである。
|
|
808
|
+
*
|
|
809
|
+
* SIGKILLで送る。holdで止めたworkerはSIGSTOP状態にあり、SIGTERMは継続されるまで
|
|
810
|
+
* 配送されない。停止したまま放置されたprocessは自分では終われない。
|
|
811
|
+
*/
|
|
812
|
+
async function reapRunWorkerProcesses({ events }) {
|
|
813
|
+
const reaped = [];
|
|
814
|
+
const skipped = [];
|
|
815
|
+
const seen = new Set();
|
|
816
|
+
for (const event of events) {
|
|
817
|
+
if (event.kind !== 'executor_dispatched') continue;
|
|
818
|
+
const binding = event.payload?.direct_os_observation_binding;
|
|
819
|
+
const pid = binding?.process_pid;
|
|
820
|
+
const recorded = binding?.process_start_identity?.identity_digest;
|
|
821
|
+
if (!Number.isSafeInteger(pid) || typeof recorded !== 'string' || seen.has(pid)) continue;
|
|
822
|
+
seen.add(pid);
|
|
823
|
+
const observed = await observeManagedProcessStartIdentity(pid).catch(() => null);
|
|
824
|
+
if (observed === null) continue; // 既に居ない
|
|
825
|
+
if (observed.identity_digest !== recorded) {
|
|
826
|
+
skipped.push({ pid, reason: 'start identity不一致(pid再利用)' });
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
try { process.kill(pid, 'SIGKILL'); reaped.push(pid); } catch { /* 競合で既に消えた */ }
|
|
830
|
+
}
|
|
831
|
+
return { reaped, skipped };
|
|
832
|
+
}
|
|
833
|
+
|
|
746
834
|
async function driveInitialScriptedManagedEpoch({
|
|
747
835
|
runDir,
|
|
748
836
|
repoRoot,
|
|
@@ -1130,6 +1218,64 @@ async function runSeamResolve({ runDir, repoRoot, findingDigest, requestPath, st
|
|
|
1130
1218
|
return resolution.lane === 'seam_transform' ? 0 : 1;
|
|
1131
1219
|
}
|
|
1132
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* 切断コストの内訳を、記録済みfindingについて投影する(read-only、docs/plan_seam-cost.md)。
|
|
1223
|
+
*
|
|
1224
|
+
* `seam resolve`が隔離worktreeで実変換まで行うのに対し、これは**変換を試す前の安い観測**である。
|
|
1225
|
+
* 「何を共有しているから単純に切れないのか」を数えられる事実として返し、試すかどうかの判断は
|
|
1226
|
+
* 操作するAIに残す。閾値も可否判定も返さない。投影であって記録ではないので、runへ何も書かない。
|
|
1227
|
+
*
|
|
1228
|
+
* symbolの帰属(concern_symbols)は入力で受ける。実行時のwitnessはpath単位の宣言しか持たず、
|
|
1229
|
+
* 係争fileの中で誰がどのsymbolを触るかはAIだけが知っている——ここで推定しない。
|
|
1230
|
+
*/
|
|
1231
|
+
async function runSeamProfile({ runDir, repoRoot, findingDigest, inputPath, stdout }) {
|
|
1232
|
+
const committed = await readCommittedEpochStore(runDir);
|
|
1233
|
+
if (committed === null) {
|
|
1234
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'runがmanaged storeへactivateされていない');
|
|
1235
|
+
}
|
|
1236
|
+
const found = await readRuntimeFindingRecord({
|
|
1237
|
+
runDir, findingDigest, planEpoch: committed.pointer.plan_epoch,
|
|
1238
|
+
});
|
|
1239
|
+
if (found.record === null) throw new CliContractError('STALE_FINDING', found.reason);
|
|
1240
|
+
const finding = found.record.finding;
|
|
1241
|
+
if (typeof finding.path !== 'string') {
|
|
1242
|
+
throw new CliContractError('SEAM_PROFILE_UNAVAILABLE',
|
|
1243
|
+
'path findingではない競合に切断コストの内訳は立たない');
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const declaration = await readBoundedJson(inputPath, 'seam profile input');
|
|
1247
|
+
const identifier = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/u;
|
|
1248
|
+
const symbols = declaration?.concern_symbols;
|
|
1249
|
+
const todoIds = [...finding.todo_ids].sort();
|
|
1250
|
+
const declaredIds = symbols === null || typeof symbols !== 'object' || Array.isArray(symbols)
|
|
1251
|
+
? null : Object.keys(symbols).sort();
|
|
1252
|
+
if (declaredIds === null
|
|
1253
|
+
|| declaredIds.join('\0') !== todoIds.join('\0')
|
|
1254
|
+
|| declaredIds.some((todoId) => !Array.isArray(symbols[todoId])
|
|
1255
|
+
|| symbols[todoId].length === 0
|
|
1256
|
+
|| !symbols[todoId].every((name) => identifier.test(name)))) {
|
|
1257
|
+
throw new CliContractError('SEAM_PROFILE_UNAVAILABLE',
|
|
1258
|
+
'concern_symbolsがfindingのtodo集合と一致しない。'
|
|
1259
|
+
+ `{"concern_symbols": {${todoIds.map((id) => `"${id}": ["<symbol>"]`).join(', ')}}}の形で渡す`);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
let sourceText;
|
|
1263
|
+
try {
|
|
1264
|
+
sourceText = await readFile(path.join(repoRoot, finding.path), 'utf8');
|
|
1265
|
+
} catch {
|
|
1266
|
+
throw new CliContractError('SEAM_PROFILE_UNAVAILABLE', `係争fileを読めない: ${finding.path}`);
|
|
1267
|
+
}
|
|
1268
|
+
const { computeSeamCostProfile } = await import('./seam-cost.mjs');
|
|
1269
|
+
const { profile, reasons } = await computeSeamCostProfile({
|
|
1270
|
+
repoRoot, sourcePath: finding.path, sourceText, ownedSymbolsByTask: symbols,
|
|
1271
|
+
});
|
|
1272
|
+
if (profile === null) {
|
|
1273
|
+
throw new CliContractError('SEAM_PROFILE_UNAVAILABLE', reasons.join(',') || 'profile_unavailable');
|
|
1274
|
+
}
|
|
1275
|
+
stdout.write(`${JSON.stringify(profile)}\n`);
|
|
1276
|
+
return 0;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1133
1279
|
async function runObserve({ runDir, stdout }) {
|
|
1134
1280
|
const { events } = await readRunStore(runDir);
|
|
1135
1281
|
const chain = verifyRunEventChain({ events });
|
|
@@ -1179,7 +1325,9 @@ async function runStatus({ runDir, stdout }) {
|
|
|
1179
1325
|
if (managed !== null) {
|
|
1180
1326
|
output.schema = 'lattice.managed_run_status.v1';
|
|
1181
1327
|
const runtimeProjection = {
|
|
1182
|
-
|
|
1328
|
+
// v2は`treatment_advice`の追加。既定modeが直列化でも変換を試せる場合があることを、
|
|
1329
|
+
// 運転側が見える形にした(ct-003)。
|
|
1330
|
+
schema: 'lattice.runtime_status_projection.v2',
|
|
1183
1331
|
...projectRuntimeStatusOverlays({ events }),
|
|
1184
1332
|
runtime_frozen: managedFrozen,
|
|
1185
1333
|
};
|
|
@@ -1844,8 +1992,6 @@ export async function runManagedSupervisorDaemon({
|
|
|
1844
1992
|
* 自分のprocessで作業するので、そのprocessを止めると制御そのものが止まる——止めない限り
|
|
1845
1993
|
* 証明できず、止めれば応答できない。**別processのexecutorを持つまでこの穴は埋まらない。**
|
|
1846
1994
|
*/
|
|
1847
|
-
const canProveQuiescence = () => true;
|
|
1848
|
-
|
|
1849
1995
|
const attributionIsDistinct = (warning, roots) => {
|
|
1850
1996
|
const paths = warning.todo_ids.map((todoId) => roots[todoId]);
|
|
1851
1997
|
if (paths.some((value) => typeof value !== 'string')) return false;
|
|
@@ -1974,19 +2120,6 @@ export async function runManagedSupervisorDaemon({
|
|
|
1974
2120
|
return decide('skipped',
|
|
1975
2121
|
'worktree rootを共有する構成では書き手を特定できない(帰属はrootだけで決まる)');
|
|
1976
2122
|
}
|
|
1977
|
-
// **holdは静止の証明を要求する。** 直接OS観測はexecutorのprocessが実際に停止している
|
|
1978
|
-
// ことまで確かめるが、executorがcontroller自身のprocessである構成では、止めると
|
|
1979
|
-
// 制御そのものが止まるので証明できない。
|
|
1980
|
-
//
|
|
1981
|
-
// ここで止めるのは、conflictがintakeをfreezeするからである。freezeしてholdが通らない
|
|
1982
|
-
// 状態を作ると、runは進むことも畳むこともできなくなる(abandonも静止を要求する)。
|
|
1983
|
-
// 止められないと分かっているなら、freezeさせない方が安全側である。判定はcheckpointが
|
|
1984
|
-
// 従来どおり担う——早期警報は早めるためだけに在るという原則どおり。
|
|
1985
|
-
if (!canProveQuiescence()) {
|
|
1986
|
-
return decide('rejected',
|
|
1987
|
-
'executorがcontroller自身のprocessで走っており、静止を証明できない'
|
|
1988
|
-
+ '(停止すると制御も止まる)。freezeさせるとrunを畳めなくなるので進めない');
|
|
1989
|
-
}
|
|
1990
2123
|
const recorded = await recordCheckpoints(escalation, probed.checkpoints);
|
|
1991
2124
|
if (!recorded.ok) return decide(recorded.outcome, recorded.detail);
|
|
1992
2125
|
|
|
@@ -2097,14 +2230,18 @@ export async function runManagedSupervisorDaemon({
|
|
|
2097
2230
|
/** 警報の処理を直列化する鎖。`onWarning`はここへ繋ぐだけにする。 */
|
|
2098
2231
|
let escalationChain = Promise.resolve();
|
|
2099
2232
|
|
|
2100
|
-
const resolveObservationBinding = async ({ binding }) => {
|
|
2233
|
+
const resolveObservationBinding = async ({ binding, ack }) => {
|
|
2234
|
+
// rebind経路はrunning bindingを持たず、ackだけで来る(`kind: 'rebind'`)。どちらから来ても
|
|
2235
|
+
// 同じdispatch記録へ辿り着けなければ、繋ぎ直す相手を特定できない。
|
|
2236
|
+
const todoId = binding?.todo_id ?? ack?.todo_id;
|
|
2237
|
+
const executorHandle = binding?.executor_handle ?? ack?.executor_handle;
|
|
2101
2238
|
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2102
2239
|
const dispatch = events.findLast((event) => event.kind === 'executor_dispatched'
|
|
2103
|
-
&& event.subject?.kind === 'todo' && event.subject.ref ===
|
|
2104
|
-
&& event.payload?.executor_handle ===
|
|
2240
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === todoId
|
|
2241
|
+
&& event.payload?.executor_handle === executorHandle);
|
|
2105
2242
|
const observation = dispatch?.payload?.direct_os_observation_binding;
|
|
2106
2243
|
if (observation === null || typeof observation !== 'object' || Array.isArray(observation)) {
|
|
2107
|
-
throw new ManagedRuntimeError('HOLD_ACKS_INCOMPLETE', `durable Direct OS binding不足: ${
|
|
2244
|
+
throw new ManagedRuntimeError('HOLD_ACKS_INCOMPLETE', `durable Direct OS binding不足: ${todoId ?? 'unknown'}`);
|
|
2108
2245
|
}
|
|
2109
2246
|
return structuredClone(observation);
|
|
2110
2247
|
};
|
|
@@ -2429,6 +2566,19 @@ export async function runManagedSupervisorDaemon({
|
|
|
2429
2566
|
if (finding.plan_epoch !== active.pointer.plan_epoch) {
|
|
2430
2567
|
throw new ManagedRuntimeError('STALE_FINDING', 'findingはactive epochに属さない');
|
|
2431
2568
|
}
|
|
2569
|
+
// 競合は2者以上いて初めて競合である。1者しか名指していない観測——誰の領分とも
|
|
2570
|
+
// 重なっていない予測超過——でfreezeすると、抜け道が無い。処置は2つとも2者を要求する
|
|
2571
|
+
// (直列化は`todo_ids.length >= 2`、seam変換は2つの面へ切る)ので、legalなrecompileが
|
|
2572
|
+
// 作れないまま止まる。予測が狭かっただけなので、記録して次のcompileで宣言を実態へ
|
|
2573
|
+
// 合わせるのが正しい応答である。
|
|
2574
|
+
if ((finding.finding?.todo_ids ?? []).length < 2) {
|
|
2575
|
+
throw new ManagedRuntimeError('FINDING_NOT_A_CONFLICT',
|
|
2576
|
+
'1 TODOしか名指していない観測はfreezeへ運べない。'
|
|
2577
|
+
+ '予測超過は競合ではないので、記録のまま次のcompileで宣言を観測へ合わせる', {
|
|
2578
|
+
finding_kind: finding.finding?.kind ?? null,
|
|
2579
|
+
todo_ids: [...(finding.finding?.todo_ids ?? [])],
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2432
2582
|
let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2433
2583
|
const todoId = finding.finding?.todo_ids?.[0] ?? active.bundle.plan.nodes[0].todo_id;
|
|
2434
2584
|
events.push(buildNextRunEvent({ events, runId: request.request_id, kind: 'conflict_found', planEpoch: active.pointer.plan_epoch,
|
|
@@ -2647,20 +2797,40 @@ export async function runManagedSupervisorDaemon({
|
|
|
2647
2797
|
!== canonicalizeArtifact(treatment.todo_ids ?? treatment.predecessor_task_ids)) {
|
|
2648
2798
|
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'treatmentがhold finding/todo集合と一致しない');
|
|
2649
2799
|
}
|
|
2650
|
-
if (recompileRequest.mode === 'intentional_serial'
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2800
|
+
if (recompileRequest.mode === 'intentional_serial') {
|
|
2801
|
+
// **path競合には資源idが無い。** finding契約はpath形の`resource_id`をnullと定めており、
|
|
2802
|
+
// path由来の資源idはmanifestの`resources`にも載らない(載るのは宣言したbare資源だけ)。
|
|
2803
|
+
// それでもrouteConflictTreatmentはpath競合を直列化レーンへ振る——請求項7の
|
|
2804
|
+
// 「一方を停止し、他方を確定し、停止した方を再開する」がその経路だからである。
|
|
2805
|
+
//
|
|
2806
|
+
// よってpath findingの時は、係争pathからfront-endと同じ形で資源idを導出して照合する。
|
|
2807
|
+
// 導出値との一致を要求するので、資源idを捏造できないという元の保証は保たれる
|
|
2808
|
+
// ——照合先がfindingそのものへ固定されるため、manifest所属を見る必要が無い。
|
|
2809
|
+
const expectedResource = treatmentFinding.finding.path === null
|
|
2810
|
+
? treatmentFinding.finding.resource_id
|
|
2811
|
+
: ownedPathResourceId(treatmentFinding.finding.path);
|
|
2812
|
+
if (expectedResource !== treatment.resource_id) {
|
|
2813
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'serial resourceがfinding resourceと一致しない');
|
|
2814
|
+
}
|
|
2815
|
+
// 宣言済み資源の競合は、従来どおり実ownershipから再導出できることまで要求する。
|
|
2816
|
+
if (treatmentFinding.finding.path === null
|
|
2817
|
+
&& !treatment.todo_ids.every((todoId) => {
|
|
2818
|
+
const manifest = active.bundle.manifests[todoId];
|
|
2819
|
+
return manifest?.resources?.includes(treatment.resource_id)
|
|
2820
|
+
|| manifest?.state_effects?.some((effect) => effect.resource_id === treatment.resource_id);
|
|
2821
|
+
})) {
|
|
2822
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'serial resourceをfresh ownershipから再導出できない');
|
|
2823
|
+
}
|
|
2661
2824
|
}
|
|
2662
2825
|
if (recompileRequest.mode === 'seam_split') {
|
|
2826
|
+
// **宣言された面の所有も数える。** `resources`/`state_effects`だけを見ていた間、
|
|
2827
|
+
// path所有はbefore/afterのどちらにも現れず、seam splitが述べる`added`は常に空の
|
|
2828
|
+
// 導出値と突き合わされていた——path競合を切るsplitは原理的に一致しなかった。
|
|
2829
|
+
// 資源idの合成形は`verifySeamSplitSuccessor`と同じ`own-<kind>-<sha16>`にする。
|
|
2663
2830
|
const ownership = (manifests) => Object.entries(manifests).flatMap(([todoId, manifest]) => [
|
|
2831
|
+
...(manifest.owns ?? []).map((own) => ({
|
|
2832
|
+
resource_id: ownedResourceId(own.kind, own.target),
|
|
2833
|
+
owner_todo_id: todoId, access_kind: 'own' })),
|
|
2664
2834
|
...manifest.resources.map((resourceId) => ({ resource_id: resourceId,
|
|
2665
2835
|
owner_todo_id: todoId, access_kind: 'own' })),
|
|
2666
2836
|
...manifest.state_effects.map((effect) => ({ resource_id: effect.resource_id,
|
|
@@ -2674,9 +2844,41 @@ export async function runManagedSupervisorDaemon({
|
|
|
2674
2844
|
].sort((left, right) => canonicalizeArtifact(left).localeCompare(canonicalizeArtifact(right)));
|
|
2675
2845
|
const difference = (left, right) => left.filter((entry) => !right.some((other) =>
|
|
2676
2846
|
canonicalizeArtifact(entry) === canonicalizeArtifact(other)));
|
|
2677
|
-
|
|
2847
|
+
// **比較の起点は、観測へ合わせた後の宣言である。**
|
|
2848
|
+
//
|
|
2849
|
+
// 実行時に見つかった競合は、片方がその資源を所有していないから起きる。変換はその
|
|
2850
|
+
// 宣言を観測へ合わせてから導出されるので(翻訳段)、splitが述べる遷移も合わせた後の
|
|
2851
|
+
// 状態からのものになる。ここで記録のままのpredecessorと比べると、実行時に見つかった
|
|
2852
|
+
// 競合から作ったsplitは永久に一致しない——請求項8が実行時に届かなくなる。
|
|
2853
|
+
//
|
|
2854
|
+
// 合わせる材料はfindingそのものであり、宣言でも入力でもない。再導出なので、
|
|
2855
|
+
// 呼び出し側が何を主張しても結果は変わらない。
|
|
2856
|
+
const contestedResource = treatmentFinding.finding.path === null
|
|
2857
|
+
? treatmentFinding.finding.resource_id
|
|
2858
|
+
: ownedPathResourceId(treatmentFinding.finding.path);
|
|
2859
|
+
const observedTodoIds = [...treatmentFinding.finding.todo_ids].sort();
|
|
2860
|
+
const reconciledOwnership = [...ownership(active.bundle.manifests)];
|
|
2861
|
+
for (const todoId of observedTodoIds) {
|
|
2862
|
+
const entry = { resource_id: contestedResource, owner_todo_id: todoId, access_kind: 'own' };
|
|
2863
|
+
if (!reconciledOwnership.some((other) => canonicalizeArtifact(other) === canonicalizeArtifact(entry))) {
|
|
2864
|
+
reconciledOwnership.push(entry);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
const reconciledEdges = [...edges(active.bundle.plan)];
|
|
2868
|
+
for (let left = 0; left < observedTodoIds.length; left += 1) {
|
|
2869
|
+
for (let right = left + 1; right < observedTodoIds.length; right += 1) {
|
|
2870
|
+
const edge = { from_todo_id: observedTodoIds[left], to_todo_id: observedTodoIds[right],
|
|
2871
|
+
kind: 'conflict' };
|
|
2872
|
+
if (!reconciledEdges.some((other) => canonicalizeArtifact(other) === canonicalizeArtifact(edge))) {
|
|
2873
|
+
reconciledEdges.push(edge);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
const sortEntries = (entries) => [...entries]
|
|
2878
|
+
.sort((left, right) => canonicalizeArtifact(left).localeCompare(canonicalizeArtifact(right)));
|
|
2879
|
+
const beforeOwnership = sortEntries(reconciledOwnership);
|
|
2678
2880
|
const afterOwnership = ownership(compiled.manifests);
|
|
2679
|
-
const beforeEdges =
|
|
2881
|
+
const beforeEdges = sortEntries(reconciledEdges);
|
|
2680
2882
|
const afterEdges = edges(newPlan);
|
|
2681
2883
|
const derivedOwnership = { added: difference(afterOwnership, beforeOwnership),
|
|
2682
2884
|
removed: difference(beforeOwnership, afterOwnership) };
|
|
@@ -2825,6 +3027,20 @@ export async function runManagedSupervisorDaemon({
|
|
|
2825
3027
|
rebindEvidence.set(todoId, { ...evidence,
|
|
2826
3028
|
controller_registration_digest: owner.registration.registration_digest });
|
|
2827
3029
|
}
|
|
3030
|
+
// **ここが再開の位置である。** rebindは静止を要求する(`write_enabled === false`)ので、
|
|
3031
|
+
// holdの直後に再開すると後継epochへ束ね直せない。carry-overは「作業を捨てない」で
|
|
3032
|
+
// あって「一度も止まらない」ではない——barrierで止まり、rebindで新epochへ繋がれ、
|
|
3033
|
+
// そこで初めて動き出す。
|
|
3034
|
+
if (rebound.size > 0) {
|
|
3035
|
+
const resumed = await resumeContinuedWorkers({ events, todoIds: [...rebound] });
|
|
3036
|
+
if (resumed.resumed.length > 0 || resumed.skipped.length > 0) {
|
|
3037
|
+
await appendControl({ run_id: request.request_id, kind: 'workers_resumed',
|
|
3038
|
+
session_nonce_digest: digestArtifact(sessionNonce), payload: {
|
|
3039
|
+
resumed_todo_ids: [...resumed.resumed].sort(),
|
|
3040
|
+
skipped_count: resumed.skipped.length,
|
|
3041
|
+
} });
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
2828
3044
|
for (const todoId of core.planDiff.redispatched) {
|
|
2829
3045
|
const successors = recompileRequest.task_migration.entries
|
|
2830
3046
|
.find((entry) => entry.predecessor_task_id === todoId)?.successor_task_ids ?? [];
|
|
@@ -3002,9 +3218,22 @@ export async function runManagedSupervisorDaemon({
|
|
|
3002
3218
|
recordedAt: canonicalNow() })];
|
|
3003
3219
|
}
|
|
3004
3220
|
await replaceEventsAtomically(runDir, events);
|
|
3005
|
-
// abandon
|
|
3006
|
-
//
|
|
3221
|
+
// **abandonは成果を捨てる決定である。** worker processの終了はcleanupではなく
|
|
3222
|
+
// その決定の一部なので、誰を止めたかを記録へ残す。closeでは何もしない——
|
|
3223
|
+
// 完走したrunのworkerは既にterminalであり、生きていればそれは欠陥である
|
|
3224
|
+
// (`closeRunIfComplete`が全TODO accepted を要求するので、そもそもcloseへ来ない)。
|
|
3007
3225
|
if (controlRequest.operation === 'abandon') {
|
|
3226
|
+
const reaped = await reapRunWorkerProcesses({ events }).catch(() => null);
|
|
3227
|
+
if (reaped !== null && (reaped.reaped.length > 0 || reaped.skipped.length > 0)) {
|
|
3228
|
+
await appendControl({ run_id: request.request_id, kind: 'worker_processes_terminated',
|
|
3229
|
+
session_nonce_digest: digestArtifact(sessionNonce), payload: {
|
|
3230
|
+
reason: shutdownReason,
|
|
3231
|
+
terminated_pids: [...reaped.reaped].sort((left, right) => left - right),
|
|
3232
|
+
skipped_count: reaped.skipped.length,
|
|
3233
|
+
} });
|
|
3234
|
+
}
|
|
3235
|
+
// 木も畳む。closeでは畳まない——木そのものがrunの成果であり、着地させる前に
|
|
3236
|
+
// 消したら受理した内容が残らない。
|
|
3008
3237
|
await removeScriptedWorktrees({ repoRoot, runDir }).catch(() => null);
|
|
3009
3238
|
}
|
|
3010
3239
|
await appendControl({ run_id: request.request_id, kind: 'supervisor_stopped',
|
|
@@ -3528,6 +3757,11 @@ export async function runManagedSupervisorDaemon({
|
|
|
3528
3757
|
// 監視fdを残さない。取り残すとtest fixtureの後片付けが重くなる。
|
|
3529
3758
|
sentinel?.close();
|
|
3530
3759
|
sentinel = null;
|
|
3760
|
+
// **これは事故処理であって、正しい閉じ方ではない。** 正規の終了はabandon(破棄の決定)か
|
|
3761
|
+
// hold後の再開であり、そのどちらも通らずdaemonが落ちる時だけここへ来る。停止した
|
|
3762
|
+
// workerは自分では終われないので、記録から辿って道連れにする。
|
|
3763
|
+
await readBoundedJson(path.join(runDir, 'events.json'), 'run events')
|
|
3764
|
+
.then((events) => reapRunWorkerProcesses({ events })).catch(() => null);
|
|
3531
3765
|
if (activationCommitted && activation !== null) {
|
|
3532
3766
|
await appendControl({ run_id: request.request_id, kind: 'supervisor_stopped',
|
|
3533
3767
|
session_nonce_digest: digestArtifact(sessionNonce), payload: { signal } });
|
|
@@ -3693,6 +3927,16 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
|
|
|
3693
3927
|
requestId: requestIdOverride,
|
|
3694
3928
|
});
|
|
3695
3929
|
};
|
|
3930
|
+
} else if (argv.length === 9
|
|
3931
|
+
&& argv[0] === 'run' && argv[1] === 'seam' && argv[2] === 'profile'
|
|
3932
|
+
&& argv[3] === '--run' && typeof argv[4] === 'string' && argv[4].length > 0
|
|
3933
|
+
&& argv[5] === '--finding' && /^[0-9a-f]{64}$/u.test(argv[6])
|
|
3934
|
+
&& argv[7] === '--input' && typeof argv[8] === 'string' && argv[8].length > 0) {
|
|
3935
|
+
action = async () => {
|
|
3936
|
+
const { repoRoot, runDir } = await resolveRunStore(cwd, argv[4]);
|
|
3937
|
+
return runSeamProfile({ runDir, repoRoot, findingDigest: argv[6],
|
|
3938
|
+
inputPath: path.resolve(cwd, argv[8]), stdout });
|
|
3939
|
+
};
|
|
3696
3940
|
} else if (argv.length === 9
|
|
3697
3941
|
&& argv[0] === 'run' && argv[1] === 'seam' && argv[2] === 'resolve'
|
|
3698
3942
|
&& argv[3] === '--run' && typeof argv[4] === 'string' && argv[4].length > 0
|
|
@@ -53,7 +53,7 @@ export const RUNTIME_CONFLICT_KINDS = Object.freeze([
|
|
|
53
53
|
'observed_write_conflict',
|
|
54
54
|
'semantic_conflict_unknown',
|
|
55
55
|
'effect_conflict_unknown',
|
|
56
|
-
'
|
|
56
|
+
'undeclared_write',
|
|
57
57
|
'stale_context',
|
|
58
58
|
]);
|
|
59
59
|
export const HOLD_REASON_KINDS = Object.freeze([
|
|
@@ -161,7 +161,7 @@ export function validateRuntimeControlEventPayload(kind, value) {
|
|
|
161
161
|
// 機械が何かに気づいたのに黙っている状態を残さない。
|
|
162
162
|
if (kind === 'io_warning_observed') {
|
|
163
163
|
return exact(value, ['warning_kind', 'todo_ids', 'path', 'probe_outcome', 'warning_digest'])
|
|
164
|
-
&& ['io_overlap_warning', '
|
|
164
|
+
&& ['io_overlap_warning', 'io_undeclared_write_warning'].includes(value.warning_kind)
|
|
165
165
|
&& Array.isArray(value.todo_ids) && value.todo_ids.length >= 1 && value.todo_ids.length <= 256
|
|
166
166
|
&& value.todo_ids.every(identifier)
|
|
167
167
|
&& value.todo_ids.every((id, index) => index === 0 || value.todo_ids[index - 1] < id)
|
|
@@ -171,6 +171,29 @@ export function validateRuntimeControlEventPayload(kind, value) {
|
|
|
171
171
|
warning_kind: value.warning_kind, todo_ids: value.todo_ids, path: value.path,
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
|
+
// hold裁定のcontinue_setをprocessへ反映した記録(請求項7・8の再開側)。
|
|
175
|
+
// barrierは全workerを止めるので、続けてよいと判定した作業を再開しなければ、
|
|
176
|
+
// 判定と実行が食い違ったままになる。
|
|
177
|
+
if (kind === 'workers_resumed') {
|
|
178
|
+
return exact(value, ['resumed_todo_ids', 'skipped_count'])
|
|
179
|
+
&& Array.isArray(value.resumed_todo_ids) && value.resumed_todo_ids.length <= 4096
|
|
180
|
+
&& value.resumed_todo_ids.every(identifier)
|
|
181
|
+
&& value.resumed_todo_ids.every((id, index) => index === 0
|
|
182
|
+
|| value.resumed_todo_ids[index - 1] < id)
|
|
183
|
+
&& Number.isSafeInteger(value.skipped_count) && value.skipped_count >= 0;
|
|
184
|
+
}
|
|
185
|
+
// 破棄の決定としてworker processを終了した記録。**cleanupではない。**
|
|
186
|
+
// 停止したworkerの行き先は「再開」か「破棄」の二択であり、破棄を選んだなら
|
|
187
|
+
// 誰を止めたかが残っていなければ、後から何が捨てられたか説明できない。
|
|
188
|
+
if (kind === 'worker_processes_terminated') {
|
|
189
|
+
return exact(value, ['reason', 'terminated_pids', 'skipped_count'])
|
|
190
|
+
&& typeof value.reason === 'string' && value.reason.length > 0
|
|
191
|
+
&& Array.isArray(value.terminated_pids) && value.terminated_pids.length <= 4096
|
|
192
|
+
&& value.terminated_pids.every((pid) => Number.isSafeInteger(pid) && pid > 0)
|
|
193
|
+
&& value.terminated_pids.every((pid, index) => index === 0
|
|
194
|
+
|| value.terminated_pids[index - 1] < pid)
|
|
195
|
+
&& Number.isSafeInteger(value.skipped_count) && value.skipped_count >= 0;
|
|
196
|
+
}
|
|
174
197
|
// probeを通った警報を既存hold経路へ入れた顛末(ADR 0143の三段目)。
|
|
175
198
|
// 成否どちらも残す。自動escalationが黙って失敗する状態を作らない——失敗の理由は
|
|
176
199
|
// `detail`が持ち、これが無いと「警報は出たのにholdが掛かっていない」を後から説明できない。
|
|
@@ -150,7 +150,7 @@ export function classifyObservedDiff(options = {}) {
|
|
|
150
150
|
const declaredWrites = manifests[todoId].writes ?? [];
|
|
151
151
|
for (const path of paths) {
|
|
152
152
|
if (!declaredWriteCovers(declaredWrites, path)) {
|
|
153
|
-
findings.push({ kind: '
|
|
153
|
+
findings.push({ kind: 'undeclared_write', todo_ids: [todoId], path });
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
}
|
|
@@ -257,7 +257,7 @@ export function coveredBy(declaredWrites, observedPath) {
|
|
|
257
257
|
* cross-bindし、closed conflict分類のfindingを返す(producer側の検出。独立再計算は
|
|
258
258
|
* runtime-decision-verifierの`classifyObservedDiff`が行う)。
|
|
259
259
|
*
|
|
260
|
-
* -
|
|
260
|
+
* - undeclared_write: 宣言write scope外へのwrite(offender=当該TODO)。
|
|
261
261
|
* - observed_write_conflict: 他のrunning TODOのdeclared writeとのpath overlap。
|
|
262
262
|
*/
|
|
263
263
|
export function detectCheckpointFindings(options = {}) {
|
|
@@ -276,7 +276,7 @@ export function detectCheckpointFindings(options = {}) {
|
|
|
276
276
|
const findings = [];
|
|
277
277
|
for (const entry of [...checkpoint.diff.entries].sort((l, r) => (l.path < r.path ? -1 : 1))) {
|
|
278
278
|
if (!coveredBy(packet.scope.writes, entry.path)) {
|
|
279
|
-
findings.push({ kind: '
|
|
279
|
+
findings.push({ kind: 'undeclared_write', todo_ids: [todoId], path: entry.path });
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
for (const otherId of [...runningTodoIds].sort()) {
|
|
@@ -247,6 +247,25 @@ function affectedPayload(raw, expectPath) {
|
|
|
247
247
|
return plainRecord(payload) ? payload : null;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* 収集済みsensor evidenceから、あるpathのaffected testを読む。
|
|
252
|
+
*
|
|
253
|
+
* 実行時の翻訳段(宣言を観測へ合わせる)が同じ観測を必要とする。payloadの形の解釈をあちらへ
|
|
254
|
+
* 書き直すと、driftを判定する規則と、driftを起こさない宣言を作る規則が別々に育つ。
|
|
255
|
+
*
|
|
256
|
+
* @returns {string[]|null} 観測できなければnull(空配列と区別する)
|
|
257
|
+
*/
|
|
258
|
+
export function affectedTestsFromEvidence({ sensorEvidence, querySet, path: target } = {}) {
|
|
259
|
+
const queries = (querySet?.queries ?? [])
|
|
260
|
+
.filter((query) => query.operation === 'affected' && query.target === target);
|
|
261
|
+
if (queries.length !== 1) return null;
|
|
262
|
+
const outcome = (sensorEvidence?.outcomes ?? [])
|
|
263
|
+
.find((entry) => entry.query_id === queries[0].id);
|
|
264
|
+
if (!plainRecord(outcome) || outcome.status !== 'ready') return null;
|
|
265
|
+
const payload = affectedPayload(outcome.raw, target);
|
|
266
|
+
return Array.isArray(payload?.affectedTests) ? [...payload.affectedTests].sort(compareText) : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
250
269
|
function affectedTarget(raw, expectPath) {
|
|
251
270
|
if (!plainRecord(raw) || !Array.isArray(raw.targets)) return null;
|
|
252
271
|
const entry = raw.targets.find((candidate) => (
|
|
@@ -486,28 +505,43 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
486
505
|
}
|
|
487
506
|
|
|
488
507
|
// affected test drift検査(witness宣言とfresh affected観測のexact比較)。
|
|
508
|
+
//
|
|
509
|
+
// 比較はTODO単位で、そのTODOが持つ**全affected束縛の観測の和**に対して行う。1面しか持たない
|
|
510
|
+
// TODOでは束縛ごとの比較と同じ結果になり、複数面を所有するTODOでも表現できる——束縛ごとに
|
|
511
|
+
// 宣言全体とexact比較すると、affectedの異なる2 pathを所有するTODOは原理的に成立せず、
|
|
512
|
+
// 実行時に所有が広がった宣言を受け取れなくなる。和なので緩みは入らない(余分な宣言も
|
|
513
|
+
// 足りない宣言もdriftのまま)。
|
|
489
514
|
const affectedDrift = [];
|
|
490
515
|
for (const todoId of todoIds) {
|
|
491
516
|
const witness = request.manual_witness[todoId];
|
|
517
|
+
const declared = [...witness.affected_tests].sort(compareText);
|
|
518
|
+
const queryIds = [];
|
|
519
|
+
const observedUnion = new Set();
|
|
520
|
+
let unreadable = false;
|
|
492
521
|
for (const binding of bindingsByTodo.get(todoId)) {
|
|
493
522
|
if (binding.expect.kind !== 'affected') continue;
|
|
494
523
|
const outcome = outcomeByQueryId.get(binding.query_id);
|
|
495
524
|
if (resolveBindingStatus(binding, outcome) !== 'ready') continue;
|
|
525
|
+
queryIds.push(binding.query_id);
|
|
496
526
|
const payload = affectedPayload(outcome.raw, binding.expect.path);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
: null;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|| observed.length !== declared.length
|
|
503
|
-
|| observed.some((test, index) => test !== declared[index])) {
|
|
504
|
-
affectedDrift.push({
|
|
505
|
-
todo_id: todoId,
|
|
506
|
-
query_id: binding.query_id,
|
|
507
|
-
declared,
|
|
508
|
-
observed,
|
|
509
|
-
});
|
|
527
|
+
if (!Array.isArray(payload?.affectedTests)) {
|
|
528
|
+
// 観測が読めないのは「一致しない」とは別の事象である。丸めずそのまま残す。
|
|
529
|
+
affectedDrift.push({ todo_id: todoId, query_id: binding.query_id, declared, observed: null });
|
|
530
|
+
unreadable = true;
|
|
531
|
+
continue;
|
|
510
532
|
}
|
|
533
|
+
for (const test of payload.affectedTests) observedUnion.add(test);
|
|
534
|
+
}
|
|
535
|
+
if (unreadable || queryIds.length === 0) continue;
|
|
536
|
+
const observed = [...observedUnion].sort(compareText);
|
|
537
|
+
if (observed.length !== declared.length
|
|
538
|
+
|| observed.some((test, index) => test !== declared[index])) {
|
|
539
|
+
affectedDrift.push({
|
|
540
|
+
todo_id: todoId,
|
|
541
|
+
query_id: queryIds.sort(compareText).join(','),
|
|
542
|
+
declared,
|
|
543
|
+
observed,
|
|
544
|
+
});
|
|
511
545
|
}
|
|
512
546
|
}
|
|
513
547
|
if (affectedDrift.length > 0) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
|
|
2
|
+
// 切断可能性の写像はtodo側と同じ正本を使う。計画時と実行時で「切れる種類」の定義が
|
|
3
|
+
// 分かれると、同じ資源が段によって別の答えを持つ。
|
|
4
|
+
import { severabilityOfConflictKind } from './todo-independence-contracts.mjs';
|
|
2
5
|
import { buildNextRunEvent, buildExecutorPackets } from './runtime-engine.mjs';
|
|
3
6
|
import { projectRuntimeState } from './runtime-projection.mjs';
|
|
4
7
|
import {
|
|
@@ -594,20 +597,46 @@ export function detectNonConvergentConflicts(options = {}) {
|
|
|
594
597
|
));
|
|
595
598
|
}
|
|
596
599
|
|
|
600
|
+
/**
|
|
601
|
+
* 係争資源が構造的に切れる種類かを返す(ADR 0144の続き、請求項8の入口判定)。
|
|
602
|
+
*
|
|
603
|
+
* pathやsymbolは面を分ければ別々に所有できる。共有stateや外部effectは分けられない——
|
|
604
|
+
* 変換をどれだけ工夫しても同じ資源に触り続ける。**これは「切れる」ではなく「切れる種類だ」で
|
|
605
|
+
* あって、実際に切れるかは試して初めて分かる。**
|
|
606
|
+
*/
|
|
607
|
+
export function severabilityOfRuntimeFinding(finding) {
|
|
608
|
+
return severabilityOfConflictKind(typeof finding?.path === 'string' ? 'path' : 'state');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 競合をどちらの処置へ運ぶか(請求項7=直列化/請求項8=変換)。
|
|
613
|
+
*
|
|
614
|
+
* `seam_transform`を返すのは事前宣言済みtreatmentが係争pathを覆っている時だけである。
|
|
615
|
+
* それ以外の既定は直列化だが、**それは「変換が不可能」という意味ではない**。実行時に初めて
|
|
616
|
+
* 見つかった競合には事前宣言が無いので、既定だけを見ると請求項8へ行く道が無いように見える。
|
|
617
|
+
*
|
|
618
|
+
* そこで`severability`と`transform_attemptable`を併せて返す。装置が言えるのは「切れる種類の
|
|
619
|
+
* 資源か」までで、**実際の難しさは変換を試して五条件で測る**(`run seam resolve`)。試すかどうかは
|
|
620
|
+
* 費用のかかる判断なので装置が決めない——隔離worktreeでの変換、focused test、再indexを毎回
|
|
621
|
+
* 走らせるかは、操作するAIが持つ文脈で決める(AGENTS.md「装置の境界にAIを含める」)。
|
|
622
|
+
*/
|
|
597
623
|
export function routeConflictTreatment(options = {}) {
|
|
598
624
|
if (!exactRecord(options, ['finding', 'predeclaredTreatments'])) {
|
|
599
625
|
fail('routeConflictTreatment optionsがexact shapeでない');
|
|
600
626
|
}
|
|
601
627
|
const { finding, predeclaredTreatments } = options;
|
|
602
628
|
if (!plainRecord(finding) || typeof finding.kind !== 'string') fail('findingが不正');
|
|
629
|
+
const severability = severabilityOfRuntimeFinding(finding);
|
|
603
630
|
if (finding.kind === 'observed_write_conflict' && typeof finding.path === 'string') {
|
|
604
631
|
for (const treatment of predeclaredTreatments) {
|
|
605
632
|
if (Array.isArray(treatment.covered_paths) && treatment.covered_paths.includes(finding.path)) {
|
|
606
|
-
return { lane: 'seam_transform', treatment: structuredClone(treatment)
|
|
633
|
+
return { lane: 'seam_transform', treatment: structuredClone(treatment),
|
|
634
|
+
severability, transform_attemptable: true };
|
|
607
635
|
}
|
|
608
636
|
}
|
|
609
637
|
}
|
|
610
|
-
return { lane: 'intentional_serial', treatment: null
|
|
638
|
+
return { lane: 'intentional_serial', treatment: null,
|
|
639
|
+
severability, transform_attemptable: severability === 'code_seam' };
|
|
611
640
|
}
|
|
612
641
|
|
|
613
642
|
/**
|