@quolu/lattice 0.46.2 → 0.48.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/bin/lattice-dashboard.mjs +2 -2
- package/package.json +1 -1
- package/src/artifact-contracts.mjs +0 -3
- package/src/cli-help.mjs +2 -2
- package/src/project-cli.mjs +19 -3
- package/src/rc3-scripted-campaign.mjs +15 -16
- package/src/runtime-cli.mjs +230 -146
- package/src/runtime-contracts.mjs +9 -5
- package/src/runtime-decision-verifier.mjs +26 -5
- package/src/runtime-engine.mjs +48 -5
- package/src/runtime-front-end.mjs +34 -15
- package/src/runtime-hold-recompile.mjs +11 -5
- package/src/runtime-managed-supervisor.mjs +54 -15
- package/src/runtime-scripted-adapter-controller.mjs +43 -19
- package/src/runtime-seam-resolve.mjs +4 -2
- package/src/seam-apply.mjs +3 -3
- package/src/seam-verification.mjs +38 -2
- package/src/todo-audit-pending.mjs +91 -0
- package/src/todo-cli.mjs +111 -18
- package/src/todo-contracts.mjs +84 -14
- package/src/todo-dashboard-registry.mjs +2 -2
- package/src/todo-gantt-html-style.mjs +6 -2
- package/src/todo-gantt-html.mjs +30 -2
- package/src/todo-gantt-scope.mjs +9 -5
- package/src/todo-independence-contracts.mjs +10 -6
- package/src/todo-independence-guidance.mjs +26 -29
- package/src/todo-note-store.mjs +148 -21
- package/src/todo-parallel-candidates.mjs +114 -0
- package/src/todo-status.mjs +254 -11
- package/src/todo-store.mjs +148 -2
- package/src/witness-scaffold.mjs +13 -32
package/src/runtime-engine.mjs
CHANGED
|
@@ -391,6 +391,22 @@ export async function observeExecutor(options = {}) {
|
|
|
391
391
|
if (!validateExecutorReceipt(observation.receipt)) {
|
|
392
392
|
fail(`terminal receiptがexecutor_receipt.v1 contractを満たさない: ${todoId}`);
|
|
393
393
|
}
|
|
394
|
+
if (observation.checkpoint !== undefined) {
|
|
395
|
+
if (!plainRecord(observation.checkpoint)
|
|
396
|
+
|| typeof observation.checkpoint.checkpoint_digest !== 'string'
|
|
397
|
+
|| !/^[0-9a-f]{64}$/.test(observation.checkpoint.checkpoint_digest)) {
|
|
398
|
+
fail(`terminal checkpointにはcheckpoint_digestが必要: ${todoId}`);
|
|
399
|
+
}
|
|
400
|
+
next.push(buildNextRunEvent({
|
|
401
|
+
events: next,
|
|
402
|
+
runId,
|
|
403
|
+
kind: 'checkpoint_observed',
|
|
404
|
+
planEpoch: observation.receipt.plan_epoch,
|
|
405
|
+
subject: { kind: 'todo', ref: todoId },
|
|
406
|
+
payload: structuredClone(observation.checkpoint),
|
|
407
|
+
recordedAt,
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
394
410
|
next.push(buildNextRunEvent({
|
|
395
411
|
events: next,
|
|
396
412
|
runId,
|
|
@@ -481,13 +497,19 @@ export function classifyCheckpointObservation(options = {}) {
|
|
|
481
497
|
const checkpoints = state.checkpoints.filter((entry) => entry.todo_id === todoId);
|
|
482
498
|
if (checkpoints.length === 0) fail(`checkpoint未観測のTODOを分類できない: ${todoId}`);
|
|
483
499
|
const checkpoint = checkpoints[checkpoints.length - 1].payload;
|
|
484
|
-
const { findings } = detect({
|
|
500
|
+
const { findings: detectedFindings } = detect({
|
|
485
501
|
todoId,
|
|
486
502
|
checkpoint,
|
|
487
503
|
packets,
|
|
488
504
|
runningTodoIds: state.running,
|
|
489
505
|
});
|
|
490
|
-
if (!Array.isArray(
|
|
506
|
+
if (!Array.isArray(detectedFindings)) fail('detectがfindings配列を返さない');
|
|
507
|
+
const observations = detectedFindings
|
|
508
|
+
.filter((finding) => finding.kind === 'undeclared_write' && finding.todo_ids?.length === 1)
|
|
509
|
+
.map((finding) => ({ ...finding, kind: 'prediction_excess' }));
|
|
510
|
+
const findings = detectedFindings.filter((finding) => !(
|
|
511
|
+
finding.kind === 'undeclared_write' && finding.todo_ids?.length === 1
|
|
512
|
+
));
|
|
491
513
|
// 再分類のidempotence: 既に保存済みのfinding(kind+todo_ids+path)は再記録しない。
|
|
492
514
|
const recordedKeys = new Set(state.conflicts.map((conflict) => (
|
|
493
515
|
`${conflict.kind}|${[...(conflict.todo_ids ?? [])].sort().join(',')}|${conflict.path ?? ''}`
|
|
@@ -521,7 +543,7 @@ export function classifyCheckpointObservation(options = {}) {
|
|
|
521
543
|
recordedAt,
|
|
522
544
|
}));
|
|
523
545
|
}
|
|
524
|
-
return { events: next, findings: freshFindings };
|
|
546
|
+
return { events: next, findings: freshFindings, observations };
|
|
525
547
|
}
|
|
526
548
|
|
|
527
549
|
const RECEIPT_BINDING_FIELDS = Object.freeze([
|
|
@@ -549,6 +571,25 @@ function witnessProvenReceiptBinding(state, receipt) {
|
|
|
549
571
|
));
|
|
550
572
|
}
|
|
551
573
|
|
|
574
|
+
function retainsOriginBinding(events, receipt, currentEpoch) {
|
|
575
|
+
if (!Number.isSafeInteger(receipt.plan_epoch) || receipt.plan_epoch >= currentEpoch) return false;
|
|
576
|
+
for (let epoch = receipt.plan_epoch; epoch < currentEpoch; epoch += 1) {
|
|
577
|
+
const witnessed = events.some((event) => event.sequence < receipt.sequence
|
|
578
|
+
&& event.kind === 'carry_over_witnessed' && event.plan_epoch === epoch
|
|
579
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === receipt.todo_id);
|
|
580
|
+
const continued = events.some((event) => event.sequence < receipt.sequence
|
|
581
|
+
&& event.kind === 'hold_decided' && event.plan_epoch === epoch
|
|
582
|
+
&& event.payload?.continue_set?.includes(receipt.todo_id));
|
|
583
|
+
const recompiled = events.some((event) => event.sequence < receipt.sequence
|
|
584
|
+
&& event.kind === 'plan_recompiled' && event.plan_epoch === epoch + 1);
|
|
585
|
+
const invalidated = events.some((event) => event.sequence < receipt.sequence
|
|
586
|
+
&& event.kind === 'context_invalidated' && event.plan_epoch === epoch + 1
|
|
587
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === receipt.todo_id);
|
|
588
|
+
if (!witnessed || !continued || !recompiled || invalidated) return false;
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
|
|
552
593
|
export function adjudicatePendingReceipts(options = {}) {
|
|
553
594
|
if (!exactRecord(options, ['runId', 'plan', 'events', 'recordedAt'])) {
|
|
554
595
|
fail('adjudicatePendingReceipts optionsがexact shapeでない');
|
|
@@ -577,6 +618,7 @@ export function adjudicatePendingReceipts(options = {}) {
|
|
|
577
618
|
if (receipt.accepted_sequence !== null || receipt.rejected_sequence !== null) continue;
|
|
578
619
|
const payload = receipt.payload ?? {};
|
|
579
620
|
const dispatch = state.dispatches[receipt.todo_id];
|
|
621
|
+
const originBindingRetained = retainsOriginBinding(events, receipt, plan.plan_epoch);
|
|
580
622
|
let rejection = null;
|
|
581
623
|
if (seenReceiptIds.has(receipt.receipt_id)) {
|
|
582
624
|
rejection = 'duplicate_receipt_id';
|
|
@@ -590,7 +632,7 @@ export function adjudicatePendingReceipts(options = {}) {
|
|
|
590
632
|
rejection = 'binding_mismatch';
|
|
591
633
|
} else if (payload.base_sha !== plan.base_sha) {
|
|
592
634
|
rejection = 'base_mismatch';
|
|
593
|
-
} else if (receipt.plan_epoch !== plan.plan_epoch) {
|
|
635
|
+
} else if (receipt.plan_epoch !== plan.plan_epoch && !originBindingRetained) {
|
|
594
636
|
rejection = 'epoch_mismatch';
|
|
595
637
|
} else if ((() => {
|
|
596
638
|
// dispatchが旧epochのTODOが現epochのreceiptを名乗る場合、epoch_rebound
|
|
@@ -602,7 +644,8 @@ export function adjudicatePendingReceipts(options = {}) {
|
|
|
602
644
|
&& event.subject.ref === receipt.todo_id
|
|
603
645
|
&& event.sequence === dispatch.sequence
|
|
604
646
|
));
|
|
605
|
-
if (
|
|
647
|
+
if (originBindingRetained || dispatchEvent === undefined
|
|
648
|
+
|| dispatchEvent.plan_epoch === plan.plan_epoch) return false;
|
|
606
649
|
const rebound = state.rebinds[receipt.todo_id];
|
|
607
650
|
return rebound === undefined
|
|
608
651
|
|| rebound.payload?.new_plan_epoch !== plan.plan_epoch
|
|
@@ -7,6 +7,7 @@ import { compileSchedulabilityGraphV2 } from './schedulability-compiler-v2.mjs';
|
|
|
7
7
|
import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
|
|
8
8
|
import {
|
|
9
9
|
BOUNDARY_MANIFEST_SCHEMA,
|
|
10
|
+
RUN_REQUEST_SCHEMA,
|
|
10
11
|
SENSOR_EXPECT_KINDS,
|
|
11
12
|
SENSOR_QUERY_OPERATIONS,
|
|
12
13
|
selfDigest,
|
|
@@ -447,6 +448,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
447
448
|
const outcomeByQueryId = normalizeEvidence(sensorEvidence, queryById);
|
|
448
449
|
|
|
449
450
|
const todoIds = request.todos.map((todo) => todo.todo_id);
|
|
451
|
+
const predictionsOnly = request.schema === RUN_REQUEST_SCHEMA;
|
|
450
452
|
|
|
451
453
|
// witnessの束縛を解決する。query set外の参照と、expect↔query targetの
|
|
452
454
|
// 不一致(別targetのreceiptへの再ラベル)はQUERY_DRIFT。
|
|
@@ -458,7 +460,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
458
460
|
const creating = new Set(witness.owns
|
|
459
461
|
.filter((own) => own.creates === true).map((own) => own.target));
|
|
460
462
|
const bindings = normalizeProvenanceQueries(witness, todoId)
|
|
461
|
-
.map((binding) => (creating.size > 0
|
|
463
|
+
.map((binding) => (!predictionsOnly && creating.size > 0
|
|
462
464
|
&& [...creating].some((target) => bindingCoversOwn(binding, { kind: 'path', target }))
|
|
463
465
|
? { ...binding, creates: true } : binding));
|
|
464
466
|
for (const binding of bindings) {
|
|
@@ -498,7 +500,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
498
500
|
for (const todoId of todoIds) {
|
|
499
501
|
for (const binding of bindingsByTodo.get(todoId)) {
|
|
500
502
|
const resolved = resolveBindingStatus(binding, outcomeByQueryId.get(binding.query_id));
|
|
501
|
-
if (resolved !== 'ready') {
|
|
503
|
+
if (!predictionsOnly && resolved !== 'ready') {
|
|
502
504
|
unknowns.push({ todo_id: todoId, kind: `sensor_${resolved}`, ref: binding.query_id });
|
|
503
505
|
}
|
|
504
506
|
}
|
|
@@ -544,7 +546,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
544
546
|
});
|
|
545
547
|
}
|
|
546
548
|
}
|
|
547
|
-
if (affectedDrift.length > 0) {
|
|
549
|
+
if (!predictionsOnly && affectedDrift.length > 0) {
|
|
548
550
|
return nonDispatchable('AFFECTED_TEST_DRIFT', { mismatches: affectedDrift });
|
|
549
551
|
}
|
|
550
552
|
|
|
@@ -570,14 +572,16 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
570
572
|
ambiguous.push({ target: own.target, query_ids: [seen, binding.query_id].sort(compareText) });
|
|
571
573
|
}
|
|
572
574
|
}
|
|
573
|
-
if (covering.length === 0) {
|
|
575
|
+
if (!predictionsOnly && covering.length === 0) {
|
|
574
576
|
unknowns.push({ todo_id: todoId, kind: 'sensor_unbound', ref: `${own.kind}:${own.target}` });
|
|
575
577
|
}
|
|
576
578
|
}
|
|
577
579
|
}
|
|
578
580
|
if (ambiguous.length > 0) return nonDispatchable('QUERY_DRIFT', { ambiguous_targets: ambiguous });
|
|
579
581
|
|
|
580
|
-
//
|
|
582
|
+
// 最新契約ではwrite scopeは予測である。既知の交差はserial conflictへ使うが、
|
|
583
|
+
// 予測が足りないこと自体はunknownへ落とさない。
|
|
584
|
+
const predictedWriteGroups = [];
|
|
581
585
|
for (let left = 0; left < todoIds.length; left += 1) {
|
|
582
586
|
for (let right = left + 1; right < todoIds.length; right += 1) {
|
|
583
587
|
const leftWitness = request.manual_witness[todoIds[left]];
|
|
@@ -591,16 +595,21 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
591
595
|
// resource経由で扱われる。owns解決のない交差だけをunknownにする。
|
|
592
596
|
if (leftOwnPaths.has(leftPath) && rightOwnPaths.has(rightPath)
|
|
593
597
|
&& leftPath === rightPath) continue;
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
598
|
+
if (predictionsOnly) {
|
|
599
|
+
predictedWriteGroups.push({
|
|
600
|
+
target: [leftPath, rightPath].sort(compareText).join(' ↔ '),
|
|
601
|
+
todoIds: [todoIds[left], todoIds[right]],
|
|
602
|
+
});
|
|
603
|
+
} else {
|
|
604
|
+
unknowns.push({
|
|
605
|
+
todo_id: todoIds[left], kind: 'undeclared_write_overlap',
|
|
606
|
+
ref: `${leftPath} ${rightPath}`,
|
|
607
|
+
});
|
|
608
|
+
unknowns.push({
|
|
609
|
+
todo_id: todoIds[right], kind: 'undeclared_write_overlap',
|
|
610
|
+
ref: `${leftPath} ${rightPath}`,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
604
613
|
}
|
|
605
614
|
}
|
|
606
615
|
}
|
|
@@ -661,6 +670,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
661
670
|
if (representative !== undefined) break;
|
|
662
671
|
}
|
|
663
672
|
if (representative === undefined) fail(`covering binding不整合: ${group.own.target}`);
|
|
673
|
+
if (predictionsOnly && resolveBindingStatus(representative, outcome) !== 'ready') continue;
|
|
664
674
|
const status = statusOutcome.status !== 'ready'
|
|
665
675
|
? statusOutcome.status
|
|
666
676
|
: resolveBindingStatus(representative, outcome);
|
|
@@ -722,6 +732,15 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
722
732
|
provenance: [manualProvenance(request)],
|
|
723
733
|
});
|
|
724
734
|
}
|
|
735
|
+
for (const [index, group] of predictedWriteGroups.entries()) {
|
|
736
|
+
resources.push({
|
|
737
|
+
resource_id: `predicted-ww-${sha16(`${group.target}:${index}`)}`,
|
|
738
|
+
kind: 'state',
|
|
739
|
+
target: group.target,
|
|
740
|
+
todo_ids: group.todoIds,
|
|
741
|
+
provenance: [manualProvenance(request)],
|
|
742
|
+
});
|
|
743
|
+
}
|
|
725
744
|
|
|
726
745
|
let dynamicIndex = 0;
|
|
727
746
|
for (const unknown of unknowns) {
|
|
@@ -301,6 +301,11 @@ function computeAffectedClosure(plan, manifests, seedTodoIds) {
|
|
|
301
301
|
return closure;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
/** runtime findingから、停止・再計画する作業群だけを取り出す。 */
|
|
305
|
+
export function affectedTodoIds(plan, manifests, seedTodoIds) {
|
|
306
|
+
return sorted(computeAffectedClosure(plan, manifests, seedTodoIds));
|
|
307
|
+
}
|
|
308
|
+
|
|
304
309
|
/**
|
|
305
310
|
* carry-over witness documentを構築し、提供sourcesに対して自己実証する。
|
|
306
311
|
* 実証できない場合はnullでなくreasons付きの失敗を返す(呼び出し側がholdへ戻す)。
|
|
@@ -754,9 +759,9 @@ export function recompileNextEpochPlan(options = {}) {
|
|
|
754
759
|
recordedAt,
|
|
755
760
|
}));
|
|
756
761
|
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
for (const todoId of sorted(
|
|
762
|
+
// 失効するのは停止・再計画対象だけ。closure外のrunning TODOはorigin bindingのまま
|
|
763
|
+
// 継続するため、contextもpartial patchも無効化しない。
|
|
764
|
+
for (const todoId of sorted(holdDecision.hold_set)) {
|
|
760
765
|
next.push(buildNextRunEvent({
|
|
761
766
|
events: next,
|
|
762
767
|
runId,
|
|
@@ -766,7 +771,7 @@ export function recompileNextEpochPlan(options = {}) {
|
|
|
766
771
|
payload: {
|
|
767
772
|
old_plan_ref: plan.plan_ref,
|
|
768
773
|
invalidated: ['agent_context', 'partial_patch', 'interface_assumption', 'boundary_evidence'],
|
|
769
|
-
reauthorized_via:
|
|
774
|
+
reauthorized_via: 'redispatch',
|
|
770
775
|
},
|
|
771
776
|
recordedAt,
|
|
772
777
|
}));
|
|
@@ -788,7 +793,8 @@ export function recompileNextEpochPlan(options = {}) {
|
|
|
788
793
|
.map((event) => event.sequence)
|
|
789
794
|
.sort((left, right) => left - right)[0] ?? null;
|
|
790
795
|
|
|
791
|
-
//
|
|
796
|
+
// restart時は全process barrierになるため、その場合だけcarry-overを現epochへ復元できる
|
|
797
|
+
// rebind packetをbundleへ残す。通常のrecompileでは使わずorigin bindingを維持する。
|
|
792
798
|
const rebindPackets = {};
|
|
793
799
|
for (const todoId of holdDecision.continue_set) {
|
|
794
800
|
const packet = packets[todoId];
|
|
@@ -262,6 +262,7 @@ export class RuntimeManagedSupervisor {
|
|
|
262
262
|
#gate = null;
|
|
263
263
|
#previousGate = null;
|
|
264
264
|
#releaseAcks = [];
|
|
265
|
+
#gateRecords = new Map();
|
|
265
266
|
#frozen = false;
|
|
266
267
|
#gateGeneration = 0;
|
|
267
268
|
|
|
@@ -321,10 +322,8 @@ export class RuntimeManagedSupervisor {
|
|
|
321
322
|
|| !Number.isSafeInteger(sequence) || sequence <= record.lastHeartbeatSequence || !digest(leaseSetDigest)) {
|
|
322
323
|
await this.#failClosed(record, 'CONTROLLER_HEARTBEAT_EXPIRED', 'heartbeat binding不正');
|
|
323
324
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
.map((entry) => entry.lease.lease_digest).sort());
|
|
327
|
-
if (leaseSetDigest !== expectedLeaseSetDigest) await this.#failClosed(record, 'CONTROLLER_HEARTBEAT_EXPIRED', 'heartbeat lease set不一致');
|
|
325
|
+
// prepare/release応答とsupervisor投影の間には短い非同期窓がある。heartbeatは
|
|
326
|
+
// livenessだけを担い、lease集合はwrite認可時のcentral gate full-chainで照合する。
|
|
328
327
|
record.lastHeartbeat = this.#clock();
|
|
329
328
|
record.lastHeartbeatSequence = sequence;
|
|
330
329
|
await this.#append('controller_heartbeat', { controller_id: controllerId, registration_digest: registrationDigest, sequence, lease_set_digest: leaseSetDigest });
|
|
@@ -366,13 +365,30 @@ export class RuntimeManagedSupervisor {
|
|
|
366
365
|
|
|
367
366
|
/** 全running bindingを一件も省略せずbarrierし、controller ackと独立OS観測を照合する。 */
|
|
368
367
|
async barrierAll({ barrierId, reason, frozenEventDigest }) {
|
|
368
|
+
return this.#barrier({ barrierId, reason, frozenEventDigest, targetTodoIds: null });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** findingの影響群だけを静止し、無関係なrunning bindingはそのまま通す。 */
|
|
372
|
+
async barrierSelected({ barrierId, reason, frozenEventDigest, todoIds }) {
|
|
373
|
+
if (!Array.isArray(todoIds) || todoIds.length === 0
|
|
374
|
+
|| todoIds.some((todoId) => !identifier(todoId))
|
|
375
|
+
|| new Set(todoIds).size !== todoIds.length) {
|
|
376
|
+
fail('HOLD_ACKS_INCOMPLETE', '対象barrierのTODO集合が不正');
|
|
377
|
+
}
|
|
378
|
+
return this.#barrier({ barrierId, reason, frozenEventDigest,
|
|
379
|
+
targetTodoIds: new Set(todoIds) });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async #barrier({ barrierId, reason, frozenEventDigest, targetTodoIds }) {
|
|
369
383
|
const durableBindings = await this.#runningBindingResolver({ runId: this.#runId, frozenEventDigest });
|
|
370
384
|
if (!identifier(barrierId) || typeof reason !== 'string' || reason.length === 0
|
|
371
385
|
|| !digest(frozenEventDigest) || !Array.isArray(durableBindings)
|
|
372
386
|
|| !durableBindings.every(validateRunningBinding)) fail('HOLD_ACKS_INCOMPLETE', 'durable running binding不正');
|
|
373
387
|
await this.assertControllerHealth();
|
|
374
388
|
this.#frozen = true;
|
|
375
|
-
const
|
|
389
|
+
const allRunningBindings = await this.#resolveRunningUnion({ durableBindings, frozenEventDigest });
|
|
390
|
+
const runningBindings = targetTodoIds === null ? allRunningBindings
|
|
391
|
+
: allRunningBindings.filter((binding) => targetTodoIds.has(binding.todo_id));
|
|
376
392
|
const unique = new Set(runningBindings.map((binding) => binding.todo_id));
|
|
377
393
|
if (unique.size !== runningBindings.length) fail('HOLD_ACKS_INCOMPLETE', 'running TODO重複');
|
|
378
394
|
const barrierControlDigest = await this.#append('barrier_requested', { barrier_id: barrierId,
|
|
@@ -413,7 +429,9 @@ export class RuntimeManagedSupervisor {
|
|
|
413
429
|
}
|
|
414
430
|
}
|
|
415
431
|
if (acknowledgements.length !== runningBindings.length) fail('HOLD_ACKS_INCOMPLETE', '未登録controller所有bindingあり');
|
|
416
|
-
const
|
|
432
|
+
const observedAfterBarrier = await this.#collectControllerRunning({ frozenEventDigest });
|
|
433
|
+
const residualBindings = targetTodoIds === null ? observedAfterBarrier
|
|
434
|
+
: observedAfterBarrier.filter((binding) => targetTodoIds.has(binding.todo_id));
|
|
417
435
|
if (residualBindings.length !== 0) {
|
|
418
436
|
fail('HOLD_ACKS_INCOMPLETE', `barrier後もrunning executor残存: ${residualBindings.map((binding) => binding.todo_id).join(',')}`);
|
|
419
437
|
}
|
|
@@ -460,10 +478,14 @@ export class RuntimeManagedSupervisor {
|
|
|
460
478
|
return observed;
|
|
461
479
|
}
|
|
462
480
|
|
|
463
|
-
/** finding/store永続化後に呼ぶtyped hold
|
|
464
|
-
async holdConflict({ findingDigest, frozenEventDigest, barrierId, reason, recordedAt
|
|
481
|
+
/** finding/store永続化後に呼ぶtyped hold閉路。対象群のquiescence完了後だけheldを返す。 */
|
|
482
|
+
async holdConflict({ findingDigest, frozenEventDigest, barrierId, reason, recordedAt,
|
|
483
|
+
todoIds = null }) {
|
|
465
484
|
if (!digest(findingDigest) || typeof recordedAt !== 'string' || Number.isNaN(Date.parse(recordedAt))) fail('FINDING_UNRESOLVED', 'hold finding binding不正');
|
|
466
|
-
const acknowledgements =
|
|
485
|
+
const acknowledgements = todoIds === null
|
|
486
|
+
? await this.barrierAll({ barrierId, reason, frozenEventDigest })
|
|
487
|
+
: await this.barrierSelected({ barrierId, reason, frozenEventDigest,
|
|
488
|
+
todoIds });
|
|
467
489
|
const result = {
|
|
468
490
|
schema: 'lattice.runtime_hold_result.v1', run_id: this.#runId,
|
|
469
491
|
finding_digest: findingDigest, barrier_id: barrierId,
|
|
@@ -635,6 +657,10 @@ export class RuntimeManagedSupervisor {
|
|
|
635
657
|
this.#previousGate = this.#gate;
|
|
636
658
|
this.#gate = gate;
|
|
637
659
|
this.#releaseAcks = releaseAcks.map((ack) => structuredClone(ack));
|
|
660
|
+
this.#gateRecords.set(gate.gate_generation, {
|
|
661
|
+
gate: structuredClone(gate),
|
|
662
|
+
releaseAcks: releaseAcks.map((ack) => structuredClone(ack)),
|
|
663
|
+
});
|
|
638
664
|
this.#gateGeneration = nextGeneration;
|
|
639
665
|
this.#frozen = false;
|
|
640
666
|
return { gate: structuredClone(gate), armedLeases: armed.map((lease) => structuredClone(lease)) };
|
|
@@ -644,19 +670,32 @@ export class RuntimeManagedSupervisor {
|
|
|
644
670
|
async authorizeWrite({ leaseDigest }) {
|
|
645
671
|
await this.assertControllerHealth();
|
|
646
672
|
const entry = this.#leases.get(leaseDigest);
|
|
647
|
-
if (this.#frozen || !entry || entry.revoked || !validateArmedWriteLease(entry.lease)
|
|
673
|
+
if (this.#frozen || !entry || entry.revoked || !validateArmedWriteLease(entry.lease)) fail('RUN_FROZEN', '有効なarmed leaseなし');
|
|
674
|
+
const gateRecord = this.#gateRecords.get(entry.lease.gate_generation);
|
|
675
|
+
const gate = gateRecord?.gate ?? (this.#gate?.gate_generation === entry.lease.gate_generation
|
|
676
|
+
? this.#gate : null);
|
|
677
|
+
const releaseAcks = gateRecord?.releaseAcks ?? (gate === this.#gate ? this.#releaseAcks : []);
|
|
678
|
+
if (gate === null) fail('RUN_FROZEN', 'leaseのorigin gateが無い');
|
|
679
|
+
const previousGate = this.#gateRecords.get(gate.gate_generation - 1)?.gate
|
|
680
|
+
?? (gate === this.#gate ? this.#previousGate : null);
|
|
681
|
+
const gateLeases = [...this.#leases.values()]
|
|
682
|
+
.filter((item) => !item.revoked && validateArmedWriteLease(item.lease)
|
|
683
|
+
&& item.lease.gate_generation === gate.gate_generation)
|
|
684
|
+
.map((item) => item.lease);
|
|
648
685
|
const verified = verifyCentralWriteGate({
|
|
649
|
-
gate
|
|
686
|
+
gate, runId: this.#runId, planEpoch: entry.lease.plan_epoch,
|
|
650
687
|
releaseBarrierDigest: entry.lease.release_barrier_digest, sessionNonceDigest: this.#sessionNonceDigest,
|
|
651
688
|
registrations: [...this.#controllers.values()].map((record) => record.registration),
|
|
652
689
|
controllers: [...this.#controllers.values()].map((record) => record.descriptor),
|
|
653
|
-
releaseAcks
|
|
654
|
-
previousGate
|
|
690
|
+
releaseAcks, armedLeases: gateLeases,
|
|
691
|
+
previousGate,
|
|
655
692
|
});
|
|
656
|
-
|
|
693
|
+
// gate commit時にarmした後続frontierのleaseを壁時計で失効させると、正しく直列待ちした
|
|
694
|
+
// taskほどdispatch不能になる。freshnessはactive gate chainとrevokeで決める。
|
|
695
|
+
if (!verified.valid) {
|
|
657
696
|
entry.revoked = true;
|
|
658
697
|
this.#frozen = true;
|
|
659
|
-
fail('RUN_FROZEN', verified.
|
|
698
|
+
fail('RUN_FROZEN', verified.reason);
|
|
660
699
|
}
|
|
661
700
|
return structuredClone(entry.lease);
|
|
662
701
|
}
|
|
@@ -340,7 +340,8 @@ async function readAndValidateGate(runDir, writeLease) {
|
|
|
340
340
|
*
|
|
341
341
|
* 読むのは3つだけ:
|
|
342
342
|
* - `hold_ms`: 書き込み後にworkerが走り続ける時間。実行時観測が成立する窓を作る。
|
|
343
|
-
* - `extra_writes`:
|
|
343
|
+
* - `extra_writes`: 全TODOに共通する宣言scope外write。
|
|
344
|
+
* - `extra_writes_by_todo`: 競合当事者だけへscope外writeを与える実daemon fixture用設定。
|
|
344
345
|
* - `mode`: 既存の`deterministic`のみ。未知の値は黙って無視せず止める。
|
|
345
346
|
*/
|
|
346
347
|
async function readScriptedBehavior(repoRoot) {
|
|
@@ -351,10 +352,10 @@ async function readScriptedBehavior(repoRoot) {
|
|
|
351
352
|
descriptor = JSON.parse(await readFile(descriptorPath, 'utf8'));
|
|
352
353
|
} catch {
|
|
353
354
|
// 未登録のまま走らせる経路(unit test等)は既定の振る舞いで動かす。
|
|
354
|
-
return { hold_ms: 0, extra_writes: [] };
|
|
355
|
+
return { hold_ms: 0, extra_writes: [], extra_writes_by_todo: {} };
|
|
355
356
|
}
|
|
356
357
|
if (typeof descriptor?.config_ref !== 'string' || typeof descriptor.config_digest !== 'string') {
|
|
357
|
-
return { hold_ms: 0, extra_writes: [] };
|
|
358
|
+
return { hold_ms: 0, extra_writes: [], extra_writes_by_todo: {} };
|
|
358
359
|
}
|
|
359
360
|
const configPath = path.join(repoRoot, ...descriptor.config_ref.split('/'));
|
|
360
361
|
let bytes;
|
|
@@ -387,7 +388,15 @@ async function readScriptedBehavior(repoRoot) {
|
|
|
387
388
|
if (!Array.isArray(extraWrites) || extraWrites.some((entry) => !safeWritePath(entry))) {
|
|
388
389
|
fail('SCRIPTED_BOOTSTRAP_INVALID', 'extra_writesが不正');
|
|
389
390
|
}
|
|
390
|
-
|
|
391
|
+
const extraWritesByTodo = config?.extra_writes_by_todo ?? {};
|
|
392
|
+
if (!plain(extraWritesByTodo) || Object.entries(extraWritesByTodo).some(([todoId, entries]) => (
|
|
393
|
+
!ID.test(todoId) || !Array.isArray(entries) || entries.some((entry) => !safeWritePath(entry))
|
|
394
|
+
))) {
|
|
395
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'extra_writes_by_todoが不正');
|
|
396
|
+
}
|
|
397
|
+
return { hold_ms: holdMs, extra_writes: [...extraWrites],
|
|
398
|
+
extra_writes_by_todo: Object.fromEntries(Object.entries(extraWritesByTodo)
|
|
399
|
+
.map(([todoId, entries]) => [todoId, [...entries]])) };
|
|
391
400
|
}
|
|
392
401
|
|
|
393
402
|
const WORKER_ENTRYPOINT = fileURLToPath(new URL('../bin/lattice-scripted-worker.mjs', import.meta.url));
|
|
@@ -675,20 +684,34 @@ export async function createScriptedAdapterController({
|
|
|
675
684
|
const priorHandle = todoToHandle.get(packet.todo_id);
|
|
676
685
|
if (priorHandle !== undefined) {
|
|
677
686
|
const prior = tasks.get(priorHandle);
|
|
678
|
-
if (prior.packet.packet_digest
|
|
679
|
-
|
|
687
|
+
if (prior.packet.packet_digest === packet.packet_digest
|
|
688
|
+
&& prior.lease.lease_digest === writeLease.lease_digest) {
|
|
689
|
+
return sign({
|
|
690
|
+
schema: 'lattice.adapter_dispatch_response.v2',
|
|
691
|
+
request_id: request.request_id,
|
|
692
|
+
executor_handle: prior.executorHandle,
|
|
693
|
+
worktree_id: prior.worktreeId,
|
|
694
|
+
packet_digest: packet.packet_digest,
|
|
695
|
+
lease_digest: writeLease.lease_digest,
|
|
696
|
+
worker_process: workerProcessOf(prior),
|
|
697
|
+
response_digest: '',
|
|
698
|
+
}, 'response_digest');
|
|
699
|
+
}
|
|
700
|
+
if (prior.state !== 'held') {
|
|
680
701
|
fail('SCRIPTED_DUPLICATE_DISPATCH', '同じTODOへ異なるpacketをdispatchできない');
|
|
681
702
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
703
|
+
// 対象限定holdで失効したattemptだけを畳み、同じTODOのsuccessorを起動する。
|
|
704
|
+
// closure外attemptはtodoToHandleを保持するので、この経路へ入らない。
|
|
705
|
+
try { process.kill(-prior.worker.process_group_id, 'SIGKILL'); }
|
|
706
|
+
catch (error) {
|
|
707
|
+
if (error?.code !== 'ESRCH') {
|
|
708
|
+
fail('SCRIPTED_EXECUTION_FAILED', '旧attemptを終了できない', {
|
|
709
|
+
pid: prior.worker.pid, reason: String(error?.code ?? error?.message ?? error),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
prior.state = 'superseded';
|
|
714
|
+
todoToHandle.delete(packet.todo_id);
|
|
692
715
|
}
|
|
693
716
|
await readAndValidateGate(canonicalRunDir, writeLease);
|
|
694
717
|
// canonical repoではなく自分の木へ書く。共有rootでは、書き込みの帰属をrootから
|
|
@@ -722,7 +745,8 @@ export async function createScriptedAdapterController({
|
|
|
722
745
|
// `detached`で独立process groupへ置く。同じgroupにcontrollerが居ると、直接OS観測が
|
|
723
746
|
// 「未記録process group memberを検出」として正しく落とす。
|
|
724
747
|
const spawned = await spawnScriptedWorker({
|
|
725
|
-
packet, worktreePath: worktreeReal,
|
|
748
|
+
packet, worktreePath: worktreeReal,
|
|
749
|
+
extraWrites: behavior.extra_writes_by_todo[packet.todo_id] ?? behavior.extra_writes,
|
|
726
750
|
holdMs: behavior.hold_ms, controllerId, runDir: canonicalRunDir,
|
|
727
751
|
});
|
|
728
752
|
const task = {
|
|
@@ -1038,9 +1062,9 @@ export async function runScriptedAdapterController({
|
|
|
1038
1062
|
registrationDigest = digest;
|
|
1039
1063
|
if (heartbeatTimer !== null) return;
|
|
1040
1064
|
heartbeatTimer = setInterval(() => {
|
|
1041
|
-
if (
|
|
1065
|
+
if (persistentSocket === null || persistentSocket.destroyed) return;
|
|
1042
1066
|
heartbeatSequence += 1;
|
|
1043
|
-
send(
|
|
1067
|
+
send(persistentSocket, sign({
|
|
1044
1068
|
schema: 'lattice.adapter_controller_heartbeat.v1',
|
|
1045
1069
|
controller_id: controller.controllerId,
|
|
1046
1070
|
registration_digest: registrationDigest,
|
|
@@ -23,6 +23,7 @@ import { affectedTestsFromEvidence } from './runtime-front-end.mjs';
|
|
|
23
23
|
import { explainSeamGate } from './seam-gate.mjs';
|
|
24
24
|
import { collectWitnessSensorEvidence, compileTodoIndependence } from './todo-independence.mjs';
|
|
25
25
|
import { todoSelfDigest } from './todo-contracts.mjs';
|
|
26
|
+
import { TODO_WITNESS_SET_SCHEMA } from './todo-independence-contracts.mjs';
|
|
26
27
|
|
|
27
28
|
export const RUNTIME_SEAM_REQUEST_SCHEMA = 'lattice.runtime_seam_request.v1';
|
|
28
29
|
// v2は翻訳段(`reconciled`)の追加である。宣言を観測へ合わせてから判定するようになったので、
|
|
@@ -184,7 +185,7 @@ export function reconcileWitnessToObservation({
|
|
|
184
185
|
/**
|
|
185
186
|
* 実行時witnessへconcern anchorを足してtodo witness setにする。
|
|
186
187
|
*
|
|
187
|
-
* 実行時のmanual_witnessはconcern_anchorsを持たない(`lattice.run_request.
|
|
188
|
+
* 実行時のmanual_witnessはconcern_anchorsを持たない(`lattice.run_request.v4`)。持たせるのでなく、
|
|
188
189
|
* 宣言から足す——係争資源の中のどのsymbolを触るかは実行時に確定する情報であり、run開始時点の
|
|
189
190
|
* 契約に書けるものではないからである。
|
|
190
191
|
*
|
|
@@ -213,7 +214,7 @@ export function buildRuntimeSeamWitnessSet({
|
|
|
213
214
|
};
|
|
214
215
|
}
|
|
215
216
|
const witnessSet = {
|
|
216
|
-
schema:
|
|
217
|
+
schema: TODO_WITNESS_SET_SCHEMA,
|
|
217
218
|
project_id: SYNTHETIC_PLAN_KEY,
|
|
218
219
|
plan_key: SYNTHETIC_PLAN_KEY,
|
|
219
220
|
capacity: { executors },
|
|
@@ -315,6 +316,7 @@ export async function resolveRuntimeSeam({
|
|
|
315
316
|
latticeBin,
|
|
316
317
|
sharedPathFor: () => declaration.path_names.shared,
|
|
317
318
|
executors: request.capacity.executors,
|
|
319
|
+
precedences: bundle.plan.precedence,
|
|
318
320
|
pathNames,
|
|
319
321
|
compileIndependence: {
|
|
320
322
|
baseArtifact,
|
package/src/seam-apply.mjs
CHANGED
|
@@ -356,7 +356,7 @@ export function seamConflictFromFinding({
|
|
|
356
356
|
*/
|
|
357
357
|
export async function applySeamConflict({
|
|
358
358
|
repoRoot, planKey, conflict, witnessSet, latticeBin, sharedPathFor,
|
|
359
|
-
executors, compileIndependence, pathNames = {},
|
|
359
|
+
executors, compileIndependence, pathNames = {}, precedences = [],
|
|
360
360
|
} = {}) {
|
|
361
361
|
const {
|
|
362
362
|
sourcePath, taskIds, ownedSymbolsByTask, proposedPathByTask, affectedTests,
|
|
@@ -492,10 +492,10 @@ export async function applySeamConflict({
|
|
|
492
492
|
: { targetResolved: !afterPairs.has(targetPair), before: beforePairs.size, after: afterPairs.size },
|
|
493
493
|
waves: {
|
|
494
494
|
before: measureWaveCount({
|
|
495
|
-
taskIds, conflictPairs: [...beforePairs].map((key) => key.split('\0')), executors,
|
|
495
|
+
taskIds, conflictPairs: [...beforePairs].map((key) => key.split('\0')), precedences, executors,
|
|
496
496
|
}).waves,
|
|
497
497
|
after: afterPairs === null ? null : measureWaveCount({
|
|
498
|
-
taskIds, conflictPairs: [...afterPairs].map((key) => key.split('\0')), executors,
|
|
498
|
+
taskIds, conflictPairs: [...afterPairs].map((key) => key.split('\0')), precedences, executors,
|
|
499
499
|
}).waves,
|
|
500
500
|
},
|
|
501
501
|
});
|
|
@@ -111,9 +111,10 @@ export function buildPostTransformWitnessSet({ witnessSet, candidate, affectedTe
|
|
|
111
111
|
* 独自の近似を持たない。変換前後を同じ規則で測らないと、改善したという主張が
|
|
112
112
|
* 測り方の差で出てしまう。
|
|
113
113
|
*/
|
|
114
|
-
export function measureWaveCount({ taskIds, conflictPairs, executors } = {}) {
|
|
114
|
+
export function measureWaveCount({ taskIds, conflictPairs, precedences = [], executors } = {}) {
|
|
115
115
|
const todos = sortedUnique(taskIds ?? []);
|
|
116
116
|
if (todos.length === 0) return { waves: null, reason: 'no_todos' };
|
|
117
|
+
const todoSet = new Set(todos);
|
|
117
118
|
const seen = new Set();
|
|
118
119
|
const conflicts = [];
|
|
119
120
|
for (const pair of conflictPairs ?? []) {
|
|
@@ -124,11 +125,24 @@ export function measureWaveCount({ taskIds, conflictPairs, executors } = {}) {
|
|
|
124
125
|
seen.add(key);
|
|
125
126
|
conflicts.push({ todo_ids: [left, right], resource_id: `pair-${conflicts.length}` });
|
|
126
127
|
}
|
|
128
|
+
const precedenceSeen = new Set();
|
|
129
|
+
const scopedPrecedences = [];
|
|
130
|
+
for (const edge of precedences) {
|
|
131
|
+
const from = edge?.from_todo_id;
|
|
132
|
+
const to = edge?.to_todo_id;
|
|
133
|
+
if (!todoSet.has(from) || !todoSet.has(to) || from === to) continue;
|
|
134
|
+
const reason = typeof edge.reason === 'string' && edge.reason.trim() !== ''
|
|
135
|
+
? edge.reason : 'plan_precedence';
|
|
136
|
+
const key = `${from}\0${to}\0${reason}`;
|
|
137
|
+
if (precedenceSeen.has(key)) continue;
|
|
138
|
+
precedenceSeen.add(key);
|
|
139
|
+
scopedPrecedences.push({ from_todo_id: from, to_todo_id: to, reason });
|
|
140
|
+
}
|
|
127
141
|
const compiled = compileSchedulabilityGraphV2({
|
|
128
142
|
schema_version: GRAPH_SCHEMA,
|
|
129
143
|
todos,
|
|
130
144
|
conflicts,
|
|
131
|
-
precedences:
|
|
145
|
+
precedences: scopedPrecedences,
|
|
132
146
|
unknowns: [],
|
|
133
147
|
capacity: Number.isSafeInteger(executors) && executors >= 1 ? executors : 1,
|
|
134
148
|
});
|
|
@@ -138,6 +152,28 @@ export function measureWaveCount({ taskIds, conflictPairs, executors } = {}) {
|
|
|
138
152
|
return { waves: compiled.plan.minimum_feasible_waves, reason: null };
|
|
139
153
|
}
|
|
140
154
|
|
|
155
|
+
/** todo planの順序制約をschedulability compilerのcanonical edgeへ写す。 */
|
|
156
|
+
export function todoPlanPrecedences(plan) {
|
|
157
|
+
const edges = [];
|
|
158
|
+
for (const edge of plan?.hard_dependencies ?? []) {
|
|
159
|
+
edges.push({
|
|
160
|
+
from_todo_id: edge.from.task_id,
|
|
161
|
+
to_todo_id: edge.to.task_id,
|
|
162
|
+
reason: 'hard_dependency',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
for (const join of plan?.joins ?? []) {
|
|
166
|
+
for (const after of join.after) {
|
|
167
|
+
edges.push({
|
|
168
|
+
from_todo_id: after.task_id,
|
|
169
|
+
to_todo_id: join.before.task_id,
|
|
170
|
+
reason: `join:${join.id}`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return edges;
|
|
175
|
+
}
|
|
176
|
+
|
|
141
177
|
const CONDITIONS = Object.freeze([
|
|
142
178
|
'behavior_equivalent',
|
|
143
179
|
'focused_tests_passed',
|