@quolu/lattice 0.50.1 → 0.52.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-work-order-adapter.mjs +20 -0
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -0
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -0
- package/package.json +5 -2
- package/src/boundary-observation-compiler-v2.mjs +1 -1
- package/src/cli-help.mjs +33 -2
- package/src/rc3-actual-dogfood.mjs +6 -2
- package/src/rc3-scripted-campaign.mjs +37 -10
- package/src/rc4-stage1-dogfood.mjs +6 -2
- package/src/runtime-adapter-registry.mjs +21 -7
- package/src/runtime-cli.mjs +476 -34
- package/src/runtime-contracts.mjs +59 -13
- package/src/runtime-controller-protocol.mjs +48 -3
- package/src/runtime-decision-verifier.mjs +70 -0
- package/src/runtime-diff-observer.mjs +66 -4
- package/src/runtime-direct-os-observer.mjs +25 -8
- package/src/runtime-driver-state.mjs +162 -0
- package/src/runtime-engine.mjs +37 -6
- package/src/runtime-front-end.mjs +39 -1
- package/src/runtime-managed-supervisor.mjs +80 -14
- package/src/runtime-multi-epoch-store.mjs +87 -14
- package/src/runtime-pull-intake.mjs +1188 -0
- package/src/runtime-work-order-contracts.mjs +91 -0
- package/src/runtime-work-order-controller.mjs +1167 -0
- package/src/seam-proposal-queries.mjs +1 -1
- package/src/todo-cli.mjs +273 -7
- package/src/todo-contracts.mjs +19 -2
- package/src/todo-gantt-html-independence.mjs +3 -2
- package/src/todo-gantt-html-shared.mjs +1 -2
- package/src/todo-gantt-html-style.mjs +13 -0
- package/src/todo-gantt-html.mjs +15 -2
- package/src/todo-gantt-layout.mjs +75 -1
- package/src/todo-gantt-nested.mjs +263 -0
- package/src/todo-gantt-svg.mjs +80 -5
- package/src/todo-independence-contracts.mjs +73 -7
- package/src/todo-independence-guidance.mjs +30 -1
- package/src/todo-independence.mjs +89 -7
- package/src/todo-revision.mjs +1 -1
- package/src/todo-split.mjs +472 -0
- package/src/todo-status.mjs +10 -1
- package/src/todo-store-git-transaction.mjs +418 -0
- package/src/todo-store.mjs +144 -4
package/src/runtime-engine.mjs
CHANGED
|
@@ -488,10 +488,10 @@ export async function observeExecutor(options = {}) {
|
|
|
488
488
|
* findingがあればconflict_found+intake_frozenを追記する(RC3-F検出、裁定はRC3-G)。
|
|
489
489
|
*/
|
|
490
490
|
export function classifyCheckpointObservation(options = {}) {
|
|
491
|
-
if (!exactRecord(options, ['runId', 'plan', 'events', 'packets', 'todoId', 'detect', 'recordedAt'])) {
|
|
491
|
+
if (!exactRecord(options, ['runId', 'plan', 'events', 'packets', 'manifests', 'todoId', 'detect', 'recordedAt'])) {
|
|
492
492
|
fail('classifyCheckpointObservation optionsがexact shapeでない');
|
|
493
493
|
}
|
|
494
|
-
const { runId, plan, events, packets, todoId, detect, recordedAt } = options;
|
|
494
|
+
const { runId, plan, events, packets, manifests, todoId, detect, recordedAt } = options;
|
|
495
495
|
if (typeof detect !== 'function') fail('detect関数が必要');
|
|
496
496
|
const state = projectRuntimeState({ events });
|
|
497
497
|
const checkpoints = state.checkpoints.filter((entry) => entry.todo_id === todoId);
|
|
@@ -501,21 +501,47 @@ export function classifyCheckpointObservation(options = {}) {
|
|
|
501
501
|
todoId,
|
|
502
502
|
checkpoint,
|
|
503
503
|
packets,
|
|
504
|
+
manifests,
|
|
504
505
|
runningTodoIds: state.running,
|
|
505
506
|
});
|
|
506
507
|
if (!Array.isArray(detectedFindings)) fail('detectがfindings配列を返さない');
|
|
507
508
|
const observations = detectedFindings
|
|
508
509
|
.filter((finding) => finding.kind === 'undeclared_write' && finding.todo_ids?.length === 1)
|
|
509
510
|
.map((finding) => ({ ...finding, kind: 'prediction_excess' }));
|
|
511
|
+
const scopeExpanded = observations.length === 0 ? [] : (() => {
|
|
512
|
+
const manifest = manifests[todoId];
|
|
513
|
+
const predictedPaths = sortedText(new Set(manifest?.writes ?? []));
|
|
514
|
+
const addedPaths = sortedText(new Set(observations.map((observation) => {
|
|
515
|
+
const location = observation.path ?? observation.resource_id;
|
|
516
|
+
if (typeof location !== 'string' || location.length === 0) {
|
|
517
|
+
fail('prediction_excessにpathまたはresource_idがない');
|
|
518
|
+
}
|
|
519
|
+
return location;
|
|
520
|
+
})));
|
|
521
|
+
const currentPaths = new Set([...predictedPaths, ...addedPaths]);
|
|
522
|
+
const incoming = (plan.precedence ?? [])
|
|
523
|
+
.filter((edge) => edge.to_todo_id === todoId)
|
|
524
|
+
.length;
|
|
525
|
+
return [{
|
|
526
|
+
task_id: todoId,
|
|
527
|
+
compared_witness_digest: null,
|
|
528
|
+
first_seen_path_count: predictedPaths.length,
|
|
529
|
+
path_count: currentPaths.size,
|
|
530
|
+
added_paths: addedPaths,
|
|
531
|
+
removed_paths: [],
|
|
532
|
+
growth_events: 1,
|
|
533
|
+
gate_shape: incoming >= 2,
|
|
534
|
+
}];
|
|
535
|
+
})();
|
|
510
536
|
const findings = detectedFindings.filter((finding) => !(
|
|
511
537
|
finding.kind === 'undeclared_write' && finding.todo_ids?.length === 1
|
|
512
538
|
));
|
|
513
|
-
// 再分類のidempotence:
|
|
539
|
+
// 再分類のidempotence: path/resourceのどちらであっても、同じfindingだけ再記録しない。
|
|
514
540
|
const recordedKeys = new Set(state.conflicts.map((conflict) => (
|
|
515
|
-
`${conflict.kind}|${[...(conflict.todo_ids ?? [])].sort().join(',')}|${conflict.path ?? ''}`
|
|
541
|
+
`${conflict.kind}|${[...(conflict.todo_ids ?? [])].sort().join(',')}|${conflict.path ?? ''}|${conflict.resource_id ?? ''}`
|
|
516
542
|
)));
|
|
517
543
|
const freshFindings = findings.filter((finding) => !recordedKeys.has(
|
|
518
|
-
`${finding.kind}|${[...(finding.todo_ids ?? [])].sort().join(',')}|${finding.path ?? ''}`,
|
|
544
|
+
`${finding.kind}|${[...(finding.todo_ids ?? [])].sort().join(',')}|${finding.path ?? ''}|${finding.resource_id ?? ''}`,
|
|
519
545
|
));
|
|
520
546
|
let next = [...events];
|
|
521
547
|
for (const finding of freshFindings) {
|
|
@@ -543,7 +569,12 @@ export function classifyCheckpointObservation(options = {}) {
|
|
|
543
569
|
recordedAt,
|
|
544
570
|
}));
|
|
545
571
|
}
|
|
546
|
-
return {
|
|
572
|
+
return {
|
|
573
|
+
events: next,
|
|
574
|
+
findings: freshFindings,
|
|
575
|
+
observations,
|
|
576
|
+
scope_expanded: scopeExpanded,
|
|
577
|
+
};
|
|
547
578
|
}
|
|
548
579
|
|
|
549
580
|
const RECEIPT_BINDING_FIELDS = Object.freeze([
|
|
@@ -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_PREDICTION_SCHEMA,
|
|
10
11
|
RUN_REQUEST_SCHEMA,
|
|
11
12
|
SENSOR_EXPECT_KINDS,
|
|
12
13
|
SENSOR_QUERY_OPERATIONS,
|
|
@@ -448,7 +449,8 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
448
449
|
const outcomeByQueryId = normalizeEvidence(sensorEvidence, queryById);
|
|
449
450
|
|
|
450
451
|
const todoIds = request.todos.map((todo) => todo.todo_id);
|
|
451
|
-
const predictionsOnly =
|
|
452
|
+
const predictionsOnly = [RUN_REQUEST_SCHEMA, RUN_REQUEST_PREDICTION_SCHEMA]
|
|
453
|
+
.includes(request.schema);
|
|
452
454
|
|
|
453
455
|
// witnessの束縛を解決する。query set外の参照と、expect↔query targetの
|
|
454
456
|
// 不一致(別targetのreceiptへの再ラベル)はQUERY_DRIFT。
|
|
@@ -642,6 +644,32 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
642
644
|
}
|
|
643
645
|
}
|
|
644
646
|
|
|
647
|
+
// 同じ意味的な線はpathが交差しなくても共有契約である。reads同士だけは並列可とし、
|
|
648
|
+
// 少なくとも片方がwritesのpairを独立したresourceへする。1 lineを全taskの単一resourceへ
|
|
649
|
+
// 丸めるとreads×readsまでconflictになるため、pair単位で実体化する。
|
|
650
|
+
const lineGroups = new Map();
|
|
651
|
+
for (const todoId of todoIds) {
|
|
652
|
+
for (const line of request.manual_witness[todoId].lines ?? []) {
|
|
653
|
+
if (!lineGroups.has(line.line_id)) {
|
|
654
|
+
lineGroups.set(line.line_id, { reads: new Set(), writes: new Set() });
|
|
655
|
+
}
|
|
656
|
+
lineGroups.get(line.line_id)[line.role].add(todoId);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const lineConflictGroups = [];
|
|
660
|
+
for (const [lineId, roles] of [...lineGroups.entries()]
|
|
661
|
+
.sort((left, right) => compareText(left[0], right[0]))) {
|
|
662
|
+
const participants = [...new Set([...roles.reads, ...roles.writes])].sort(compareText);
|
|
663
|
+
for (let left = 0; left < participants.length; left += 1) {
|
|
664
|
+
for (let right = left + 1; right < participants.length; right += 1) {
|
|
665
|
+
const leftId = participants[left];
|
|
666
|
+
const rightId = participants[right];
|
|
667
|
+
if (!roles.writes.has(leftId) && !roles.writes.has(rightId)) continue;
|
|
668
|
+
lineConflictGroups.push({ lineId, todoIds: [leftId, rightId] });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
645
673
|
// state_effect宣言のないbare shared resourceは、方向不明の共有資源として
|
|
646
674
|
// conflict化する(安全と推測しない)。
|
|
647
675
|
const bareResourceGroups = new Map();
|
|
@@ -732,6 +760,15 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
732
760
|
provenance: [manualProvenance(request)],
|
|
733
761
|
});
|
|
734
762
|
}
|
|
763
|
+
for (const group of lineConflictGroups) {
|
|
764
|
+
resources.push({
|
|
765
|
+
resource_id: `line-${sha16(group.lineId)}-${sha16(group.todoIds.join('\0'))}`,
|
|
766
|
+
kind: 'line',
|
|
767
|
+
target: group.lineId,
|
|
768
|
+
todo_ids: group.todoIds,
|
|
769
|
+
provenance: [manualProvenance(request)],
|
|
770
|
+
});
|
|
771
|
+
}
|
|
735
772
|
for (const [index, group] of predictedWriteGroups.entries()) {
|
|
736
773
|
resources.push({
|
|
737
774
|
resource_id: `predicted-ww-${sha16(`${group.target}:${index}`)}`,
|
|
@@ -831,6 +868,7 @@ export function compileRuntimePlanV1(options = {}) {
|
|
|
831
868
|
state_effects: witness.state_effects,
|
|
832
869
|
unknowns: witness.unknowns,
|
|
833
870
|
affected_tests: witness.affected_tests,
|
|
871
|
+
...(witness.lines === undefined ? {} : { lines: witness.lines }),
|
|
834
872
|
graph_evidence: bindingsByTodo.get(todoId).map((binding) => {
|
|
835
873
|
const outcome = outcomeByQueryId.get(binding.query_id);
|
|
836
874
|
return {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
validateAdapterLaunchDescriptor,
|
|
20
20
|
validateAdapterRegistry,
|
|
21
21
|
validateControllerDescriptor,
|
|
22
|
+
validateControllerError,
|
|
22
23
|
validateControllerHeartbeat,
|
|
23
24
|
validateControllerRegistration,
|
|
24
25
|
validateControllerResponse,
|
|
@@ -308,7 +309,9 @@ export class RuntimeManagedSupervisor {
|
|
|
308
309
|
if (this.#controllers.has(descriptor.controller_id)
|
|
309
310
|
|| this.#registrationToController.has(registration.registration_digest)) fail('ADAPTER_CONTROLLER_UNAVAILABLE', 'controller二重登録');
|
|
310
311
|
const now = this.#clock();
|
|
311
|
-
const record = { descriptor: structuredClone(descriptor), registration: structuredClone(registration),
|
|
312
|
+
const record = { descriptor: structuredClone(descriptor), registration: structuredClone(registration),
|
|
313
|
+
transport, lastHeartbeat: now, lastHeartbeatSequence: 0,
|
|
314
|
+
lastRecordedLeaseSetDigest: null, connected: true, revoked: false };
|
|
312
315
|
this.#controllers.set(descriptor.controller_id, record);
|
|
313
316
|
this.#registrationToController.set(registration.registration_digest, descriptor.controller_id);
|
|
314
317
|
await this.#append('controller_registered', { controller_id: descriptor.controller_id, registration_digest: registration.registration_digest });
|
|
@@ -326,7 +329,13 @@ export class RuntimeManagedSupervisor {
|
|
|
326
329
|
// livenessだけを担い、lease集合はwrite認可時のcentral gate full-chainで照合する。
|
|
327
330
|
record.lastHeartbeat = this.#clock();
|
|
328
331
|
record.lastHeartbeatSequence = sequence;
|
|
329
|
-
|
|
332
|
+
// heartbeatはlivenessの更新であり、同じlease集合のtickを全件journalへ積む必要はない。
|
|
333
|
+
// 初回とlease集合の変化だけを耐久記録し、壁時計に比例するjournal成長を止める。
|
|
334
|
+
if (record.lastRecordedLeaseSetDigest !== leaseSetDigest) {
|
|
335
|
+
await this.#append('controller_heartbeat', { controller_id: controllerId,
|
|
336
|
+
registration_digest: registrationDigest, sequence, lease_set_digest: leaseSetDigest });
|
|
337
|
+
record.lastRecordedLeaseSetDigest = leaseSetDigest;
|
|
338
|
+
}
|
|
330
339
|
}
|
|
331
340
|
|
|
332
341
|
async disconnect(controllerId) {
|
|
@@ -357,9 +366,14 @@ export class RuntimeManagedSupervisor {
|
|
|
357
366
|
if (!validateControllerResponse(operation, response, request.request_id)) {
|
|
358
367
|
await this.#failClosed(record, 'ADAPTER_CONTROLLER_UNAVAILABLE', `${operation} response不正`);
|
|
359
368
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
369
|
+
// running pollは状態遷移ではない。全tickを耐久化すると長寿命workerほどcontrol journalを
|
|
370
|
+
// 膨らませ、artifact document上限でsupervisor自身を落とす。dispatchと非running観測だけを残す。
|
|
371
|
+
if (operation === 'dispatch' || response.observation?.state !== 'running') {
|
|
372
|
+
await this.#append(operation === 'dispatch' ? 'dispatch_routed' : 'observation_routed', {
|
|
373
|
+
controller_id: controllerId, request_digest: request.request_digest,
|
|
374
|
+
response_digest: response.response_digest,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
363
377
|
return structuredClone(response);
|
|
364
378
|
}
|
|
365
379
|
|
|
@@ -774,7 +788,15 @@ async function exchangeControllerHandshake({ socketPath, runId, supervisorSessio
|
|
|
774
788
|
});
|
|
775
789
|
}
|
|
776
790
|
|
|
777
|
-
function createControllerSocketTransport(
|
|
791
|
+
function createControllerSocketTransport(
|
|
792
|
+
socketPath,
|
|
793
|
+
timeoutMs,
|
|
794
|
+
dispatchLivenessTimeoutMs = timeoutMs,
|
|
795
|
+
) {
|
|
796
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1
|
|
797
|
+
|| !Number.isSafeInteger(dispatchLivenessTimeoutMs) || dispatchLivenessTimeoutMs < 1) {
|
|
798
|
+
fail('SUPERVISOR_CONFIGURATION_INVALID', 'controller transport timeout不正');
|
|
799
|
+
}
|
|
778
800
|
const socket = net.createConnection({ path: socketPath });
|
|
779
801
|
const pending = new Map();
|
|
780
802
|
let buffer = '';
|
|
@@ -786,6 +808,22 @@ function createControllerSocketTransport(socketPath, timeoutMs) {
|
|
|
786
808
|
socket.once('error', reject);
|
|
787
809
|
});
|
|
788
810
|
socket.setEncoding('utf8');
|
|
811
|
+
const armRequestTimer = (requestId, entry) => {
|
|
812
|
+
clearTimeout(entry.timer);
|
|
813
|
+
const waitMs = entry.operation === 'dispatch' ? dispatchLivenessTimeoutMs : timeoutMs;
|
|
814
|
+
entry.timer = setTimeout(() => {
|
|
815
|
+
pending.delete(requestId);
|
|
816
|
+
entry.reject(new ManagedRuntimeError(
|
|
817
|
+
'ADAPTER_CONTROLLER_UNAVAILABLE',
|
|
818
|
+
`${entry.operation} timeout`,
|
|
819
|
+
));
|
|
820
|
+
}, waitMs);
|
|
821
|
+
};
|
|
822
|
+
const refreshDispatchLiveness = () => {
|
|
823
|
+
for (const [requestId, entry] of pending.entries()) {
|
|
824
|
+
if (entry.operation === 'dispatch') armRequestTimer(requestId, entry);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
789
827
|
const failPending = (detail) => {
|
|
790
828
|
connected = false;
|
|
791
829
|
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new ManagedRuntimeError('ADAPTER_CONTROLLER_UNAVAILABLE', detail)); }
|
|
@@ -801,15 +839,29 @@ function createControllerSocketTransport(socketPath, timeoutMs) {
|
|
|
801
839
|
const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
|
|
802
840
|
let document;
|
|
803
841
|
try { document = JSON.parse(line); } catch { failPending('controller document JSON不正'); socket.destroy(); return; }
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
842
|
+
const errorEntry = pending.get(document?.request_id);
|
|
843
|
+
if (errorEntry && validateControllerError(document, document.request_id)) {
|
|
844
|
+
pending.delete(document.request_id);
|
|
845
|
+
clearTimeout(errorEntry.timer);
|
|
846
|
+
const detail = Object.keys(document.detail).length === 0
|
|
847
|
+
? ''
|
|
848
|
+
: `: ${canonicalizeArtifact(document.detail)}`;
|
|
849
|
+
errorEntry.reject(new ManagedRuntimeError(
|
|
850
|
+
'ADAPTER_CONTROLLER_UNAVAILABLE',
|
|
851
|
+
`${document.code}: ${document.message}${detail}`,
|
|
852
|
+
));
|
|
808
853
|
socket.destroy();
|
|
809
854
|
return;
|
|
810
855
|
}
|
|
811
856
|
if (validateControllerHeartbeat(document)) {
|
|
812
|
-
|
|
857
|
+
if (heartbeatHandler === null) {
|
|
858
|
+
socket.destroy();
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
Promise.resolve()
|
|
862
|
+
.then(() => heartbeatHandler(structuredClone(document)))
|
|
863
|
+
.then(() => refreshDispatchLiveness())
|
|
864
|
+
.catch(() => socket.destroy());
|
|
813
865
|
continue;
|
|
814
866
|
}
|
|
815
867
|
const entry = pending.get(document.request_id);
|
|
@@ -826,14 +878,19 @@ function createControllerSocketTransport(socketPath, timeoutMs) {
|
|
|
826
878
|
await ready;
|
|
827
879
|
if (!connected) fail('ADAPTER_CONTROLLER_UNAVAILABLE', 'controller persistent socket不達');
|
|
828
880
|
return new Promise((resolve, reject) => {
|
|
829
|
-
const
|
|
830
|
-
pending.set(request.request_id,
|
|
881
|
+
const entry = { operation, resolve, reject, timer: null };
|
|
882
|
+
pending.set(request.request_id, entry);
|
|
883
|
+
armRequestTimer(request.request_id, entry);
|
|
831
884
|
socket.write(`${canonicalizeArtifact(request)}\n`);
|
|
832
885
|
});
|
|
833
886
|
},
|
|
834
887
|
});
|
|
835
888
|
}
|
|
836
889
|
|
|
890
|
+
export const runtimeManagedSupervisorInternal = Object.freeze({
|
|
891
|
+
createControllerSocketTransport,
|
|
892
|
+
});
|
|
893
|
+
|
|
837
894
|
/**
|
|
838
895
|
* durable registryをpreflightし、実controller hostとのnonce challenge後にだけactivation証拠を返す。
|
|
839
896
|
* callerは成功返却前にrun storeへ何もpublishしてはならない。
|
|
@@ -987,7 +1044,11 @@ async function activateManagedSupervisorController({ repoRoot, runDir, runId, ad
|
|
|
987
1044
|
supervisorDescriptor.descriptor_digest = selfDigest(supervisorDescriptor, 'descriptor_digest');
|
|
988
1045
|
const activationControlEvent = { schema: 'lattice.runtime_control_event.v1', run_id: runId, sequence: 1, previous_digest: null, kind: 'supervisor_activated', session_nonce_digest: sessionNonceDigest, payload: { supervisor_descriptor_digest: supervisorDescriptor.descriptor_digest, controller_descriptor_digest: controllerDescriptor.descriptor_digest, registration_digest: registration.registration_digest }, recorded_at: supervisorDescriptor.activated_at, event_digest: '' };
|
|
989
1046
|
activationControlEvent.event_digest = selfDigest(activationControlEvent, 'event_digest');
|
|
990
|
-
const controllerTransport = createControllerSocketTransport(
|
|
1047
|
+
const controllerTransport = createControllerSocketTransport(
|
|
1048
|
+
handshakeConnectPath,
|
|
1049
|
+
timeoutMs,
|
|
1050
|
+
controllerDescriptor.heartbeat.ttl_ms,
|
|
1051
|
+
);
|
|
991
1052
|
let disposed = false;
|
|
992
1053
|
const disposeController = async () => {
|
|
993
1054
|
if (disposed) return;
|
|
@@ -1178,6 +1239,11 @@ export async function sendRuntimeActivationRequest({ socketPath, request, expect
|
|
|
1178
1239
|
});
|
|
1179
1240
|
}
|
|
1180
1241
|
|
|
1242
|
+
/** 外部workerの完了を含む初回activationは、同じbootstrap接続で最終結果まで待つ。 */
|
|
1243
|
+
export function sendRuntimeActivationRequestUntilSettled(args) {
|
|
1244
|
+
return sendRuntimeActivationRequest({ ...args, timeoutMs: 0 });
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1181
1247
|
/** stopped/crashed supervisorの同一run明示再起動前処理。live/foreign socketは削除しない。 */
|
|
1182
1248
|
export async function prepareManagedSupervisorRestart({ runDir }) {
|
|
1183
1249
|
if (!path.isAbsolute(runDir) || await realpath(runDir) !== runDir) fail('RUN_NOT_MANAGED', 'restart runDir不正');
|
|
@@ -28,17 +28,51 @@ import {
|
|
|
28
28
|
const HEX_DIGEST = /^[0-9a-f]{64}$/u;
|
|
29
29
|
const EPOCH_DIRECTORY = /^\d{8}$/u;
|
|
30
30
|
const TRANSACTION_ID = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/u;
|
|
31
|
+
const RUNTIME_UPGRADE_COMMAND = 'npm install -g @quolu/lattice@latest --prefer-online';
|
|
31
32
|
|
|
32
33
|
export class RuntimeEpochStoreError extends Error {
|
|
33
|
-
constructor(code, message) {
|
|
34
|
+
constructor(code, message, detail) {
|
|
34
35
|
super(message);
|
|
35
36
|
this.name = 'RuntimeEpochStoreError';
|
|
36
37
|
this.code = code;
|
|
38
|
+
this.detail = detail;
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
function fail(code, message) {
|
|
41
|
-
throw new RuntimeEpochStoreError(code, message);
|
|
42
|
+
function fail(code, message, detail) {
|
|
43
|
+
throw new RuntimeEpochStoreError(code, message, detail);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function schemaGeneration(schema, family) {
|
|
47
|
+
if (typeof schema !== 'string') return null;
|
|
48
|
+
const prefix = `${family}.v`;
|
|
49
|
+
if (!schema.startsWith(prefix)) return null;
|
|
50
|
+
const generationText = schema.slice(prefix.length);
|
|
51
|
+
if (!/^[1-9][0-9]*$/u.test(generationText)) return null;
|
|
52
|
+
const generation = Number.parseInt(generationText, 10);
|
|
53
|
+
return Number.isSafeInteger(generation) ? generation : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function rejectFutureRuntimeStoreSchema(schema, { artifact, family, expectedVersion }) {
|
|
57
|
+
const observedVersion = schemaGeneration(schema, family);
|
|
58
|
+
if (observedVersion === null || observedVersion <= expectedVersion) return;
|
|
59
|
+
const expectedSchema = `${family}.v${expectedVersion}`;
|
|
60
|
+
fail(
|
|
61
|
+
'UNSUPPORTED_RUNTIME_STORE_VERSION',
|
|
62
|
+
`${artifact}のschema ${schema} はこのLatticeが対応する ${expectedSchema} より新しい`,
|
|
63
|
+
{
|
|
64
|
+
artifact,
|
|
65
|
+
observed_schema: schema,
|
|
66
|
+
expected_schema: expectedSchema,
|
|
67
|
+
observed_version: observedVersion,
|
|
68
|
+
expected_version: expectedVersion,
|
|
69
|
+
upgrade_command: RUNTIME_UPGRADE_COMMAND,
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rejectFutureArtifact(value, options) {
|
|
75
|
+
rejectFutureRuntimeStoreSchema(value?.schema, options);
|
|
42
76
|
}
|
|
43
77
|
|
|
44
78
|
function plainRecord(value) {
|
|
@@ -245,6 +279,7 @@ function validateLegacyInputs({ request, compileArtifact, legacyMeta }) {
|
|
|
245
279
|
}
|
|
246
280
|
|
|
247
281
|
function normalizeActivationMeta(meta, request, compileArtifact) {
|
|
282
|
+
rejectFutureArtifact(meta, { artifact: 'run_meta', family: 'lattice.run_meta', expectedVersion: 2 });
|
|
248
283
|
if (meta?.schema === 'lattice.run_meta.v1') return meta;
|
|
249
284
|
if (exactRecord(meta, [
|
|
250
285
|
'schema', 'run_id', 'executor_adapter', 'run_event_schema', 'control_event_schema',
|
|
@@ -348,33 +383,51 @@ export function validatePhaseRevisionCommitReceipt(value) {
|
|
|
348
383
|
&& selfDigestValid(value, 'receipt_digest');
|
|
349
384
|
}
|
|
350
385
|
|
|
351
|
-
function
|
|
386
|
+
function validateRunMetaV2Envelope(meta) {
|
|
352
387
|
return exactRecord(meta, [
|
|
353
388
|
'schema', 'run_id', 'executor_adapter', 'run_event_schema', 'control_event_schema',
|
|
354
389
|
'epoch_bundle_schema', 'created_plan_digest', 'meta_digest',
|
|
355
390
|
]) && meta.schema === 'lattice.run_meta.v2'
|
|
391
|
+
&& typeof meta.run_id === 'string'
|
|
392
|
+
&& typeof meta.executor_adapter === 'string'
|
|
393
|
+
&& typeof meta.run_event_schema === 'string'
|
|
394
|
+
&& typeof meta.control_event_schema === 'string'
|
|
395
|
+
&& typeof meta.epoch_bundle_schema === 'string'
|
|
396
|
+
&& HEX_DIGEST.test(meta.created_plan_digest ?? '')
|
|
397
|
+
&& selfDigestValid(meta, 'meta_digest');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function validateRunMetaV2(meta, createdBundle) {
|
|
401
|
+
return validateRunMetaV2Envelope(meta)
|
|
356
402
|
&& meta.run_id === createdBundle.run_id
|
|
357
403
|
&& meta.run_event_schema === 'lattice.run_event.v1'
|
|
358
404
|
&& meta.control_event_schema === 'lattice.runtime_control_event.v1'
|
|
359
405
|
&& meta.epoch_bundle_schema === 'lattice.runtime_epoch_bundle.v1'
|
|
360
|
-
&& meta.created_plan_digest === createdBundle.plan.plan_digest
|
|
361
|
-
&& selfDigestValid(meta, 'meta_digest');
|
|
406
|
+
&& meta.created_plan_digest === createdBundle.plan.plan_digest;
|
|
362
407
|
}
|
|
363
408
|
|
|
364
|
-
function
|
|
409
|
+
function validateCommittedPointerEnvelope(pointer) {
|
|
365
410
|
return exactRecord(pointer, [
|
|
366
411
|
'schema', 'run_id', 'plan_epoch', 'plan_ref', 'bundle_digest',
|
|
367
412
|
'activation_run_event_digest', 'activation_control_event_digest', 'pointer_digest',
|
|
368
413
|
]) && pointer.schema === 'lattice.committed_epoch_pointer.v1'
|
|
369
|
-
&& pointer.run_id ===
|
|
370
|
-
&& pointer.plan_epoch
|
|
371
|
-
&& pointer.plan_ref ===
|
|
372
|
-
&& pointer.bundle_digest
|
|
414
|
+
&& typeof pointer.run_id === 'string'
|
|
415
|
+
&& Number.isSafeInteger(pointer.plan_epoch) && pointer.plan_epoch >= 1
|
|
416
|
+
&& typeof pointer.plan_ref === 'string'
|
|
417
|
+
&& HEX_DIGEST.test(pointer.bundle_digest ?? '')
|
|
373
418
|
&& HEX_DIGEST.test(pointer.activation_run_event_digest ?? '')
|
|
374
419
|
&& HEX_DIGEST.test(pointer.activation_control_event_digest ?? '')
|
|
375
420
|
&& selfDigestValid(pointer, 'pointer_digest');
|
|
376
421
|
}
|
|
377
422
|
|
|
423
|
+
function validateCommittedPointer(pointer, bundle) {
|
|
424
|
+
return validateCommittedPointerEnvelope(pointer)
|
|
425
|
+
&& pointer.run_id === bundle.run_id
|
|
426
|
+
&& pointer.plan_epoch === bundle.plan_epoch
|
|
427
|
+
&& pointer.plan_ref === bundle.plan.plan_ref
|
|
428
|
+
&& pointer.bundle_digest === bundle.bundle_digest;
|
|
429
|
+
}
|
|
430
|
+
|
|
378
431
|
/** v1 aliasをbyte不変のままepoch 1へ昇格し、pointerを最後にcommitする。 */
|
|
379
432
|
export async function activateEpochOneStore({
|
|
380
433
|
runDir, request, compileArtifact, legacyMeta, activationRunEventDigest, activationControlEventDigest,
|
|
@@ -462,17 +515,35 @@ export async function activateEpochOneStore({
|
|
|
462
515
|
/** pointerを正本としてactive bundleだけを読む。directory最大値へfallbackしない。 */
|
|
463
516
|
export async function readCommittedEpochStore(runDir) {
|
|
464
517
|
const meta = await readRegularJson(path.join(runDir, 'run-meta.json'), 'run meta');
|
|
518
|
+
rejectFutureArtifact(meta, { artifact: 'run_meta', family: 'lattice.run_meta', expectedVersion: 2 });
|
|
465
519
|
if (meta?.schema !== 'lattice.run_meta.v2') return null;
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
520
|
+
if (!validateRunMetaV2Envelope(meta)) fail('INVALID_RUN_STORE', 'run meta envelopeが不正');
|
|
521
|
+
rejectFutureRuntimeStoreSchema(meta.run_event_schema,
|
|
522
|
+
{ artifact: 'run_event', family: 'lattice.run_event', expectedVersion: 1 });
|
|
523
|
+
rejectFutureRuntimeStoreSchema(meta.control_event_schema,
|
|
524
|
+
{ artifact: 'runtime_control_event', family: 'lattice.runtime_control_event', expectedVersion: 1 });
|
|
525
|
+
rejectFutureRuntimeStoreSchema(meta.epoch_bundle_schema,
|
|
526
|
+
{ artifact: 'runtime_epoch_bundle', family: 'lattice.runtime_epoch_bundle', expectedVersion: 1 });
|
|
527
|
+
if (meta.run_event_schema !== 'lattice.run_event.v1'
|
|
528
|
+
|| meta.control_event_schema !== 'lattice.runtime_control_event.v1'
|
|
529
|
+
|| meta.epoch_bundle_schema !== 'lattice.runtime_epoch_bundle.v1') {
|
|
530
|
+
fail('INVALID_RUN_STORE', 'run meta schema bindingが不正');
|
|
469
531
|
}
|
|
532
|
+
const pointer = await readRegularJson(path.join(runDir, 'committed-epoch.json'), 'committed epoch pointer');
|
|
533
|
+
rejectFutureArtifact(pointer, {
|
|
534
|
+
artifact: 'committed_epoch_pointer', family: 'lattice.committed_epoch_pointer', expectedVersion: 1,
|
|
535
|
+
});
|
|
536
|
+
if (!validateCommittedPointerEnvelope(pointer)) fail('INVALID_RUN_STORE', 'committed epoch pointer envelopeが不正');
|
|
470
537
|
const epochName = String(pointer.plan_epoch).padStart(8, '0');
|
|
471
538
|
if (!EPOCH_DIRECTORY.test(epochName)) fail('INVALID_RUN_STORE', 'epoch directory名が不正');
|
|
472
539
|
const bundle = await readRegularJson(path.join(runDir, 'epochs', epochName, 'epoch-bundle.json'), 'epoch bundle');
|
|
540
|
+
rejectFutureArtifact(bundle,
|
|
541
|
+
{ artifact: 'runtime_epoch_bundle', family: 'lattice.runtime_epoch_bundle', expectedVersion: 1 });
|
|
473
542
|
const createdBundle = pointer.plan_epoch === 1
|
|
474
543
|
? bundle
|
|
475
544
|
: await readRegularJson(path.join(runDir, 'epochs', '00000001', 'epoch-bundle.json'), 'created epoch bundle');
|
|
545
|
+
rejectFutureArtifact(createdBundle,
|
|
546
|
+
{ artifact: 'runtime_epoch_bundle', family: 'lattice.runtime_epoch_bundle', expectedVersion: 1 });
|
|
476
547
|
if (!validateRuntimeEpochBundle(bundle)
|
|
477
548
|
|| !validateRuntimeEpochBundle(createdBundle)
|
|
478
549
|
|| createdBundle.plan_epoch !== 1
|
|
@@ -485,6 +556,8 @@ export async function readCommittedEpochStore(runDir) {
|
|
|
485
556
|
const current = epoch === pointer.plan_epoch
|
|
486
557
|
? bundle
|
|
487
558
|
: await readRegularJson(path.join(runDir, 'epochs', String(epoch).padStart(8, '0'), 'epoch-bundle.json'), `epoch ${epoch} bundle`);
|
|
559
|
+
rejectFutureArtifact(current,
|
|
560
|
+
{ artifact: 'runtime_epoch_bundle', family: 'lattice.runtime_epoch_bundle', expectedVersion: 1 });
|
|
488
561
|
if (!validateRuntimeEpochBundle(current) || current.plan_epoch !== epoch
|
|
489
562
|
|| current.run_id !== meta.run_id
|
|
490
563
|
|| current.predecessor_bundle_digest !== previous.bundle_digest) {
|