simfile 0.0.1

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 (249) hide show
  1. package/README.md +143 -0
  2. package/dist/cli/index.d.ts +3 -0
  3. package/dist/cli/index.d.ts.map +1 -0
  4. package/dist/cli/index.js +348 -0
  5. package/dist/coverage/index.d.ts +2 -0
  6. package/dist/coverage/index.d.ts.map +1 -0
  7. package/dist/coverage/index.js +1 -0
  8. package/dist/coverage/matrix.d.ts +21 -0
  9. package/dist/coverage/matrix.d.ts.map +1 -0
  10. package/dist/coverage/matrix.js +173 -0
  11. package/dist/coverage/render.d.ts +3 -0
  12. package/dist/coverage/render.d.ts.map +1 -0
  13. package/dist/coverage/render.js +26 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +7 -0
  17. package/dist/kernel/duration.d.ts +9 -0
  18. package/dist/kernel/duration.d.ts.map +1 -0
  19. package/dist/kernel/duration.js +44 -0
  20. package/dist/kernel/index.d.ts +4 -0
  21. package/dist/kernel/index.d.ts.map +1 -0
  22. package/dist/kernel/index.js +3 -0
  23. package/dist/kernel/range.d.ts +6 -0
  24. package/dist/kernel/range.d.ts.map +1 -0
  25. package/dist/kernel/range.js +16 -0
  26. package/dist/kernel/stochastic.d.ts +10 -0
  27. package/dist/kernel/stochastic.d.ts.map +1 -0
  28. package/dist/kernel/stochastic.js +25 -0
  29. package/dist/ledger/index.d.ts +4 -0
  30. package/dist/ledger/index.d.ts.map +1 -0
  31. package/dist/ledger/index.js +3 -0
  32. package/dist/ledger/markers.d.ts +46 -0
  33. package/dist/ledger/markers.d.ts.map +1 -0
  34. package/dist/ledger/markers.js +108 -0
  35. package/dist/ledger/stable.d.ts +65 -0
  36. package/dist/ledger/stable.d.ts.map +1 -0
  37. package/dist/ledger/stable.js +78 -0
  38. package/dist/ledger/validation.d.ts +16 -0
  39. package/dist/ledger/validation.d.ts.map +1 -0
  40. package/dist/ledger/validation.js +127 -0
  41. package/dist/moltnet/index.d.ts +3 -0
  42. package/dist/moltnet/index.d.ts.map +1 -0
  43. package/dist/moltnet/index.js +2 -0
  44. package/dist/moltnet/transcript-export.d.ts +34 -0
  45. package/dist/moltnet/transcript-export.d.ts.map +1 -0
  46. package/dist/moltnet/transcript-export.js +68 -0
  47. package/dist/moltnet/world-participant.d.ts +77 -0
  48. package/dist/moltnet/world-participant.d.ts.map +1 -0
  49. package/dist/moltnet/world-participant.js +221 -0
  50. package/dist/observe/artifacts.d.ts +16 -0
  51. package/dist/observe/artifacts.d.ts.map +1 -0
  52. package/dist/observe/artifacts.js +31 -0
  53. package/dist/observe/causalStreams.d.ts +19 -0
  54. package/dist/observe/causalStreams.d.ts.map +1 -0
  55. package/dist/observe/causalStreams.js +38 -0
  56. package/dist/observe/compute.d.ts +17 -0
  57. package/dist/observe/compute.d.ts.map +1 -0
  58. package/dist/observe/compute.js +97 -0
  59. package/dist/observe/index.d.ts +11 -0
  60. package/dist/observe/index.d.ts.map +1 -0
  61. package/dist/observe/index.js +10 -0
  62. package/dist/observe/manifest.d.ts +79 -0
  63. package/dist/observe/manifest.d.ts.map +1 -0
  64. package/dist/observe/manifest.js +87 -0
  65. package/dist/observe/memoryBanks.d.ts +38 -0
  66. package/dist/observe/memoryBanks.d.ts.map +1 -0
  67. package/dist/observe/memoryBanks.js +86 -0
  68. package/dist/observe/observe.d.ts +32 -0
  69. package/dist/observe/observe.d.ts.map +1 -0
  70. package/dist/observe/observe.js +73 -0
  71. package/dist/observe/report.d.ts +165 -0
  72. package/dist/observe/report.d.ts.map +1 -0
  73. package/dist/observe/report.js +138 -0
  74. package/dist/observe/seedSpread.d.ts +49 -0
  75. package/dist/observe/seedSpread.d.ts.map +1 -0
  76. package/dist/observe/seedSpread.js +262 -0
  77. package/dist/observe/seedSpreadArtifacts.d.ts +49 -0
  78. package/dist/observe/seedSpreadArtifacts.d.ts.map +1 -0
  79. package/dist/observe/seedSpreadArtifacts.js +118 -0
  80. package/dist/observe/spreadMatcher.d.ts +45 -0
  81. package/dist/observe/spreadMatcher.d.ts.map +1 -0
  82. package/dist/observe/spreadMatcher.js +124 -0
  83. package/dist/report/index.d.ts +4 -0
  84. package/dist/report/index.d.ts.map +1 -0
  85. package/dist/report/index.js +3 -0
  86. package/dist/report/probeArtifact.d.ts +26 -0
  87. package/dist/report/probeArtifact.d.ts.map +1 -0
  88. package/dist/report/probeArtifact.js +119 -0
  89. package/dist/report/probes.d.ts +52 -0
  90. package/dist/report/probes.d.ts.map +1 -0
  91. package/dist/report/probes.js +164 -0
  92. package/dist/report/score.d.ts +27 -0
  93. package/dist/report/score.d.ts.map +1 -0
  94. package/dist/report/score.js +14 -0
  95. package/dist/report/transcripts.d.ts +12 -0
  96. package/dist/report/transcripts.d.ts.map +1 -0
  97. package/dist/report/transcripts.js +17 -0
  98. package/dist/runtime/causal-fixture.d.ts +26 -0
  99. package/dist/runtime/causal-fixture.d.ts.map +1 -0
  100. package/dist/runtime/causal-fixture.js +36 -0
  101. package/dist/runtime/clock.d.ts +25 -0
  102. package/dist/runtime/clock.d.ts.map +1 -0
  103. package/dist/runtime/clock.js +63 -0
  104. package/dist/runtime/condition.d.ts +55 -0
  105. package/dist/runtime/condition.d.ts.map +1 -0
  106. package/dist/runtime/condition.js +122 -0
  107. package/dist/runtime/emit-causal-fixture.d.ts +2 -0
  108. package/dist/runtime/emit-causal-fixture.d.ts.map +1 -0
  109. package/dist/runtime/emit-causal-fixture.js +45 -0
  110. package/dist/runtime/expression.d.ts +11 -0
  111. package/dist/runtime/expression.d.ts.map +1 -0
  112. package/dist/runtime/expression.js +305 -0
  113. package/dist/runtime/numeric.d.ts +7 -0
  114. package/dist/runtime/numeric.d.ts.map +1 -0
  115. package/dist/runtime/numeric.js +11 -0
  116. package/dist/runtime/rule-actions.d.ts +5 -0
  117. package/dist/runtime/rule-actions.d.ts.map +1 -0
  118. package/dist/runtime/rule-actions.js +82 -0
  119. package/dist/runtime/run-record.d.ts +73 -0
  120. package/dist/runtime/run-record.d.ts.map +1 -0
  121. package/dist/runtime/run-record.js +179 -0
  122. package/dist/runtime/step-tick.d.ts +43 -0
  123. package/dist/runtime/step-tick.d.ts.map +1 -0
  124. package/dist/runtime/step-tick.js +177 -0
  125. package/dist/runtime/trace-compile.d.ts +66 -0
  126. package/dist/runtime/trace-compile.d.ts.map +1 -0
  127. package/dist/runtime/trace-compile.js +157 -0
  128. package/dist/runtime/trace-export.d.ts +5 -0
  129. package/dist/runtime/trace-export.d.ts.map +1 -0
  130. package/dist/runtime/trace-export.js +4 -0
  131. package/dist/runtime/trace-run.d.ts +15 -0
  132. package/dist/runtime/trace-run.d.ts.map +1 -0
  133. package/dist/runtime/trace-run.js +92 -0
  134. package/dist/runtime/trace.d.ts +10 -0
  135. package/dist/runtime/trace.d.ts.map +1 -0
  136. package/dist/runtime/trace.js +9 -0
  137. package/dist/runtime/types.d.ts +61 -0
  138. package/dist/runtime/types.d.ts.map +1 -0
  139. package/dist/runtime/types.js +1 -0
  140. package/dist/runtime/viewer-trace.d.ts +64 -0
  141. package/dist/runtime/viewer-trace.d.ts.map +1 -0
  142. package/dist/runtime/viewer-trace.js +164 -0
  143. package/dist/runtime/world-act.d.ts +55 -0
  144. package/dist/runtime/world-act.d.ts.map +1 -0
  145. package/dist/runtime/world-act.js +156 -0
  146. package/dist/schema/binding.d.ts +23 -0
  147. package/dist/schema/binding.d.ts.map +1 -0
  148. package/dist/schema/binding.js +249 -0
  149. package/dist/schema/index.d.ts +5 -0
  150. package/dist/schema/index.d.ts.map +1 -0
  151. package/dist/schema/index.js +4 -0
  152. package/dist/schema/model.d.ts +244 -0
  153. package/dist/schema/model.d.ts.map +1 -0
  154. package/dist/schema/model.js +222 -0
  155. package/dist/schema/parse.d.ts +10 -0
  156. package/dist/schema/parse.d.ts.map +1 -0
  157. package/dist/schema/parse.js +15 -0
  158. package/dist/schema/semantic.d.ts +3 -0
  159. package/dist/schema/semantic.d.ts.map +1 -0
  160. package/dist/schema/semantic.js +135 -0
  161. package/dist/sims/composeRunManifest.d.ts +54 -0
  162. package/dist/sims/composeRunManifest.d.ts.map +1 -0
  163. package/dist/sims/composeRunManifest.js +40 -0
  164. package/dist/sims/composedJungianSimDriver.d.ts +50 -0
  165. package/dist/sims/composedJungianSimDriver.d.ts.map +1 -0
  166. package/dist/sims/composedJungianSimDriver.js +199 -0
  167. package/dist/sims/composedOfficeSimDriver.d.ts +53 -0
  168. package/dist/sims/composedOfficeSimDriver.d.ts.map +1 -0
  169. package/dist/sims/composedOfficeSimDriver.js +162 -0
  170. package/dist/sims/exchangeWait.d.ts +56 -0
  171. package/dist/sims/exchangeWait.d.ts.map +1 -0
  172. package/dist/sims/exchangeWait.js +66 -0
  173. package/dist/sims/index.d.ts +14 -0
  174. package/dist/sims/index.d.ts.map +1 -0
  175. package/dist/sims/index.js +13 -0
  176. package/dist/sims/moltnetRoomClient.d.ts +47 -0
  177. package/dist/sims/moltnetRoomClient.d.ts.map +1 -0
  178. package/dist/sims/moltnetRoomClient.js +41 -0
  179. package/dist/sims/poll.d.ts +19 -0
  180. package/dist/sims/poll.d.ts.map +1 -0
  181. package/dist/sims/poll.js +23 -0
  182. package/dist/sims/spawnfileCli.d.ts +49 -0
  183. package/dist/sims/spawnfileCli.d.ts.map +1 -0
  184. package/dist/sims/spawnfileCli.js +90 -0
  185. package/dist/sims/spawnfileReceipts.d.ts +76 -0
  186. package/dist/sims/spawnfileReceipts.d.ts.map +1 -0
  187. package/dist/sims/spawnfileReceipts.js +107 -0
  188. package/dist/sims/worldDrivenOfficeSimDriver.d.ts +59 -0
  189. package/dist/sims/worldDrivenOfficeSimDriver.d.ts.map +1 -0
  190. package/dist/sims/worldDrivenOfficeSimDriver.js +182 -0
  191. package/dist/sims/worldLedgerWriter.d.ts +32 -0
  192. package/dist/sims/worldLedgerWriter.d.ts.map +1 -0
  193. package/dist/sims/worldLedgerWriter.js +39 -0
  194. package/dist/sims/worldSeedLint.d.ts +42 -0
  195. package/dist/sims/worldSeedLint.d.ts.map +1 -0
  196. package/dist/sims/worldSeedLint.js +73 -0
  197. package/dist/sims/worldTickIngest.d.ts +27 -0
  198. package/dist/sims/worldTickIngest.d.ts.map +1 -0
  199. package/dist/sims/worldTickIngest.js +35 -0
  200. package/dist/sims/worldTickLoop.d.ts +64 -0
  201. package/dist/sims/worldTickLoop.d.ts.map +1 -0
  202. package/dist/sims/worldTickLoop.js +122 -0
  203. package/dist/view/engineProvenance.d.ts +46 -0
  204. package/dist/view/engineProvenance.d.ts.map +1 -0
  205. package/dist/view/engineProvenance.js +76 -0
  206. package/dist/view/index.d.ts +14 -0
  207. package/dist/view/index.d.ts.map +1 -0
  208. package/dist/view/index.js +107 -0
  209. package/dist/view/membranes.d.ts +14 -0
  210. package/dist/view/membranes.d.ts.map +1 -0
  211. package/dist/view/membranes.js +127 -0
  212. package/dist/view/runDetect.d.ts +2 -0
  213. package/dist/view/runDetect.d.ts.map +1 -0
  214. package/dist/view/runDetect.js +47 -0
  215. package/dist/view/runRawArtifacts.d.ts +48 -0
  216. package/dist/view/runRawArtifacts.d.ts.map +1 -0
  217. package/dist/view/runRawArtifacts.js +143 -0
  218. package/dist/view/runTimeline.d.ts +24 -0
  219. package/dist/view/runTimeline.d.ts.map +1 -0
  220. package/dist/view/runTimeline.js +204 -0
  221. package/dist/view/runTimelineRecords.d.ts +42 -0
  222. package/dist/view/runTimelineRecords.d.ts.map +1 -0
  223. package/dist/view/runTimelineRecords.js +244 -0
  224. package/dist/view/runTimelineRefs.d.ts +27 -0
  225. package/dist/view/runTimelineRefs.d.ts.map +1 -0
  226. package/dist/view/runTimelineRefs.js +31 -0
  227. package/dist/view/runTimelineTypes.d.ts +99 -0
  228. package/dist/view/runTimelineTypes.d.ts.map +1 -0
  229. package/dist/view/runTimelineTypes.js +1 -0
  230. package/dist/view/runViewModel.d.ts +21 -0
  231. package/dist/view/runViewModel.d.ts.map +1 -0
  232. package/dist/view/runViewModel.js +66 -0
  233. package/dist/view/runViewModelCompute.d.ts +24 -0
  234. package/dist/view/runViewModelCompute.d.ts.map +1 -0
  235. package/dist/view/runViewModelCompute.js +157 -0
  236. package/dist/view/runViewModelTypes.d.ts +177 -0
  237. package/dist/view/runViewModelTypes.d.ts.map +1 -0
  238. package/dist/view/runViewModelTypes.js +1 -0
  239. package/dist/view/runWorldTrace.d.ts +97 -0
  240. package/dist/view/runWorldTrace.d.ts.map +1 -0
  241. package/dist/view/runWorldTrace.js +88 -0
  242. package/dist/view/server.d.ts +12 -0
  243. package/dist/view/server.d.ts.map +1 -0
  244. package/dist/view/server.js +306 -0
  245. package/package.json +65 -0
  246. package/web/dist/assets/index-CJHLjfsG.js +49 -0
  247. package/web/dist/assets/index-DJOsPsU8.css +1 -0
  248. package/web/dist/index.html +13 -0
  249. package/web/dist/models/AnimatedMushnub.glb +0 -0
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Engine provenance disclosure (honesty gap fix): a run-replay viewer must
3
+ * never let a deterministic scripted screenplay be mistaken for live
4
+ * agents. This module classifies each engine name a run declares — either
5
+ * per-agent (from `spawnfile/up-receipt.json`'s `engines[]`, when the
6
+ * run-dir carries one) or the single collapsed `manifest.engine` string —
7
+ * into one of three per-entry buckets (`scripted` / `real-engine` /
8
+ * `unknown`), then folds those into one overall `EngineProvenance.mode` the
9
+ * viewer badges unmissably.
10
+ *
11
+ * Classification rule (never guess):
12
+ * - `fake-*` or exactly `scripted` -> `scripted` (a canned, authored
13
+ * screenplay — deterministic, not emergent dialogue).
14
+ * - A recognized real model engine name (`grok`, `codex`, `agy`, `claude`,
15
+ * matched as a whole word/hyphenated-prefix so `claude-sonnet-4.5` or
16
+ * `grok-4` still match) -> `real-engine`.
17
+ * - Anything else, including an entirely absent engine field -> `unknown`.
18
+ * `unknown` NEVER collapses to `real-engine` — absence of disclosure is
19
+ * never treated as evidence of a real engine.
20
+ *
21
+ * Overall `mode` is the single per-entry classification when every entry
22
+ * agrees, or `"mixed"` when entries disagree (e.g. one agent scripted,
23
+ * another real — or a real name alongside an undetermined one). `"mixed"`
24
+ * is itself a disclosure, not a suppressed detail.
25
+ */
26
+ export type EngineClassification = "scripted" | "real-engine" | "unknown";
27
+ export interface EngineEntry {
28
+ /** Present when derived from a per-agent up-receipt `engines[]` row; absent for a single collapsed `manifest.engine` string. */
29
+ agent?: string;
30
+ engine: string;
31
+ }
32
+ export interface EngineProvenance {
33
+ mode: EngineClassification | "mixed";
34
+ engines: EngineEntry[];
35
+ label: string;
36
+ }
37
+ /** Classifies one engine name string. Exported for direct testing of the rule in isolation from the aggregation below. */
38
+ export declare const classifyEngineName: (engine: string) => EngineClassification;
39
+ /**
40
+ * Folds a list of engine entries (per-agent or a single collapsed entry)
41
+ * into the one `EngineProvenance` the viewer badges. An empty list (no
42
+ * engine field disclosed anywhere) is `"unknown"` — never defaulted to
43
+ * real or scripted.
44
+ */
45
+ export declare const computeEngineProvenance: (engines: readonly EngineEntry[]) => EngineProvenance;
46
+ //# sourceMappingURL=engineProvenance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engineProvenance.d.ts","sourceRoot":"","sources":["../../src/view/engineProvenance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,CAAC;AAE1E,MAAM,WAAW,WAAW;IAC1B,gIAAgI;IAChI,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC;IACrC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;AAaD,0HAA0H;AAC1H,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,KAAG,oBAKnD,CAAC;AAmBF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,SAAS,WAAW,EAAE,KAAG,gBAezE,CAAC"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Engine provenance disclosure (honesty gap fix): a run-replay viewer must
3
+ * never let a deterministic scripted screenplay be mistaken for live
4
+ * agents. This module classifies each engine name a run declares — either
5
+ * per-agent (from `spawnfile/up-receipt.json`'s `engines[]`, when the
6
+ * run-dir carries one) or the single collapsed `manifest.engine` string —
7
+ * into one of three per-entry buckets (`scripted` / `real-engine` /
8
+ * `unknown`), then folds those into one overall `EngineProvenance.mode` the
9
+ * viewer badges unmissably.
10
+ *
11
+ * Classification rule (never guess):
12
+ * - `fake-*` or exactly `scripted` -> `scripted` (a canned, authored
13
+ * screenplay — deterministic, not emergent dialogue).
14
+ * - A recognized real model engine name (`grok`, `codex`, `agy`, `claude`,
15
+ * matched as a whole word/hyphenated-prefix so `claude-sonnet-4.5` or
16
+ * `grok-4` still match) -> `real-engine`.
17
+ * - Anything else, including an entirely absent engine field -> `unknown`.
18
+ * `unknown` NEVER collapses to `real-engine` — absence of disclosure is
19
+ * never treated as evidence of a real engine.
20
+ *
21
+ * Overall `mode` is the single per-entry classification when every entry
22
+ * agrees, or `"mixed"` when entries disagree (e.g. one agent scripted,
23
+ * another real — or a real name alongside an undetermined one). `"mixed"`
24
+ * is itself a disclosure, not a suppressed detail.
25
+ */
26
+ const SCRIPTED_PATTERN = /^fake-/iu;
27
+ const REAL_ENGINE_NAMES = ["grok", "codex", "agy", "claude"];
28
+ /** Matches a real engine name as a whole segment (`grok`, `grok-4`, `claude-sonnet-4.5`) so a substring of an unrelated word never false-matches. */
29
+ const realEngineNameFrom = (engine) => {
30
+ const lowered = engine.toLowerCase();
31
+ return REAL_ENGINE_NAMES.find((name) => lowered === name || lowered.startsWith(`${name}-`) || lowered.startsWith(`${name}_`));
32
+ };
33
+ /** Classifies one engine name string. Exported for direct testing of the rule in isolation from the aggregation below. */
34
+ export const classifyEngineName = (engine) => {
35
+ const lowered = engine.trim().toLowerCase();
36
+ if (lowered === "scripted" || SCRIPTED_PATTERN.test(lowered))
37
+ return "scripted";
38
+ if (realEngineNameFrom(lowered))
39
+ return "real-engine";
40
+ return "unknown";
41
+ };
42
+ const SCRIPTED_LABEL = "⚙ SCRIPTED RUN — authored screenplay, not emergent dialogue (deterministic, not live agents)";
43
+ const UNKNOWN_LABEL = "❓ ENGINE UNKNOWN — provenance not disclosed, do not assume real agents";
44
+ const realEngineLabel = (engines) => {
45
+ const names = [...new Set(engines.map((entry) => entry.engine))];
46
+ return `🧠 REAL ENGINE · ${names.join(", ")}`;
47
+ };
48
+ const mixedLabel = (engines) => {
49
+ const parts = engines.map((entry) => {
50
+ const classification = classifyEngineName(entry.engine);
51
+ const tag = classification === "scripted" ? "scripted" : classification === "real-engine" ? `real: ${entry.engine}` : "unknown";
52
+ return entry.agent ? `${entry.agent}: ${tag}` : tag;
53
+ });
54
+ return `◐ MIXED ENGINES — ${parts.join(", ")} (not uniformly real or scripted)`;
55
+ };
56
+ /**
57
+ * Folds a list of engine entries (per-agent or a single collapsed entry)
58
+ * into the one `EngineProvenance` the viewer badges. An empty list (no
59
+ * engine field disclosed anywhere) is `"unknown"` — never defaulted to
60
+ * real or scripted.
61
+ */
62
+ export const computeEngineProvenance = (engines) => {
63
+ if (engines.length === 0) {
64
+ return { mode: "unknown", engines: [], label: UNKNOWN_LABEL };
65
+ }
66
+ const classifications = new Set(engines.map((entry) => classifyEngineName(entry.engine)));
67
+ if (classifications.size === 1) {
68
+ const [mode] = classifications;
69
+ if (mode === "scripted")
70
+ return { mode, engines: [...engines], label: SCRIPTED_LABEL };
71
+ if (mode === "real-engine")
72
+ return { mode, engines: [...engines], label: realEngineLabel(engines) };
73
+ return { mode: "unknown", engines: [...engines], label: UNKNOWN_LABEL };
74
+ }
75
+ return { mode: "mixed", engines: [...engines], label: mixedLabel(engines) };
76
+ };
@@ -0,0 +1,14 @@
1
+ export interface ViewCommandOptions {
2
+ port: number;
3
+ sourcePath: string;
4
+ statePath?: string;
5
+ openBrowser: boolean;
6
+ }
7
+ export interface ParsedViewCommand {
8
+ options?: ViewCommandOptions;
9
+ error?: string;
10
+ help?: boolean;
11
+ }
12
+ export declare const parseViewArguments: (argv: readonly string[]) => ParsedViewCommand;
13
+ export declare const runViewCommand: (argv: readonly string[]) => Promise<number>;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/view/index.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,eAAO,MAAM,kBAAkB,GAAI,MAAM,SAAS,MAAM,EAAE,KAAG,iBAyE5D,CAAA;AAED,eAAO,MAAM,cAAc,GAAU,MAAM,SAAS,MAAM,EAAE,KAAG,OAAO,CAAC,MAAM,CAiC5E,CAAC"}
@@ -0,0 +1,107 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createViewerServer } from "./server.js";
3
+ export const parseViewArguments = (argv) => {
4
+ const parsed = {
5
+ port: 4400,
6
+ sourcePath: ".",
7
+ openBrowser: true,
8
+ };
9
+ for (let index = 0; index < argv.length; index += 1) {
10
+ const arg = argv[index];
11
+ if (arg === "--help" || arg === "-h") {
12
+ return { help: true };
13
+ }
14
+ if (arg === "--port") {
15
+ const value = argv[index + 1];
16
+ if (!value) {
17
+ return { error: "Missing value for --port" };
18
+ }
19
+ const port = Number(value);
20
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
21
+ return { error: "Invalid value for --port" };
22
+ }
23
+ parsed.port = port;
24
+ index += 1;
25
+ continue;
26
+ }
27
+ if (arg.startsWith("--port=")) {
28
+ const port = Number(arg.slice("--port=".length));
29
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
30
+ return { error: "Invalid value for --port" };
31
+ }
32
+ parsed.port = port;
33
+ continue;
34
+ }
35
+ if (arg === "--no-open") {
36
+ parsed.openBrowser = false;
37
+ continue;
38
+ }
39
+ if (arg === "--state") {
40
+ const value = argv[index + 1];
41
+ if (!value) {
42
+ return { error: "Missing value for --state" };
43
+ }
44
+ parsed.statePath = value;
45
+ index += 1;
46
+ continue;
47
+ }
48
+ if (arg.startsWith("--state=")) {
49
+ parsed.statePath = arg.slice("--state=".length);
50
+ continue;
51
+ }
52
+ if (arg.startsWith("-")) {
53
+ return { error: `Unknown flag ${arg}` };
54
+ }
55
+ if (parsed.sourcePath !== ".") {
56
+ return { error: `Unexpected positional argument ${arg}` };
57
+ }
58
+ parsed.sourcePath = arg;
59
+ }
60
+ return {
61
+ options: {
62
+ ...parsed,
63
+ mode: parsed.statePath ? "live" : "replay",
64
+ },
65
+ };
66
+ };
67
+ export const runViewCommand = async (argv) => {
68
+ const parsed = parseViewArguments(argv);
69
+ if (parsed.help) {
70
+ process.stdout.write(viewUsage());
71
+ return 0;
72
+ }
73
+ if (parsed.error || !parsed.options) {
74
+ if (parsed.error)
75
+ process.stderr.write(`${parsed.error}\n`);
76
+ process.stderr.write(viewUsage());
77
+ return 1;
78
+ }
79
+ const handle = await createViewerServer({
80
+ port: parsed.options.port,
81
+ sourcePath: parsed.options.sourcePath,
82
+ statePath: parsed.options.statePath,
83
+ mode: parsed.options.statePath ? "live" : "replay",
84
+ });
85
+ const modeLabel = parsed.options.statePath ? "live" : "replay";
86
+ process.stdout.write(`Simfile viewer running at ${handle.url} (mode: ${modeLabel})\n`);
87
+ process.stdout.write(`Press Ctrl+C to stop.\n`);
88
+ if (parsed.options.openBrowser) {
89
+ openBrowser(handle.url);
90
+ }
91
+ await new Promise(() => { });
92
+ return 0;
93
+ };
94
+ const openBrowser = (url) => {
95
+ const command = process.platform === "darwin" ? "open"
96
+ : process.platform === "win32" ? "start \"\""
97
+ : "xdg-open";
98
+ spawn(command, [url], { detached: true, stdio: "ignore", shell: true });
99
+ };
100
+ const viewUsage = () => [
101
+ "Usage:",
102
+ " simfile view --state <path>",
103
+ " simfile view <run-record-dir>",
104
+ " simfile view --port 4400 --no-open --state .sim/",
105
+ " simfile view --help",
106
+ "",
107
+ ].join("\n");
@@ -0,0 +1,14 @@
1
+ import type { RunTimelineMembrane } from "./runTimelineTypes.js";
2
+ /**
3
+ * Builds the membranes array. Empty (never throwing) when the report is
4
+ * absent/unparseable or declares no interior self-teams — the office-sim
5
+ * golden (no teams, no report in its run-dir) legitimately yields `[]`.
6
+ */
7
+ export declare const deriveMembranes: (report: unknown) => RunTimelineMembrane[];
8
+ /**
9
+ * Reads `spawnfile-report.json` from a run-dir and derives its membranes.
10
+ * Returns `[]` when the file is missing (a non-composed or single-team run
11
+ * that never copied a report in) — never throws for a run without one.
12
+ */
13
+ export declare const readRunMembranes: (runDir: string) => Promise<RunTimelineMembrane[]>;
14
+ //# sourceMappingURL=membranes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"membranes.d.ts","sourceRoot":"","sources":["../../src/view/membranes.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAwHjE;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,QAAQ,OAAO,KAAG,mBAAmB,EAwCpE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,mBAAmB,EAAE,CAQpF,CAAC"}
@@ -0,0 +1,127 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { agentRef, roomRef } from "./runTimelineRefs.js";
4
+ const asRecord = (value) => typeof value === "object" && value !== null ? value : undefined;
5
+ const asString = (value) => (typeof value === "string" ? value : undefined);
6
+ const stripPrefix = (id, prefix) => (id.startsWith(prefix) ? id.slice(prefix.length) : id);
7
+ /** Team-slug set: every `nodes[]` entry with `kind: "team"`, stripped of the `team:` id prefix. */
8
+ const collectTeamSlugs = (report) => {
9
+ const teams = new Set();
10
+ const nodes = Array.isArray(report.nodes) ? report.nodes : [];
11
+ for (const node of nodes) {
12
+ if (asString(node.kind) === "team") {
13
+ const id = asString(node.id);
14
+ if (id)
15
+ teams.add(stripPrefix(id, "team:"));
16
+ }
17
+ }
18
+ return teams;
19
+ };
20
+ /**
21
+ * team-slug -> representative `agent:<id>` ref, read from every agent node's
22
+ * `active_environments.moltnet[network][room].member_slot`: when that slot is
23
+ * a known team slug OTHER than the agent's own id, this agent is that team's
24
+ * representative on a parent floor.
25
+ */
26
+ const collectRepresentatives = (report, teamSlugs) => {
27
+ const byTeam = new Map();
28
+ const nodes = Array.isArray(report.nodes) ? report.nodes : [];
29
+ for (const node of nodes) {
30
+ if (asString(node.kind) !== "agent")
31
+ continue;
32
+ const nodeId = asString(node.id);
33
+ if (!nodeId)
34
+ continue;
35
+ const agentSlug = stripPrefix(nodeId, "agent:");
36
+ const active = asRecord(node.active_environments);
37
+ const moltnet = asRecord(active?.moltnet);
38
+ if (!moltnet)
39
+ continue;
40
+ for (const networkBinding of Object.values(moltnet)) {
41
+ const rooms = asRecord(asRecord(networkBinding)?.rooms);
42
+ if (!rooms)
43
+ continue;
44
+ for (const roomBinding of Object.values(rooms)) {
45
+ const slot = asString(asRecord(roomBinding)?.member_slot);
46
+ if (slot && slot !== agentSlug && teamSlugs.has(slot) && !byTeam.has(slot)) {
47
+ byTeam.set(slot, agentRef(agentSlug));
48
+ }
49
+ }
50
+ }
51
+ }
52
+ return byTeam;
53
+ };
54
+ /** The `server_plans[]` a team owns, matched by the compiler's `id === `${teamSlug}-${networkId}`` convention. */
55
+ const ownedServerPlans = (report, teamSlug) => {
56
+ const moltnet = asRecord(asRecord(report.container)?.moltnet);
57
+ const plans = Array.isArray(moltnet?.server_plans)
58
+ ? moltnet.server_plans
59
+ : [];
60
+ return plans.filter((plan) => {
61
+ const id = asString(plan.id);
62
+ const networkId = asString(plan.network_id);
63
+ return Boolean(id && networkId && id === `${teamSlug}-${networkId}`);
64
+ });
65
+ };
66
+ /**
67
+ * Builds the membranes array. Empty (never throwing) when the report is
68
+ * absent/unparseable or declares no interior self-teams — the office-sim
69
+ * golden (no teams, no report in its run-dir) legitimately yields `[]`.
70
+ */
71
+ export const deriveMembranes = (report) => {
72
+ const shape = asRecord(report);
73
+ if (!shape)
74
+ return [];
75
+ const teamSlugs = collectTeamSlugs(shape);
76
+ const representatives = collectRepresentatives(shape, teamSlugs);
77
+ const membranes = [];
78
+ for (const [teamSlug, representative] of representatives) {
79
+ const plans = ownedServerPlans(shape, teamSlug);
80
+ if (plans.length === 0)
81
+ continue;
82
+ const interiorRooms = [];
83
+ const members = new Set();
84
+ for (const plan of plans) {
85
+ const networkId = asString(plan.network_id);
86
+ const rooms = Array.isArray(plan.rooms) ? plan.rooms : [];
87
+ for (const room of rooms) {
88
+ const roomId = asString(room.id);
89
+ if (!networkId || !roomId)
90
+ continue;
91
+ interiorRooms.push(roomRef(networkId, roomId));
92
+ const roomMembers = Array.isArray(room.members) ? room.members : [];
93
+ for (const member of roomMembers) {
94
+ const memberId = asString(member);
95
+ if (memberId)
96
+ members.add(agentRef(memberId));
97
+ }
98
+ }
99
+ }
100
+ if (interiorRooms.length === 0)
101
+ continue;
102
+ membranes.push({
103
+ ref: `team:${teamSlug}`,
104
+ label: teamSlug,
105
+ representative,
106
+ interiorRooms: interiorRooms.sort(),
107
+ members: [...members].sort()
108
+ });
109
+ }
110
+ return membranes.sort((left, right) => left.ref.localeCompare(right.ref));
111
+ };
112
+ /**
113
+ * Reads `spawnfile-report.json` from a run-dir and derives its membranes.
114
+ * Returns `[]` when the file is missing (a non-composed or single-team run
115
+ * that never copied a report in) — never throws for a run without one.
116
+ */
117
+ export const readRunMembranes = async (runDir) => {
118
+ const raw = await readFile(path.join(runDir, "spawnfile-report.json"), "utf8").catch(() => null);
119
+ if (raw === null)
120
+ return [];
121
+ try {
122
+ return deriveMembranes(JSON.parse(raw));
123
+ }
124
+ catch {
125
+ return [];
126
+ }
127
+ };
@@ -0,0 +1,2 @@
1
+ export declare const isObserveRunDir: (dirPath: string) => Promise<boolean>;
2
+ //# sourceMappingURL=runDetect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runDetect.d.ts","sourceRoot":"","sources":["../../src/view/runDetect.ts"],"names":[],"mappings":"AAmCA,eAAO,MAAM,eAAe,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,OAAO,CAStE,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { access, readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ /**
4
+ * Detects a compose-and-observe run directory (`simfile.run-manifest.v1` + at
5
+ * least one moltnet transcript) so `simfile view <dir>` can pick the
6
+ * run-reader replay mode instead of the world/3D replay mode. Deliberately
7
+ * narrow: it only checks the two artifacts that make the golden fixtures
8
+ * (`fixtures/observe/office-sim-golden/`, `fixtures/observe/jungian-daimon-org-golden/`)
9
+ * and any future real run directory renderable, never a full manifest parse —
10
+ * `runObserve` does the real validation once this returns true. A transcript
11
+ * may be the single-network flat `raw/moltnet/transcript.json` OR any
12
+ * multi-network per-network `raw/moltnet/<network_id>/transcript.json`.
13
+ */
14
+ const hasMoltnetTranscript = async (dirPath) => {
15
+ const moltnetDir = path.join(dirPath, "raw", "moltnet");
16
+ try {
17
+ await access(path.join(moltnetDir, "transcript.json"));
18
+ return true;
19
+ }
20
+ catch {
21
+ // fall through to the per-network scan
22
+ }
23
+ const entries = await readdir(moltnetDir, { withFileTypes: true }).catch(() => []);
24
+ for (const entry of entries) {
25
+ if (!entry.isDirectory())
26
+ continue;
27
+ try {
28
+ await access(path.join(moltnetDir, entry.name, "transcript.json"));
29
+ return true;
30
+ }
31
+ catch {
32
+ // keep scanning
33
+ }
34
+ }
35
+ return false;
36
+ };
37
+ export const isObserveRunDir = async (dirPath) => {
38
+ if (!(await hasMoltnetTranscript(dirPath)))
39
+ return false;
40
+ try {
41
+ const raw = JSON.parse(await readFile(path.join(dirPath, "manifest.json"), "utf8"));
42
+ return raw.version === "simfile.run-manifest.v1";
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ };
@@ -0,0 +1,48 @@
1
+ import type { EngineEntry } from "./engineProvenance.js";
2
+ import type { RawMnemeEvent, RawTranscript, RunTelemetrySample } from "./runViewModelTypes.js";
3
+ export declare const readMnemeEventsByBank: (runDir: string) => Promise<Map<string, RawMnemeEvent[]>>;
4
+ /**
5
+ * Normalizes both `raw/moltnet/transcript.json` shapes into one internal
6
+ * message list: the golden fixture's `{seedMessageText, transcript: [...]}`
7
+ * and a real composed run's export shape `{conversations: [{messages:
8
+ * [...]}] }` (`moltnet/transcript-export.ts`'s `MoltnetExportedTranscript`).
9
+ * Every conversation's messages are flattened and re-sorted by
10
+ * `created_at` so multi-conversation exports still read chronologically;
11
+ * single-conversation runs (the only shape produced today) are a no-op
12
+ * re-sort of an already-ordered list.
13
+ */
14
+ export declare const normalizeRawTranscript: (raw: unknown) => RawTranscript;
15
+ /**
16
+ * Reads every moltnet transcript in a run-dir and merges them into one
17
+ * chronologically-sorted message list. A single-network run writes the flat
18
+ * `raw/moltnet/transcript.json` (the golden/office-sim shape); a multi-network
19
+ * composed run (the jungian psyche) writes one per network under
20
+ * `raw/moltnet/<network_id>/transcript.json`. Both are read and unioned so the
21
+ * timeline's `messagesById` spans every room across every network — the join
22
+ * an interior-council membrane needs. Message ids are globally-unique UUIDs, so
23
+ * a flat re-sort of the union is a correct chronological interleave.
24
+ */
25
+ export declare const readTranscript: (runDir: string) => Promise<RawTranscript>;
26
+ /**
27
+ * Reads `world/telemetry.json` when present (world-driven runs only —
28
+ * `office-secret-v0-golden` ships one, `office-sim-golden` does not).
29
+ * Returns `null` on any missing/malformed file rather than throwing: this
30
+ * artifact is optional, never a requirement for a run to render
31
+ * (`runDetect.ts`'s shape check does not require it).
32
+ */
33
+ export declare const readWorldTelemetry: (runDir: string) => Promise<RunTelemetrySample[] | null>;
34
+ /** `true` when at least one sample carries at least one variable — the gate that decides whether a run has real gauge data to render (`RunViewModel.variableSamples`'s own doc comment: never a fabricated empty gauge). */
35
+ export declare const hasVariableSamples: (samples: readonly RunTelemetrySample[] | null) => boolean;
36
+ /**
37
+ * Reads `spawnfile/up-receipt.json`'s `engines[]` (per-agent engine
38
+ * breakdown) when a composed run-dir carries one. Loosely shaped, not
39
+ * schema-validated — this is an optional, best-effort finer-grained signal
40
+ * layered on top of the manifest's own single `engine` field, so a
41
+ * malformed/partial file degrades to `undefined` (the caller falls back to
42
+ * `manifest.engine`) rather than throwing. No run-dir currently ships this
43
+ * file, but a composed driver's `spawnfileReceipts.ts` up-receipt schema
44
+ * already carries `engines` when a real deployment reports mixed per-agent
45
+ * engines, so this reads it the moment one does.
46
+ */
47
+ export declare const readUpReceiptEngines: (runDir: string) => Promise<EngineEntry[] | undefined>;
48
+ //# sourceMappingURL=runRawArtifacts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runRawArtifacts.d.ts","sourceRoot":"","sources":["../../src/view/runRawArtifacts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAwB,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAmBrH,eAAO,MAAM,qBAAqB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAYhG,CAAC;AAkBF;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,GAAI,KAAK,OAAO,KAAG,aAWrD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,aAAa,CAsB1E,CAAC;AASF;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,kBAAkB,EAAE,GAAG,IAAI,CAe5F,CAAC;AAEF,4NAA4N;AAC5N,eAAO,MAAM,kBAAkB,GAAI,SAAS,SAAS,kBAAkB,EAAE,GAAG,IAAI,KAAG,OACK,CAAC;AAEzF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,WAAW,EAAE,GAAG,SAAS,CAkB5F,CAAC"}
@@ -0,0 +1,143 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ /**
4
+ * Shared, dependency-free reads of the raw run-directory artifacts every
5
+ * run-reader/run-replay data path needs: the moltnet transcript, the
6
+ * per-bank mneme event logs, and (increment 3) the world telemetry
7
+ * snapshot. Extracted out of `runViewModel.ts` so `runTimeline.ts`
8
+ * (`buildRunTimeline`) can read the exact same records without
9
+ * re-implementing or duplicating file I/O (`AGENTS.md`: small composable
10
+ * modules, no invented state).
11
+ */
12
+ const parseJsonlLines = (text) => text
13
+ .split(/\r?\n/u)
14
+ .map((line) => line.trim())
15
+ .filter((line) => line.length > 0)
16
+ .map((line) => JSON.parse(line));
17
+ export const readMnemeEventsByBank = async (runDir) => {
18
+ const mnemeDir = path.join(runDir, "raw", "mneme");
19
+ const bankDirs = await readdir(mnemeDir, { withFileTypes: true }).catch(() => []);
20
+ const byBank = new Map();
21
+ for (const entry of bankDirs) {
22
+ if (!entry.isDirectory())
23
+ continue;
24
+ const text = await readFile(path.join(mnemeDir, entry.name, "events.jsonl"), "utf8").catch(() => null);
25
+ if (text === null)
26
+ continue;
27
+ byBank.set(entry.name, parseJsonlLines(text));
28
+ }
29
+ return byBank;
30
+ };
31
+ const isExportShape = (value) => typeof value === "object" && value !== null && Array.isArray(value.conversations);
32
+ /**
33
+ * Normalizes both `raw/moltnet/transcript.json` shapes into one internal
34
+ * message list: the golden fixture's `{seedMessageText, transcript: [...]}`
35
+ * and a real composed run's export shape `{conversations: [{messages:
36
+ * [...]}] }` (`moltnet/transcript-export.ts`'s `MoltnetExportedTranscript`).
37
+ * Every conversation's messages are flattened and re-sorted by
38
+ * `created_at` so multi-conversation exports still read chronologically;
39
+ * single-conversation runs (the only shape produced today) are a no-op
40
+ * re-sort of an already-ordered list.
41
+ */
42
+ export const normalizeRawTranscript = (raw) => {
43
+ if (isExportShape(raw)) {
44
+ const transcript = raw.conversations
45
+ .flatMap((conversation) => conversation.messages ?? [])
46
+ .slice()
47
+ .sort((left, right) => Date.parse(left.created_at) - Date.parse(right.created_at));
48
+ return { transcript };
49
+ }
50
+ const golden = raw;
51
+ return { seedMessageText: golden.seedMessageText, transcript: golden.transcript ?? [] };
52
+ };
53
+ /**
54
+ * Reads every moltnet transcript in a run-dir and merges them into one
55
+ * chronologically-sorted message list. A single-network run writes the flat
56
+ * `raw/moltnet/transcript.json` (the golden/office-sim shape); a multi-network
57
+ * composed run (the jungian psyche) writes one per network under
58
+ * `raw/moltnet/<network_id>/transcript.json`. Both are read and unioned so the
59
+ * timeline's `messagesById` spans every room across every network — the join
60
+ * an interior-council membrane needs. Message ids are globally-unique UUIDs, so
61
+ * a flat re-sort of the union is a correct chronological interleave.
62
+ */
63
+ export const readTranscript = async (runDir) => {
64
+ const moltnetDir = path.join(runDir, "raw", "moltnet");
65
+ const files = [path.join(moltnetDir, "transcript.json")];
66
+ const entries = await readdir(moltnetDir, { withFileTypes: true }).catch(() => []);
67
+ for (const entry of entries) {
68
+ if (entry.isDirectory())
69
+ files.push(path.join(moltnetDir, entry.name, "transcript.json"));
70
+ }
71
+ let seedMessageText;
72
+ const merged = [];
73
+ for (const file of files) {
74
+ const text = await readFile(file, "utf8").catch(() => null);
75
+ if (text === null)
76
+ continue;
77
+ const normalized = normalizeRawTranscript(JSON.parse(text));
78
+ if (seedMessageText === undefined && normalized.seedMessageText !== undefined) {
79
+ seedMessageText = normalized.seedMessageText;
80
+ }
81
+ merged.push(...normalized.transcript);
82
+ }
83
+ merged.sort((left, right) => Date.parse(left.created_at) - Date.parse(right.created_at));
84
+ return { seedMessageText, transcript: merged };
85
+ };
86
+ /**
87
+ * Reads `world/telemetry.json` when present (world-driven runs only —
88
+ * `office-secret-v0-golden` ships one, `office-sim-golden` does not).
89
+ * Returns `null` on any missing/malformed file rather than throwing: this
90
+ * artifact is optional, never a requirement for a run to render
91
+ * (`runDetect.ts`'s shape check does not require it).
92
+ */
93
+ export const readWorldTelemetry = async (runDir) => {
94
+ const raw = await readFile(path.join(runDir, "world", "telemetry.json"), "utf8").catch(() => null);
95
+ if (raw === null)
96
+ return null;
97
+ try {
98
+ const parsed = JSON.parse(raw);
99
+ if (!Array.isArray(parsed.samples))
100
+ return null;
101
+ return parsed.samples.map((sample) => ({
102
+ tick: sample.tick,
103
+ simTime: sample.sim_time,
104
+ phase: sample.phase,
105
+ variables: sample.variables ?? {},
106
+ }));
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ };
112
+ /** `true` when at least one sample carries at least one variable — the gate that decides whether a run has real gauge data to render (`RunViewModel.variableSamples`'s own doc comment: never a fabricated empty gauge). */
113
+ export const hasVariableSamples = (samples) => samples !== null && samples.some((sample) => Object.keys(sample.variables).length > 0);
114
+ /**
115
+ * Reads `spawnfile/up-receipt.json`'s `engines[]` (per-agent engine
116
+ * breakdown) when a composed run-dir carries one. Loosely shaped, not
117
+ * schema-validated — this is an optional, best-effort finer-grained signal
118
+ * layered on top of the manifest's own single `engine` field, so a
119
+ * malformed/partial file degrades to `undefined` (the caller falls back to
120
+ * `manifest.engine`) rather than throwing. No run-dir currently ships this
121
+ * file, but a composed driver's `spawnfileReceipts.ts` up-receipt schema
122
+ * already carries `engines` when a real deployment reports mixed per-agent
123
+ * engines, so this reads it the moment one does.
124
+ */
125
+ export const readUpReceiptEngines = async (runDir) => {
126
+ const raw = await readFile(path.join(runDir, "spawnfile", "up-receipt.json"), "utf8").catch(() => null);
127
+ if (raw === null)
128
+ return undefined;
129
+ try {
130
+ const parsed = JSON.parse(raw);
131
+ if (!Array.isArray(parsed.engines))
132
+ return undefined;
133
+ const entries = parsed.engines.filter((entry) => typeof entry === "object" &&
134
+ entry !== null &&
135
+ typeof entry.engine === "string" &&
136
+ entry.engine.length > 0 &&
137
+ (typeof entry.agent === "string" || entry.agent === undefined));
138
+ return entries.length > 0 ? entries : undefined;
139
+ }
140
+ catch {
141
+ return undefined;
142
+ }
143
+ };