@sublang/playbook 7.0.0 → 9.0.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 (62) hide show
  1. package/README.md +20 -7
  2. package/docs/cli.md +88 -43
  3. package/docs/configuration.md +221 -119
  4. package/docs/embedding.md +78 -27
  5. package/package.json +4 -3
  6. package/reference/sdlc/captain.playbook/captain.playbook.js +16 -5
  7. package/reference/sdlc/captain.playbook/captain.playbook.ts +20 -6
  8. package/reference/sdlc/code.md +1 -1
  9. package/reference/sdlc/code.playbook/bin/interactive-session.js +816 -0
  10. package/reference/sdlc/code.playbook/bin/launch-config.js +1078 -116
  11. package/reference/sdlc/code.playbook/bin/playbook.js +489 -34
  12. package/reference/sdlc/code.playbook/bin/run.js +283 -298
  13. package/reference/sdlc/code.playbook/bin/session-store.js +818 -26
  14. package/reference/sdlc/code.playbook/code.fsm.d.ts +9 -6
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.js +2 -2
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +2 -2
  17. package/reference/sdlc/code.playbook/code.fsm.js +18 -15
  18. package/reference/sdlc/code.playbook/code.fsm.ts +21 -21
  19. package/reference/sdlc/code.playbook/code.gears.md +1 -1
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +2 -1
  21. package/reference/sdlc/code.playbook/code.playbook.js +25 -15
  22. package/reference/sdlc/code.playbook/code.playbook.ts +34 -17
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +5 -13
  24. package/reference/sdlc/code.playbook/code.registry.js +3 -10
  25. package/reference/sdlc/code.playbook/code.registry.ts +7 -32
  26. package/reference/sdlc/code.playbook/playbook-captain.d.ts +39 -14
  27. package/reference/sdlc/code.playbook/playbook-captain.js +1014 -299
  28. package/reference/sdlc/code.playbook/playbook-captain.ts +1450 -406
  29. package/reference/sdlc/code.playbook/playbook.config.template.yaml +41 -49
  30. package/reference/sdlc/decide.md +4 -4
  31. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +10 -10
  32. package/reference/sdlc/decide.playbook/decide.fsm.js +21 -14
  33. package/reference/sdlc/decide.playbook/decide.fsm.ts +27 -23
  34. package/reference/sdlc/decide.playbook/decide.gears.md +3 -5
  35. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -13
  36. package/reference/sdlc/decide.playbook/decide.playbook.js +465 -246
  37. package/reference/sdlc/decide.playbook/decide.playbook.ts +623 -283
  38. package/reference/sdlc/decide.playbook/decide.registry.d.ts +5 -13
  39. package/reference/sdlc/decide.playbook/decide.registry.js +3 -9
  40. package/reference/sdlc/decide.playbook/decide.registry.ts +7 -31
  41. package/reference/sdlc/review.md +4 -5
  42. package/reference/sdlc/review.playbook/review.fsm.d.ts +9 -11
  43. package/reference/sdlc/review.playbook/review.fsm.js +30 -24
  44. package/reference/sdlc/review.playbook/review.fsm.ts +39 -35
  45. package/reference/sdlc/review.playbook/review.gears.md +6 -5
  46. package/reference/sdlc/review.playbook/review.playbook.d.ts +2 -1
  47. package/reference/sdlc/review.playbook/review.playbook.js +29 -23
  48. package/reference/sdlc/review.playbook/review.playbook.ts +38 -28
  49. package/reference/sdlc/review.playbook/review.registry.d.ts +5 -13
  50. package/reference/sdlc/review.playbook/review.registry.js +3 -16
  51. package/reference/sdlc/review.playbook/review.registry.ts +7 -38
  52. package/slc/gears2fsm.md +45 -24
  53. package/slc/link.md +297 -135
  54. package/slc/text2gears.md +19 -18
  55. package/src/runtime.d.ts +21 -16
  56. package/src/runtime.ts +20 -23
  57. package/src/xstate-playbook-runtime.d.ts +34 -20
  58. package/src/xstate-playbook-runtime.js +973 -400
  59. package/src/xstate-playbook-runtime.ts +1203 -457
  60. package/src/xstate-runtime.d.ts +17 -7
  61. package/src/xstate-runtime.js +198 -81
  62. package/src/xstate-runtime.ts +339 -112
@@ -19,12 +19,24 @@ import {
19
19
  } from 'node:fs/promises';
20
20
  import { homedir, hostname as systemHostname } from 'node:os';
21
21
  import { dirname, isAbsolute, join, resolve } from 'node:path';
22
+ import { fileURLToPath, pathToFileURL } from 'node:url';
23
+ import { isDeepStrictEqual } from 'node:util';
24
+ import { assertSupportedEffort } from '@sublang/cligent';
25
+ import { KNOWN_PLAYER_ADAPTERS } from '@sublang/cligent/tmux-play';
22
26
  import { snapshotJsonValue } from '../../../../src/xstate-runtime.js';
27
+ import { assertPlaybookCaptainShellSnapshot } from '../playbook-captain.js';
23
28
 
24
- export const CAPTAIN_SESSION_RECORD_SCHEMA_VERSION = 2;
29
+ export const CAPTAIN_SESSION_RECORD_SCHEMA_VERSION = 3;
25
30
  export const CAPTAIN_SESSION_RECORD_KIND = 'captain-session';
26
31
  export const SESSION_ID_PATTERN =
27
32
  /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
33
+ export const CAPTAIN_SESSION_STRUCTURAL_PROJECTION_SCHEMA_VERSION = 1;
34
+ export const CAPTAIN_SESSION_EXECUTION_PROJECTION_SCHEMA_VERSION = 2;
35
+
36
+ const PLAYER_ID_PATTERN = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/;
37
+ const ROLE_ID_PATTERN = /^[a-z][a-z0-9_-]*$/;
38
+ const RESERVED_ID = 'captain';
39
+ const KNOWN_ADAPTERS = new Set(KNOWN_PLAYER_ADAPTERS);
28
40
 
29
41
  const COMMON_RECORD_KEYS = [
30
42
  'schemaVersion',
@@ -34,7 +46,8 @@ const COMMON_RECORD_KEYS = [
34
46
  'createdAt',
35
47
  'updatedAt',
36
48
  'cwd',
37
- 'config',
49
+ 'structuralProjection',
50
+ 'lastAppliedExecutionProjection',
38
51
  'snapshot',
39
52
  ];
40
53
  const UNCERTAIN_KEYS = [
@@ -43,6 +56,25 @@ const UNCERTAIN_KEYS = [
43
56
  'attemptId',
44
57
  'attemptNumber',
45
58
  'markedAt',
59
+ 'attemptedExecutionProjection',
60
+ ];
61
+ const RELEASED_SCHEMA_2_COMMON_RECORD_KEYS = [
62
+ 'schemaVersion',
63
+ 'kind',
64
+ 'state',
65
+ 'sessionId',
66
+ 'createdAt',
67
+ 'updatedAt',
68
+ 'cwd',
69
+ 'config',
70
+ 'snapshot',
71
+ ];
72
+ const RELEASED_SCHEMA_2_UNCERTAIN_KEYS = [
73
+ 'baseUpdatedAt',
74
+ 'input',
75
+ 'attemptId',
76
+ 'attemptNumber',
77
+ 'markedAt',
46
78
  ];
47
79
  const LEASE_SCHEMA_VERSION = 1;
48
80
  const LEASE_KIND = 'captain-session-lease';
@@ -68,6 +100,15 @@ const DEFAULT_FS_OPERATIONS = Object.freeze({
68
100
  unlink,
69
101
  });
70
102
 
103
+ class CaptainSessionRecordSchemaError extends Error {
104
+ constructor(schemaVersion, message, cause) {
105
+ super(message);
106
+ this.name = 'CaptainSessionRecordSchemaError';
107
+ this.schemaVersion = schemaVersion;
108
+ this.cause = cause;
109
+ }
110
+ }
111
+
71
112
  export function defaultCaptainSessionsDir(
72
113
  env = process.env,
73
114
  home = env.HOME ?? homedir(),
@@ -130,11 +171,11 @@ export function createCaptainSessionStore(options = {}) {
130
171
  if (cause?.code === 'ENOENT' && missing === 'undefined') return undefined;
131
172
  if (cause?.code === 'ENOENT') {
132
173
  throw new Error(
133
- `Captain session ${JSON.stringify(sessionId)} does not exist`,
174
+ `Captain session ${JSON.stringify(sessionId)} at ${JSON.stringify(path)} does not exist`,
134
175
  );
135
176
  }
136
177
  throw new Error(
137
- `cannot read Captain session ${JSON.stringify(sessionId)}: ${errorMessage(cause)}`,
178
+ `cannot read Captain session ${JSON.stringify(sessionId)} at ${JSON.stringify(path)}: ${errorMessage(cause)}`,
138
179
  );
139
180
  }
140
181
  let value;
@@ -142,13 +183,36 @@ export function createCaptainSessionStore(options = {}) {
142
183
  value = JSON.parse(text);
143
184
  } catch (cause) {
144
185
  throw new Error(
145
- `Captain session ${JSON.stringify(sessionId)} is not valid JSON: ${errorMessage(cause)}`,
186
+ `Captain session ${JSON.stringify(sessionId)} at ${JSON.stringify(path)} is not valid JSON: ${errorMessage(cause)}`,
146
187
  );
147
188
  }
148
- const record = validateCaptainSessionRecord(value);
189
+ let record;
190
+ try {
191
+ record = validateCaptainSessionRecord(value);
192
+ } catch (cause) {
193
+ const context =
194
+ `Captain session ${JSON.stringify(sessionId)} at ` +
195
+ `${JSON.stringify(path)}`;
196
+ if (cause instanceof CaptainSessionRecordSchemaError) {
197
+ if (cause.schemaVersion === 2 && value.sessionId !== sessionId) {
198
+ throw new Error(
199
+ `Captain session file ${JSON.stringify(path)} contains record ` +
200
+ JSON.stringify(value.sessionId),
201
+ );
202
+ }
203
+ throw new CaptainSessionRecordSchemaError(
204
+ cause.schemaVersion,
205
+ `${context}: ${cause.message}`,
206
+ cause,
207
+ );
208
+ }
209
+ throw new Error(`${context} is invalid: ${errorMessage(cause)}`, {
210
+ cause,
211
+ });
212
+ }
149
213
  if (record.sessionId !== sessionId) {
150
214
  throw new Error(
151
- `Captain session file ${JSON.stringify(sessionId)} contains record ` +
215
+ `Captain session file ${JSON.stringify(path)} contains record ` +
152
216
  JSON.stringify(record.sessionId),
153
217
  );
154
218
  }
@@ -157,7 +221,15 @@ export function createCaptainSessionStore(options = {}) {
157
221
 
158
222
  const read = (sessionId) => readRecord(sessionId);
159
223
 
160
- const latest = async () => {
224
+ const latest = async ({ onLegacyRecord } = {}) => {
225
+ if (
226
+ onLegacyRecord !== undefined &&
227
+ typeof onLegacyRecord !== 'function'
228
+ ) {
229
+ throw new Error(
230
+ 'Captain session legacy-record observer must be a function',
231
+ );
232
+ }
161
233
  let names;
162
234
  try {
163
235
  await assertPrivateDirectory(sessionsDir, fs);
@@ -175,7 +247,24 @@ export function createCaptainSessionStore(options = {}) {
175
247
  if (!SESSION_ID_PATTERN.test(sessionId)) continue;
176
248
  // Canonically named records are store-owned. Corruption must not make
177
249
  // --continue silently select an older logical session.
178
- candidates.push(await readRecord(sessionId));
250
+ try {
251
+ candidates.push(await readRecord(sessionId));
252
+ } catch (error) {
253
+ if (
254
+ error instanceof CaptainSessionRecordSchemaError &&
255
+ error.schemaVersion === 2
256
+ ) {
257
+ await onLegacyRecord?.(
258
+ Object.freeze({
259
+ sessionId,
260
+ path: recordPathFor(sessionId),
261
+ schemaVersion: 2,
262
+ }),
263
+ );
264
+ continue;
265
+ }
266
+ throw error;
267
+ }
179
268
  }
180
269
  candidates.sort((left, right) => {
181
270
  const byUpdated = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
@@ -647,10 +736,55 @@ function createLease({
647
736
  return record;
648
737
  });
649
738
 
650
- const beginTurn = ({ input, attemptId, fresh } = {}) =>
739
+ const initializeSettled = ({
740
+ cwd,
741
+ structuralProjection,
742
+ executionProjection,
743
+ snapshot,
744
+ } = {}) =>
745
+ runExclusive(async () => {
746
+ const initial = validateFreshBoundary({
747
+ cwd,
748
+ structuralProjection,
749
+ snapshot,
750
+ });
751
+ const applied = assertCaptainSessionExecutionCompatible(
752
+ initial.structuralProjection,
753
+ executionProjection,
754
+ );
755
+ const createdAt = timestampFrom(now(), 'session timestamp');
756
+ const updatedAt = nextTimestamp(now(), createdAt);
757
+ const record = validateCaptainSessionRecord({
758
+ schemaVersion: CAPTAIN_SESSION_RECORD_SCHEMA_VERSION,
759
+ kind: CAPTAIN_SESSION_RECORD_KIND,
760
+ state: 'settled',
761
+ sessionId,
762
+ createdAt,
763
+ updatedAt,
764
+ cwd: initial.cwd,
765
+ structuralProjection: initial.structuralProjection,
766
+ lastAppliedExecutionProjection: applied,
767
+ snapshot: initial.snapshot,
768
+ });
769
+ await assertOwnerUnchecked();
770
+ await writeRecord(record, { noReplace: true });
771
+ await assertOwnerUnchecked();
772
+ return record;
773
+ });
774
+
775
+ const beginTurn = ({
776
+ input,
777
+ attemptId,
778
+ attemptedExecutionProjection,
779
+ fresh,
780
+ } = {}) =>
651
781
  runExclusive(async () => {
652
782
  assertAcceptedInput(input);
653
783
  assertUuid(attemptId, 'Captain session attempt id');
784
+ const attempted = validateCaptainSessionExecutionProjection(
785
+ attemptedExecutionProjection,
786
+ 'Captain session attempted execution projection',
787
+ );
654
788
  await assertOwnerUnchecked();
655
789
  const prior = await readRecord(sessionId, { missing: 'undefined' });
656
790
  let record;
@@ -668,7 +802,8 @@ function createLease({
668
802
  createdAt: timestamp,
669
803
  updatedAt: timestamp,
670
804
  cwd: initial.cwd,
671
- config: initial.config,
805
+ structuralProjection: initial.structuralProjection,
806
+ lastAppliedExecutionProjection: attempted,
672
807
  snapshot: initial.snapshot,
673
808
  uncertain: {
674
809
  baseUpdatedAt: null,
@@ -676,6 +811,7 @@ function createLease({
676
811
  attemptId,
677
812
  attemptNumber: 1,
678
813
  markedAt: timestamp,
814
+ attemptedExecutionProjection: attempted,
679
815
  },
680
816
  });
681
817
  await assertOwnerUnchecked();
@@ -696,7 +832,9 @@ function createLease({
696
832
  createdAt: prior.createdAt,
697
833
  updatedAt: timestamp,
698
834
  cwd: prior.cwd,
699
- config: prior.config,
835
+ structuralProjection: prior.structuralProjection,
836
+ lastAppliedExecutionProjection:
837
+ prior.lastAppliedExecutionProjection,
700
838
  snapshot: prior.snapshot,
701
839
  uncertain: {
702
840
  baseUpdatedAt: prior.updatedAt,
@@ -704,6 +842,7 @@ function createLease({
704
842
  attemptId,
705
843
  attemptNumber: 1,
706
844
  markedAt: timestamp,
845
+ attemptedExecutionProjection: attempted,
707
846
  },
708
847
  });
709
848
  await assertOwnerUnchecked();
@@ -737,7 +876,9 @@ function createLease({
737
876
  createdAt: prior.createdAt,
738
877
  updatedAt: timestamp,
739
878
  cwd: prior.cwd,
740
- config: prior.config,
879
+ structuralProjection: prior.structuralProjection,
880
+ lastAppliedExecutionProjection:
881
+ prior.lastAppliedExecutionProjection,
741
882
  snapshot: prior.snapshot,
742
883
  uncertain: {
743
884
  baseUpdatedAt: prior.uncertain.baseUpdatedAt,
@@ -745,6 +886,8 @@ function createLease({
745
886
  attemptId: nextAttemptId,
746
887
  attemptNumber: prior.uncertain.attemptNumber + 1,
747
888
  markedAt: timestamp,
889
+ attemptedExecutionProjection:
890
+ prior.uncertain.attemptedExecutionProjection,
748
891
  },
749
892
  });
750
893
  await assertOwnerUnchecked();
@@ -770,7 +913,9 @@ function createLease({
770
913
  createdAt: prior.createdAt,
771
914
  updatedAt: timestamp,
772
915
  cwd: prior.cwd,
773
- config: prior.config,
916
+ structuralProjection: prior.structuralProjection,
917
+ lastAppliedExecutionProjection:
918
+ prior.uncertain.attemptedExecutionProjection,
774
919
  snapshot,
775
920
  });
776
921
  await assertOwnerUnchecked();
@@ -803,7 +948,9 @@ function createLease({
803
948
  createdAt: prior.createdAt,
804
949
  updatedAt: prior.uncertain.baseUpdatedAt,
805
950
  cwd: prior.cwd,
806
- config: prior.config,
951
+ structuralProjection: prior.structuralProjection,
952
+ lastAppliedExecutionProjection:
953
+ prior.lastAppliedExecutionProjection,
807
954
  snapshot: prior.snapshot,
808
955
  });
809
956
  await writeRecord(record, { noReplace: false });
@@ -822,6 +969,7 @@ function createLease({
822
969
  sessionId,
823
970
  ownerToken: owner.ownerToken,
824
971
  read,
972
+ initializeSettled,
825
973
  beginTurn,
826
974
  beginRetry,
827
975
  settle,
@@ -831,11 +979,115 @@ function createLease({
831
979
  });
832
980
  }
833
981
 
982
+ export function validateCaptainSessionExecutionProjection(
983
+ value,
984
+ path = 'Captain session execution projection',
985
+ ) {
986
+ const projection = requireRecord(snapshotJsonValue(value, path), path);
987
+ validateCaptainSessionProjection(projection, { path, structural: false });
988
+ return projection;
989
+ }
990
+
991
+ export function validateCaptainSessionStructuralProjection(
992
+ value,
993
+ path = 'Captain session structural projection',
994
+ ) {
995
+ const projection = requireRecord(snapshotJsonValue(value, path), path);
996
+ validateCaptainSessionProjection(projection, { path, structural: true });
997
+ return projection;
998
+ }
999
+
1000
+ export function projectCaptainSessionStructure(value) {
1001
+ const execution = validateCaptainSessionExecutionProjection(value);
1002
+ const fixedAgent = (agent) => ({
1003
+ adapter: agent.adapter,
1004
+ ...(agent.instruction === undefined
1005
+ ? {}
1006
+ : { instruction: agent.instruction }),
1007
+ ...(agent.permissions === undefined
1008
+ ? {}
1009
+ : { permissions: agent.permissions }),
1010
+ });
1011
+ return validateCaptainSessionStructuralProjection({
1012
+ schemaVersion: CAPTAIN_SESSION_STRUCTURAL_PROJECTION_SCHEMA_VERSION,
1013
+ captain: fixedAgent(execution.captain),
1014
+ players: execution.players.map(({ id, ...agent }) => ({
1015
+ id,
1016
+ ...fixedAgent(agent),
1017
+ })),
1018
+ catalog: Object.fromEntries(
1019
+ Object.entries(execution.catalog).map(([id, item]) => [
1020
+ id,
1021
+ {
1022
+ id: item.id,
1023
+ from: item.from,
1024
+ manifestCommand: item.manifestCommand,
1025
+ command: item.command,
1026
+ intent: item.intent,
1027
+ artifactSchema: item.artifactSchema,
1028
+ requiredRoleIds: item.requiredRoleIds,
1029
+ concurrentRoleSets: item.concurrentRoleSets,
1030
+ roles: Object.fromEntries(
1031
+ Object.entries(item.roles).map(([roleId, binding]) => [
1032
+ roleId,
1033
+ { playerId: binding.playerId },
1034
+ ]),
1035
+ ),
1036
+ options: item.options,
1037
+ },
1038
+ ]),
1039
+ ),
1040
+ });
1041
+ }
1042
+
1043
+ export function assertCaptainSessionExecutionCompatible(
1044
+ structuralProjection,
1045
+ executionProjection,
1046
+ ) {
1047
+ const structural = validateCaptainSessionStructuralProjection(
1048
+ structuralProjection,
1049
+ );
1050
+ const execution = validateCaptainSessionExecutionProjection(
1051
+ executionProjection,
1052
+ );
1053
+ const projected = projectCaptainSessionStructure(execution);
1054
+ if (!isDeepStrictEqual(projected, structural)) {
1055
+ throw new Error(
1056
+ 'Captain session execution projection does not reproduce the stored structural projection',
1057
+ );
1058
+ }
1059
+ return execution;
1060
+ }
1061
+
1062
+ export function captainSessionSelectedMembers(value) {
1063
+ const structural = validateCaptainSessionStructuralProjection(value);
1064
+ return snapshotJsonValue(
1065
+ {
1066
+ playbookIds: Object.keys(structural.catalog),
1067
+ playerIds: referencedPlayerIds(structural.catalog),
1068
+ },
1069
+ 'Captain session selected members',
1070
+ );
1071
+ }
1072
+
834
1073
  export function validateCaptainSessionRecord(value) {
835
1074
  const record = requireRecord(
836
1075
  snapshotJsonValue(value, 'Captain session record'),
837
1076
  'Captain session record',
838
1077
  );
1078
+ if (record.schemaVersion !== CAPTAIN_SESSION_RECORD_SCHEMA_VERSION) {
1079
+ if (record.schemaVersion === 2) {
1080
+ assertReleasedSchema2CaptainSessionRecord(record);
1081
+ throw new CaptainSessionRecordSchemaError(
1082
+ record.schemaVersion,
1083
+ 'Captain session record schema 2 has incompatible root-owned player identity; schema 3 is required',
1084
+ );
1085
+ }
1086
+ throw new CaptainSessionRecordSchemaError(
1087
+ record.schemaVersion,
1088
+ `Captain session record schema ${JSON.stringify(record.schemaVersion)} is not supported`,
1089
+ );
1090
+ }
839
1091
  if (record.state !== 'settled' && record.state !== 'uncertain') {
840
1092
  throw new Error('Captain session record state is not supported');
841
1093
  }
@@ -846,11 +1098,6 @@ export function validateCaptainSessionRecord(value) {
846
1098
  : COMMON_RECORD_KEYS,
847
1099
  'Captain session record',
848
1100
  );
849
- if (record.schemaVersion !== CAPTAIN_SESSION_RECORD_SCHEMA_VERSION) {
850
- throw new Error(
851
- `Captain session record schema ${JSON.stringify(record.schemaVersion)} is not supported`,
852
- );
853
- }
854
1101
  if (record.kind !== CAPTAIN_SESSION_RECORD_KIND) {
855
1102
  throw new Error('Captain session record kind is not supported');
856
1103
  }
@@ -871,8 +1118,25 @@ export function validateCaptainSessionRecord(value) {
871
1118
  if (resolve(record.cwd) !== record.cwd) {
872
1119
  throw new Error('Captain session record cwd must be normalized');
873
1120
  }
874
- requireRecord(record.config, 'Captain session record config');
875
- requireRecord(record.snapshot, 'Captain session record snapshot');
1121
+ const structural = validateCaptainSessionStructuralProjection(
1122
+ record.structuralProjection,
1123
+ 'Captain session record structuralProjection',
1124
+ );
1125
+ const lastApplied = assertCaptainSessionExecutionCompatible(
1126
+ structural,
1127
+ record.lastAppliedExecutionProjection,
1128
+ );
1129
+ void lastApplied;
1130
+ const snapshot = assertPlaybookCaptainShellSnapshot(record.snapshot);
1131
+ assertSnapshotMatchesStructure(snapshot, structural);
1132
+ if (
1133
+ snapshot.captain.sessionId === record.sessionId ||
1134
+ snapshot.issuedSessionIds.includes(record.sessionId)
1135
+ ) {
1136
+ throw new Error(
1137
+ 'Captain session public id collides with an internal Captain session id',
1138
+ );
1139
+ }
876
1140
 
877
1141
  if (record.state === 'uncertain') {
878
1142
  const uncertain = requireRecord(
@@ -887,7 +1151,7 @@ export function validateCaptainSessionRecord(value) {
887
1151
  if (uncertain.baseUpdatedAt !== null) {
888
1152
  canonicalTimestamp(uncertain.baseUpdatedAt, 'uncertain.baseUpdatedAt');
889
1153
  if (
890
- Date.parse(uncertain.baseUpdatedAt) < Date.parse(createdAt) ||
1154
+ Date.parse(uncertain.baseUpdatedAt) <= Date.parse(createdAt) ||
891
1155
  Date.parse(uncertain.baseUpdatedAt) >= Date.parse(updatedAt)
892
1156
  ) {
893
1157
  throw new Error(
@@ -919,11 +1183,107 @@ export function validateCaptainSessionRecord(value) {
919
1183
  'Captain session uncertain markedAt must equal updatedAt',
920
1184
  );
921
1185
  }
1186
+ const attempted = assertCaptainSessionExecutionCompatible(
1187
+ structural,
1188
+ uncertain.attemptedExecutionProjection,
1189
+ );
1190
+ if (uncertain.baseUpdatedAt === null) {
1191
+ if (!isDeepStrictEqual(lastApplied, attempted)) {
1192
+ throw new Error(
1193
+ 'fresh Captain session baseline and attempted execution projections must match',
1194
+ );
1195
+ }
1196
+ assertTurnZeroSnapshot(
1197
+ snapshot,
1198
+ 'fresh Captain session record snapshot',
1199
+ );
1200
+ }
922
1201
  }
923
1202
 
924
1203
  return record;
925
1204
  }
926
1205
 
1206
+ function assertReleasedSchema2CaptainSessionRecord(record) {
1207
+ if (record.state !== 'settled' && record.state !== 'uncertain') {
1208
+ throw new Error('Captain session record state is not supported');
1209
+ }
1210
+ rejectUnknownOrMissingKeys(
1211
+ record,
1212
+ record.state === 'uncertain'
1213
+ ? [...RELEASED_SCHEMA_2_COMMON_RECORD_KEYS, 'uncertain']
1214
+ : RELEASED_SCHEMA_2_COMMON_RECORD_KEYS,
1215
+ 'Captain session record',
1216
+ );
1217
+ if (record.kind !== CAPTAIN_SESSION_RECORD_KIND) {
1218
+ throw new Error('Captain session record kind is not supported');
1219
+ }
1220
+ assertSessionId(record.sessionId);
1221
+ const createdAt = canonicalTimestamp(record.createdAt, 'createdAt');
1222
+ const updatedAt = canonicalTimestamp(record.updatedAt, 'updatedAt');
1223
+ if (Date.parse(updatedAt) < Date.parse(createdAt)) {
1224
+ throw new Error('Captain session record updatedAt precedes createdAt');
1225
+ }
1226
+ if (record.state === 'settled' && updatedAt === createdAt) {
1227
+ throw new Error(
1228
+ 'settled Captain session updatedAt must follow its creation marker',
1229
+ );
1230
+ }
1231
+ if (typeof record.cwd !== 'string' || !isAbsolute(record.cwd)) {
1232
+ throw new Error('Captain session record cwd must be an absolute path');
1233
+ }
1234
+ if (resolve(record.cwd) !== record.cwd) {
1235
+ throw new Error('Captain session record cwd must be normalized');
1236
+ }
1237
+ requireRecord(record.config, 'Captain session record config');
1238
+ requireRecord(record.snapshot, 'Captain session record snapshot');
1239
+
1240
+ if (record.state !== 'uncertain') return;
1241
+ const uncertain = requireRecord(
1242
+ record.uncertain,
1243
+ 'Captain session record uncertain',
1244
+ );
1245
+ rejectUnknownOrMissingKeys(
1246
+ uncertain,
1247
+ RELEASED_SCHEMA_2_UNCERTAIN_KEYS,
1248
+ 'Captain session record uncertain',
1249
+ );
1250
+ if (uncertain.baseUpdatedAt !== null) {
1251
+ canonicalTimestamp(uncertain.baseUpdatedAt, 'uncertain.baseUpdatedAt');
1252
+ if (
1253
+ Date.parse(uncertain.baseUpdatedAt) < Date.parse(createdAt) ||
1254
+ Date.parse(uncertain.baseUpdatedAt) >= Date.parse(updatedAt)
1255
+ ) {
1256
+ throw new Error(
1257
+ 'Captain session uncertain baseUpdatedAt must identify an earlier settled boundary',
1258
+ );
1259
+ }
1260
+ } else {
1261
+ const isFirstAttempt = uncertain.attemptNumber === 1;
1262
+ if (isFirstAttempt !== (updatedAt === createdAt)) {
1263
+ throw new Error(
1264
+ 'fresh Captain session retry timestamps must match the attempt boundary',
1265
+ );
1266
+ }
1267
+ }
1268
+ assertAcceptedInput(uncertain.input);
1269
+ assertUuid(uncertain.attemptId, 'Captain session attempt id');
1270
+ if (
1271
+ !Number.isSafeInteger(uncertain.attemptNumber) ||
1272
+ uncertain.attemptNumber <= 0
1273
+ ) {
1274
+ throw new Error('Captain session attempt number must be a positive integer');
1275
+ }
1276
+ const markedAt = canonicalTimestamp(
1277
+ uncertain.markedAt,
1278
+ 'uncertain.markedAt',
1279
+ );
1280
+ if (markedAt !== updatedAt) {
1281
+ throw new Error(
1282
+ 'Captain session uncertain markedAt must equal updatedAt',
1283
+ );
1284
+ }
1285
+ }
1286
+
927
1287
  function validateFreshBoundary(value) {
928
1288
  const boundary = requireRecord(
929
1289
  snapshotJsonValue(value, 'fresh Captain session boundary'),
@@ -931,7 +1291,7 @@ function validateFreshBoundary(value) {
931
1291
  );
932
1292
  rejectUnknownOrMissingKeys(
933
1293
  boundary,
934
- ['cwd', 'config', 'snapshot'],
1294
+ ['cwd', 'structuralProjection', 'snapshot'],
935
1295
  'fresh Captain session boundary',
936
1296
  );
937
1297
  if (typeof boundary.cwd !== 'string' || !isAbsolute(boundary.cwd)) {
@@ -940,11 +1300,384 @@ function validateFreshBoundary(value) {
940
1300
  if (resolve(boundary.cwd) !== boundary.cwd) {
941
1301
  throw new Error('fresh Captain session cwd must be normalized');
942
1302
  }
943
- requireRecord(boundary.config, 'fresh Captain session config');
944
- requireRecord(boundary.snapshot, 'fresh Captain session snapshot');
1303
+ const structural = validateCaptainSessionStructuralProjection(
1304
+ boundary.structuralProjection,
1305
+ 'fresh Captain session structuralProjection',
1306
+ );
1307
+ const snapshot = assertPlaybookCaptainShellSnapshot(boundary.snapshot);
1308
+ assertSnapshotMatchesStructure(snapshot, structural);
1309
+ assertTurnZeroSnapshot(snapshot, 'fresh Captain session boundary snapshot');
945
1310
  return boundary;
946
1311
  }
947
1312
 
1313
+ function assertTurnZeroSnapshot(snapshot, path) {
1314
+ if (
1315
+ snapshot.sequences.turn !== 0 ||
1316
+ snapshot.sequences.journal !== 0 ||
1317
+ snapshot.journal.length !== 0 ||
1318
+ snapshot.captain.conversation.kind !== 'unopened'
1319
+ ) {
1320
+ throw new Error(`${path} must be an initialized turn-zero shell snapshot`);
1321
+ }
1322
+ }
1323
+
1324
+ function validateCaptainSessionProjection(
1325
+ projection,
1326
+ { path, structural },
1327
+ ) {
1328
+ rejectUnknownOrMissingKeys(
1329
+ projection,
1330
+ ['schemaVersion', 'captain', 'players', 'catalog'],
1331
+ path,
1332
+ );
1333
+ const expectedSchema = structural
1334
+ ? CAPTAIN_SESSION_STRUCTURAL_PROJECTION_SCHEMA_VERSION
1335
+ : CAPTAIN_SESSION_EXECUTION_PROJECTION_SCHEMA_VERSION;
1336
+ if (projection.schemaVersion !== expectedSchema) {
1337
+ throw new Error(
1338
+ `${path}.schemaVersion ${JSON.stringify(projection.schemaVersion)} is not supported (expected ${expectedSchema})`,
1339
+ );
1340
+ }
1341
+ validateProjectedAgent(projection.captain, `${path}.captain`, {
1342
+ structural,
1343
+ });
1344
+ if (!Array.isArray(projection.players)) {
1345
+ throw new Error(`${path}.players must be an array`);
1346
+ }
1347
+ const playerIds = [];
1348
+ for (let index = 0; index < projection.players.length; index += 1) {
1349
+ const playerPath = `${path}.players[${index}]`;
1350
+ const player = requireRecord(projection.players[index], playerPath);
1351
+ exactOptionalKeys(
1352
+ player,
1353
+ structural
1354
+ ? ['id', 'adapter']
1355
+ : ['id', 'adapter', 'model', 'effort'],
1356
+ ['instruction', 'permissions'],
1357
+ playerPath,
1358
+ );
1359
+ assertPlayerId(player.id, `${playerPath}.id`);
1360
+ validateProjectedAgent(player, playerPath, { structural, hasId: true });
1361
+ playerIds.push(player.id);
1362
+ }
1363
+ if (new Set(playerIds).size !== playerIds.length) {
1364
+ throw new Error(`${path}.players contains a duplicate player id`);
1365
+ }
1366
+
1367
+ const catalog = requireRecord(projection.catalog, `${path}.catalog`);
1368
+ const catalogEntries = Object.entries(catalog);
1369
+ if (catalogEntries.length === 0) {
1370
+ throw new Error(`${path}.catalog must not be empty`);
1371
+ }
1372
+ const commands = new Set();
1373
+ for (const [id, rawItem] of catalogEntries) {
1374
+ const itemPath = `${path}.catalog.${id}`;
1375
+ const item = requireRecord(rawItem, itemPath);
1376
+ rejectUnknownOrMissingKeys(
1377
+ item,
1378
+ [
1379
+ 'id',
1380
+ 'from',
1381
+ 'manifestCommand',
1382
+ 'command',
1383
+ 'intent',
1384
+ 'artifactSchema',
1385
+ 'requiredRoleIds',
1386
+ 'concurrentRoleSets',
1387
+ 'roles',
1388
+ 'options',
1389
+ ],
1390
+ itemPath,
1391
+ );
1392
+ if (requireCanonicalNonblank(item.id, `${itemPath}.id`) !== id) {
1393
+ throw new Error(`${itemPath}.id must equal its catalog key`);
1394
+ }
1395
+ if (id === RESERVED_ID) {
1396
+ throw new Error(`${itemPath}.id uses reserved id "captain"`);
1397
+ }
1398
+ validateCanonicalModuleSpecifier(item.from, `${itemPath}.from`);
1399
+ requireCanonicalNonblank(
1400
+ item.manifestCommand,
1401
+ `${itemPath}.manifestCommand`,
1402
+ );
1403
+ const command = requireCanonicalNonblank(
1404
+ item.command,
1405
+ `${itemPath}.command`,
1406
+ );
1407
+ if (command === RESERVED_ID) {
1408
+ throw new Error(`${itemPath}.command uses reserved command "captain"`);
1409
+ }
1410
+ if (commands.has(command)) {
1411
+ throw new Error(`${path}.catalog contains duplicate command ${JSON.stringify(command)}`);
1412
+ }
1413
+ commands.add(command);
1414
+ if (typeof item.intent !== 'string') {
1415
+ throw new Error(`${itemPath}.intent must be a string`);
1416
+ }
1417
+ if (item.artifactSchema !== 2) {
1418
+ throw new Error(`${itemPath}.artifactSchema must be exactly 2`);
1419
+ }
1420
+ const requiredRoleIds = validateRoleIds(
1421
+ item.requiredRoleIds,
1422
+ `${itemPath}.requiredRoleIds`,
1423
+ );
1424
+ validateConcurrentRoleSets(
1425
+ item.concurrentRoleSets,
1426
+ requiredRoleIds,
1427
+ `${itemPath}.concurrentRoleSets`,
1428
+ );
1429
+ const roles = requireRecord(item.roles, `${itemPath}.roles`);
1430
+ if (!isDeepStrictEqual(Object.keys(roles), requiredRoleIds)) {
1431
+ throw new Error(`${itemPath}.roles must exactly follow requiredRoleIds`);
1432
+ }
1433
+ for (const roleId of requiredRoleIds) {
1434
+ const bindingPath = `${itemPath}.roles.${roleId}`;
1435
+ const binding = requireRecord(roles[roleId], bindingPath);
1436
+ rejectUnknownOrMissingKeys(
1437
+ binding,
1438
+ structural
1439
+ ? ['playerId']
1440
+ : ['playerId', 'model', 'effort'],
1441
+ bindingPath,
1442
+ );
1443
+ assertPlayerId(binding.playerId, `${bindingPath}.playerId`);
1444
+ if (!structural) {
1445
+ validateTuningSelection(binding.model, `${bindingPath}.model`);
1446
+ const player = projection.players.find(
1447
+ (candidate) => candidate.id === binding.playerId,
1448
+ );
1449
+ if (player === undefined) {
1450
+ throw new Error(
1451
+ `${bindingPath}.playerId names a player absent from ${path}.players`,
1452
+ );
1453
+ }
1454
+ validateEffortSelection(
1455
+ binding.effort,
1456
+ player.adapter,
1457
+ `${bindingPath}.effort`,
1458
+ );
1459
+ }
1460
+ }
1461
+ for (const [setIndex, set] of item.concurrentRoleSets.entries()) {
1462
+ const concurrentPlayers = set.map(
1463
+ (roleId) => roles[roleId].playerId,
1464
+ );
1465
+ if (new Set(concurrentPlayers).size !== concurrentPlayers.length) {
1466
+ throw new Error(
1467
+ `${itemPath}.concurrentRoleSets[${setIndex}] binds one player more than once`,
1468
+ );
1469
+ }
1470
+ }
1471
+ requireRecord(item.options, `${itemPath}.options`);
1472
+ }
1473
+ const referenced = referencedPlayerIds(catalog);
1474
+ if (!isDeepStrictEqual(playerIds, referenced)) {
1475
+ throw new Error(
1476
+ `${path}.players must equal the ordered player ids referenced by catalog roles`,
1477
+ );
1478
+ }
1479
+ }
1480
+
1481
+ function validateProjectedAgent(value, path, { structural, hasId = false }) {
1482
+ const agent = requireRecord(value, path);
1483
+ if (!hasId) {
1484
+ exactOptionalKeys(
1485
+ agent,
1486
+ structural ? ['adapter'] : ['adapter', 'model', 'effort'],
1487
+ ['instruction', 'permissions'],
1488
+ path,
1489
+ );
1490
+ }
1491
+ const adapter = requireCanonicalNonblank(agent.adapter, `${path}.adapter`);
1492
+ if (!KNOWN_ADAPTERS.has(adapter)) {
1493
+ throw new Error(`${path}.adapter ${JSON.stringify(adapter)} is not supported`);
1494
+ }
1495
+ if (!structural) {
1496
+ validateTuningSelection(agent.model, `${path}.model`);
1497
+ validateEffortSelection(agent.effort, adapter, `${path}.effort`);
1498
+ }
1499
+ if (agent.instruction !== undefined && typeof agent.instruction !== 'string') {
1500
+ throw new Error(`${path}.instruction must be a string`);
1501
+ }
1502
+ if (agent.permissions !== undefined) {
1503
+ validatePermissionPolicy(agent.permissions, `${path}.permissions`);
1504
+ }
1505
+ }
1506
+
1507
+ function validateTuningSelection(value, path) {
1508
+ const selection = requireRecord(value, path);
1509
+ if (selection.kind === 'provider-default') {
1510
+ rejectUnknownOrMissingKeys(selection, ['kind'], path);
1511
+ return;
1512
+ }
1513
+ if (selection.kind === 'value') {
1514
+ rejectUnknownOrMissingKeys(selection, ['kind', 'value'], path);
1515
+ requireNonblank(selection.value, `${path}.value`);
1516
+ return;
1517
+ }
1518
+ throw new Error(`${path}.kind is not supported`);
1519
+ }
1520
+
1521
+ function validateEffortSelection(value, adapter, path) {
1522
+ validateTuningSelection(value, path);
1523
+ if (value.kind !== 'value') return;
1524
+ try {
1525
+ assertSupportedEffort(adapter, value.value, `${path}.value`);
1526
+ } catch (cause) {
1527
+ throw new Error(errorMessage(cause));
1528
+ }
1529
+ }
1530
+
1531
+ function validatePermissionPolicy(value, path) {
1532
+ const permissions = requireRecord(value, path);
1533
+ exactOptionalKeys(
1534
+ permissions,
1535
+ [],
1536
+ [
1537
+ 'mode',
1538
+ 'fileWrite',
1539
+ 'shellExecute',
1540
+ 'networkAccess',
1541
+ 'writablePaths',
1542
+ ],
1543
+ path,
1544
+ );
1545
+ if (
1546
+ permissions.mode !== undefined &&
1547
+ permissions.mode !== 'auto' &&
1548
+ permissions.mode !== 'bypass'
1549
+ ) {
1550
+ throw new Error(`${path}.mode must be "auto" or "bypass"`);
1551
+ }
1552
+ for (const key of ['fileWrite', 'shellExecute', 'networkAccess']) {
1553
+ if (
1554
+ permissions[key] !== undefined &&
1555
+ !['allow', 'ask', 'deny'].includes(permissions[key])
1556
+ ) {
1557
+ throw new Error(`${path}.${key} must be "allow", "ask", or "deny"`);
1558
+ }
1559
+ }
1560
+ if (permissions.writablePaths !== undefined) {
1561
+ if (
1562
+ !Array.isArray(permissions.writablePaths) ||
1563
+ permissions.writablePaths.some(
1564
+ (entry) => typeof entry !== 'string' || entry.length === 0,
1565
+ )
1566
+ ) {
1567
+ throw new Error(
1568
+ `${path}.writablePaths must be an array of non-empty strings`,
1569
+ );
1570
+ }
1571
+ }
1572
+ }
1573
+
1574
+ function validateRoleIds(value, path) {
1575
+ if (
1576
+ !Array.isArray(value) ||
1577
+ value.some(
1578
+ (roleId) =>
1579
+ typeof roleId !== 'string' ||
1580
+ !ROLE_ID_PATTERN.test(roleId) ||
1581
+ roleId === RESERVED_ID,
1582
+ ) ||
1583
+ new Set(value).size !== value.length
1584
+ ) {
1585
+ throw new Error(`${path} must contain distinct canonical local role ids`);
1586
+ }
1587
+ return value;
1588
+ }
1589
+
1590
+ function validateConcurrentRoleSets(value, requiredRoleIds, path) {
1591
+ if (!Array.isArray(value)) {
1592
+ throw new Error(`${path} must be an array`);
1593
+ }
1594
+ const required = new Set(requiredRoleIds);
1595
+ const signatures = new Set();
1596
+ for (let index = 0; index < value.length; index += 1) {
1597
+ const set = value[index];
1598
+ if (
1599
+ !Array.isArray(set) ||
1600
+ set.length < 2 ||
1601
+ set.some((roleId) => !required.has(roleId)) ||
1602
+ new Set(set).size !== set.length
1603
+ ) {
1604
+ throw new Error(
1605
+ `${path}[${index}] must contain at least two distinct required roles`,
1606
+ );
1607
+ }
1608
+ const signature = JSON.stringify(set);
1609
+ if (signatures.has(signature)) {
1610
+ throw new Error(`${path} contains duplicate set ${signature}`);
1611
+ }
1612
+ signatures.add(signature);
1613
+ }
1614
+ }
1615
+
1616
+ function referencedPlayerIds(catalog) {
1617
+ const seen = new Set();
1618
+ const ids = [];
1619
+ for (const item of Object.values(catalog)) {
1620
+ for (const roleId of item.requiredRoleIds) {
1621
+ const playerId = item.roles[roleId].playerId;
1622
+ if (!seen.has(playerId)) {
1623
+ seen.add(playerId);
1624
+ ids.push(playerId);
1625
+ }
1626
+ }
1627
+ }
1628
+ return ids;
1629
+ }
1630
+
1631
+ function assertSnapshotMatchesStructure(snapshot, structural) {
1632
+ if (!isDeepStrictEqual(snapshot.captain.agent, structural.captain)) {
1633
+ throw new Error(
1634
+ 'Captain session snapshot Captain envelope differs from structuralProjection',
1635
+ );
1636
+ }
1637
+ const structuralPlayers = new Map(
1638
+ structural.players.map(({ id, ...agent }) => [id, agent]),
1639
+ );
1640
+ if (
1641
+ !isDeepStrictEqual(
1642
+ Object.keys(snapshot.playerSessions),
1643
+ [...structuralPlayers.keys()],
1644
+ )
1645
+ ) {
1646
+ throw new Error(
1647
+ 'Captain session snapshot player ledger differs from structuralProjection roster',
1648
+ );
1649
+ }
1650
+ for (const [playerId, agent] of structuralPlayers) {
1651
+ const { resumeToken: _resumeToken, ...savedAgent } =
1652
+ snapshot.playerSessions[playerId];
1653
+ if (!isDeepStrictEqual(savedAgent, agent)) {
1654
+ throw new Error(
1655
+ `Captain session snapshot player ${JSON.stringify(playerId)} envelope differs from structuralProjection`,
1656
+ );
1657
+ }
1658
+ }
1659
+ if (snapshot.mode !== 'engaged.parked') return;
1660
+ for (const frame of snapshot.frames) {
1661
+ const item = structural.catalog[frame.playbookId];
1662
+ if (item === undefined) {
1663
+ throw new Error(
1664
+ `Captain session snapshot frame names unknown stored playbook ${JSON.stringify(frame.playbookId)}`,
1665
+ );
1666
+ }
1667
+ const roleBindings = Object.fromEntries(
1668
+ item.requiredRoleIds.map((roleId) => [
1669
+ roleId,
1670
+ item.roles[roleId].playerId,
1671
+ ]),
1672
+ );
1673
+ if (!isDeepStrictEqual(frame.roleBindings, roleBindings)) {
1674
+ throw new Error(
1675
+ `Captain session snapshot frame ${JSON.stringify(frame.playbookId)} role bindings differ from structuralProjection`,
1676
+ );
1677
+ }
1678
+ }
1679
+ }
1680
+
948
1681
  function validateLeaseOwner(value) {
949
1682
  const owner = requireRecord(
950
1683
  snapshotJsonValue(value, 'Captain session lease owner'),
@@ -994,6 +1727,18 @@ function assertSessionId(value) {
994
1727
  assertUuid(value, 'Captain session id');
995
1728
  }
996
1729
 
1730
+ function assertPlayerId(value, path) {
1731
+ if (
1732
+ typeof value !== 'string' ||
1733
+ !PLAYER_ID_PATTERN.test(value) ||
1734
+ value === RESERVED_ID
1735
+ ) {
1736
+ throw new Error(
1737
+ `${path} must be a non-reserved player id matching ${PLAYER_ID_PATTERN.source}`,
1738
+ );
1739
+ }
1740
+ }
1741
+
997
1742
  function assertUuid(value, path) {
998
1743
  if (typeof value !== 'string' || !SESSION_ID_PATTERN.test(value)) {
999
1744
  throw new Error(`${path} must be a UUID`);
@@ -1020,6 +1765,53 @@ function rejectUnknownOrMissingKeys(value, expected, path) {
1020
1765
  }
1021
1766
  }
1022
1767
 
1768
+ function exactOptionalKeys(value, required, optional, path) {
1769
+ rejectUnknownOrMissingKeys(
1770
+ value,
1771
+ [
1772
+ ...required,
1773
+ ...optional.filter((key) => Object.hasOwn(value, key)),
1774
+ ],
1775
+ path,
1776
+ );
1777
+ }
1778
+
1779
+ function requireNonblank(value, path) {
1780
+ if (typeof value !== 'string' || value.trim().length === 0) {
1781
+ throw new Error(`${path} must be a nonblank string`);
1782
+ }
1783
+ return value;
1784
+ }
1785
+
1786
+ function requireCanonicalNonblank(value, path) {
1787
+ const text = requireNonblank(value, path);
1788
+ if (text !== text.trim()) {
1789
+ throw new Error(`${path} must be in canonical trimmed form`);
1790
+ }
1791
+ return text;
1792
+ }
1793
+
1794
+ function validateCanonicalModuleSpecifier(value, path) {
1795
+ const specifier = requireCanonicalNonblank(value, path);
1796
+ if (
1797
+ isAbsolute(specifier) ||
1798
+ /^(?:\.{1,2}(?:[\\/]|$)|[\\/]|[A-Za-z]:[\\/])/.test(specifier)
1799
+ ) {
1800
+ throw new Error(`${path} must be a canonical module specifier`);
1801
+ }
1802
+ if (!specifier.startsWith('file:')) return specifier;
1803
+ let canonical;
1804
+ try {
1805
+ canonical = pathToFileURL(fileURLToPath(specifier)).href;
1806
+ } catch {
1807
+ throw new Error(`${path} must be a canonical file URL`);
1808
+ }
1809
+ if (canonical !== specifier) {
1810
+ throw new Error(`${path} must be a canonical file URL`);
1811
+ }
1812
+ return specifier;
1813
+ }
1814
+
1023
1815
  function canonicalTimestamp(value, field) {
1024
1816
  if (
1025
1817
  typeof value !== 'string' ||