@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.
Files changed (42) hide show
  1. package/bin/lattice-work-order-adapter.mjs +20 -0
  2. package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -0
  3. package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -0
  4. package/package.json +5 -2
  5. package/src/boundary-observation-compiler-v2.mjs +1 -1
  6. package/src/cli-help.mjs +33 -2
  7. package/src/rc3-actual-dogfood.mjs +6 -2
  8. package/src/rc3-scripted-campaign.mjs +37 -10
  9. package/src/rc4-stage1-dogfood.mjs +6 -2
  10. package/src/runtime-adapter-registry.mjs +21 -7
  11. package/src/runtime-cli.mjs +476 -34
  12. package/src/runtime-contracts.mjs +59 -13
  13. package/src/runtime-controller-protocol.mjs +48 -3
  14. package/src/runtime-decision-verifier.mjs +70 -0
  15. package/src/runtime-diff-observer.mjs +66 -4
  16. package/src/runtime-direct-os-observer.mjs +25 -8
  17. package/src/runtime-driver-state.mjs +162 -0
  18. package/src/runtime-engine.mjs +37 -6
  19. package/src/runtime-front-end.mjs +39 -1
  20. package/src/runtime-managed-supervisor.mjs +80 -14
  21. package/src/runtime-multi-epoch-store.mjs +87 -14
  22. package/src/runtime-pull-intake.mjs +1188 -0
  23. package/src/runtime-work-order-contracts.mjs +91 -0
  24. package/src/runtime-work-order-controller.mjs +1167 -0
  25. package/src/seam-proposal-queries.mjs +1 -1
  26. package/src/todo-cli.mjs +273 -7
  27. package/src/todo-contracts.mjs +19 -2
  28. package/src/todo-gantt-html-independence.mjs +3 -2
  29. package/src/todo-gantt-html-shared.mjs +1 -2
  30. package/src/todo-gantt-html-style.mjs +13 -0
  31. package/src/todo-gantt-html.mjs +15 -2
  32. package/src/todo-gantt-layout.mjs +75 -1
  33. package/src/todo-gantt-nested.mjs +263 -0
  34. package/src/todo-gantt-svg.mjs +80 -5
  35. package/src/todo-independence-contracts.mjs +73 -7
  36. package/src/todo-independence-guidance.mjs +30 -1
  37. package/src/todo-independence.mjs +89 -7
  38. package/src/todo-revision.mjs +1 -1
  39. package/src/todo-split.mjs +472 -0
  40. package/src/todo-status.mjs +10 -1
  41. package/src/todo-store-git-transaction.mjs +418 -0
  42. package/src/todo-store.mjs +144 -4
@@ -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: 既に保存済みのfinding(kind+todo_ids+path)は再記録しない。
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 { events: next, findings: freshFindings, observations };
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 = request.schema === RUN_REQUEST_SCHEMA;
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), transport, lastHeartbeat: now, lastHeartbeatSequence: 0, connected: true, revoked: false };
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
- await this.#append('controller_heartbeat', { controller_id: controllerId, registration_digest: registrationDigest, sequence, lease_set_digest: leaseSetDigest });
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
- await this.#append(operation === 'dispatch' ? 'dispatch_routed' : 'observation_routed', {
361
- controller_id: controllerId, request_digest: request.request_digest, response_digest: response.response_digest,
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(socketPath, timeoutMs) {
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
- if (document?.schema === 'lattice.scripted_adapter_error.v1'
805
- && typeof document.code === 'string'
806
- && typeof document.message === 'string') {
807
- failPending(`${document.code}: ${document.message}`);
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
- Promise.resolve(heartbeatHandler?.(structuredClone(document))).catch(() => socket.destroy());
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 timer = setTimeout(() => { pending.delete(request.request_id); reject(new ManagedRuntimeError('ADAPTER_CONTROLLER_UNAVAILABLE', `${operation} timeout`)); }, timeoutMs);
830
- pending.set(request.request_id, { operation, resolve, reject, timer });
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(handshakeConnectPath, timeoutMs);
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 validateRunMetaV2(meta, createdBundle) {
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 validateCommittedPointer(pointer, bundle) {
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 === bundle.run_id
370
- && pointer.plan_epoch === bundle.plan_epoch
371
- && pointer.plan_ref === bundle.plan.plan_ref
372
- && pointer.bundle_digest === bundle.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
- const pointer = await readRegularJson(path.join(runDir, 'committed-epoch.json'), 'committed epoch pointer');
467
- if (!Number.isInteger(pointer?.plan_epoch) || pointer.plan_epoch < 1) {
468
- fail('INVALID_RUN_STORE', 'committed epoch pointerのepochが不正');
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) {