@method-ai-hq/sdk 0.0.0-stage → 0.14.2

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 (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -2
  3. package/dist/packages/contracts/src/identity.d.ts +16 -0
  4. package/dist/packages/contracts/src/identity.js +41 -0
  5. package/dist/packages/contracts/src/method-package.d.ts +71 -0
  6. package/dist/packages/contracts/src/method-package.js +22 -0
  7. package/dist/packages/method-document/src/conversion-report.d.ts +138 -0
  8. package/dist/packages/method-document/src/conversion-report.js +23 -0
  9. package/dist/packages/sdk/examples/daily-briefing/README.md +31 -0
  10. package/dist/packages/sdk/examples/daily-briefing/TASK.md +3 -0
  11. package/dist/packages/sdk/examples/daily-briefing/approved-report.md +138 -0
  12. package/dist/packages/sdk/examples/daily-briefing/briefing_artifacts.py +39 -0
  13. package/dist/packages/sdk/examples/daily-briefing/briefing_check_inputs.py +47 -0
  14. package/dist/packages/sdk/examples/daily-briefing/briefing_files.py +31 -0
  15. package/dist/packages/sdk/examples/daily-briefing/briefing_manifest.py +23 -0
  16. package/dist/packages/sdk/examples/daily-briefing/briefing_markdown.py +67 -0
  17. package/dist/packages/sdk/examples/daily-briefing/briefing_progress.py +18 -0
  18. package/dist/packages/sdk/examples/daily-briefing/briefing_render.py +71 -0
  19. package/dist/packages/sdk/examples/daily-briefing/briefing_sessions.py +37 -0
  20. package/dist/packages/sdk/examples/daily-briefing/briefing_times.py +158 -0
  21. package/dist/packages/sdk/examples/daily-briefing/briefing_validation.py +101 -0
  22. package/dist/packages/sdk/examples/daily-briefing/briefing_website.py +215 -0
  23. package/dist/packages/sdk/examples/daily-briefing/checks.json +19 -0
  24. package/dist/packages/sdk/examples/daily-briefing/daily-briefing.method +158 -0
  25. package/dist/packages/sdk/examples/daily-briefing/inputs.json +1 -0
  26. package/dist/packages/sdk/examples/daily-briefing/package-lock.json +10 -0
  27. package/dist/packages/sdk/examples/daily-briefing/package.json +4 -0
  28. package/dist/packages/sdk/examples/daily-briefing/pyproject.toml +8 -0
  29. package/dist/packages/sdk/examples/daily-briefing/reader/reader.css +35 -0
  30. package/dist/packages/sdk/examples/daily-briefing/reader/reader.js +42 -0
  31. package/dist/packages/sdk/examples/daily-briefing/sample/context.json +1 -0
  32. package/dist/packages/sdk/examples/daily-briefing/sample/documents/document-001.md +219 -0
  33. package/dist/packages/sdk/examples/daily-briefing/sample/media.json +1 -0
  34. package/dist/packages/sdk/examples/daily-briefing/sample/metadata/documents.json +39 -0
  35. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0001.json +49 -0
  36. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0002.json +49 -0
  37. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0003.json +49 -0
  38. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0004.json +49 -0
  39. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0005.json +49 -0
  40. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0006.json +49 -0
  41. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0007.json +49 -0
  42. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0008.json +49 -0
  43. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0009.json +49 -0
  44. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0010.json +49 -0
  45. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0011.json +49 -0
  46. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0012.json +49 -0
  47. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0013.json +49 -0
  48. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0014.json +49 -0
  49. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0015.json +49 -0
  50. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0016.json +49 -0
  51. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0017.json +49 -0
  52. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0018.json +49 -0
  53. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0019.json +49 -0
  54. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0020.json +49 -0
  55. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0021.json +49 -0
  56. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0022.json +49 -0
  57. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0023.json +49 -0
  58. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0024.json +49 -0
  59. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0025.json +49 -0
  60. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0026.json +49 -0
  61. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0027.json +49 -0
  62. package/dist/packages/sdk/examples/daily-briefing/sample/records/chatgpt-0028.json +49 -0
  63. package/dist/packages/sdk/examples/daily-briefing/sample/sessions.json +1 -0
  64. package/dist/packages/sdk/examples/daily-briefing/sample/timeline.jsonl +28 -0
  65. package/dist/packages/sdk/examples/daily-briefing/sample/untimed.jsonl +0 -0
  66. package/dist/packages/sdk/examples/daily-briefing/sample-output.zip +0 -0
  67. package/dist/packages/sdk/examples/daily-briefing/sample-report.md +51 -0
  68. package/dist/packages/sdk/examples/daily-briefing/uv.lock +128 -0
  69. package/dist/packages/sdk/examples/daily-briefing/vendor/marked-LICENSE.md +44 -0
  70. package/dist/packages/sdk/examples/daily-briefing/vendor/marked.mjs +77 -0
  71. package/dist/packages/sdk/examples/files.json +126 -0
  72. package/dist/packages/sdk/examples/message-routing/README.md +23 -0
  73. package/dist/packages/sdk/examples/message-routing/TASK.md +5 -0
  74. package/dist/packages/sdk/examples/message-routing/choose-destination.mjs +10 -0
  75. package/dist/packages/sdk/examples/message-routing/create-ticket.mjs +15 -0
  76. package/dist/packages/sdk/examples/message-routing/inputs.json +1 -0
  77. package/dist/packages/sdk/examples/message-routing/message-routing.method +45 -0
  78. package/dist/packages/sdk/examples/message-routing/result.fixture.json +6 -0
  79. package/dist/packages/sdk/examples/message-routing/routing.mjs +29 -0
  80. package/dist/packages/sdk/examples/message-routing/routing.test.mjs +27 -0
  81. package/dist/packages/sdk/examples/message-routing/ticket.method +88 -0
  82. package/dist/packages/sdk/examples/message-routing/ticket.test.mjs +64 -0
  83. package/dist/packages/sdk/examples/notes-summary/README.md +23 -0
  84. package/dist/packages/sdk/examples/notes-summary/TASK.md +3 -0
  85. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/case.json +36 -0
  86. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/examples.json +16 -0
  87. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/files/0 +10 -0
  88. package/dist/packages/sdk/examples/notes-summary/cases/dates-in-decisions/recording.json +195 -0
  89. package/dist/packages/sdk/examples/notes-summary/notes/2026-10-05-standup.md +4 -0
  90. package/dist/packages/sdk/examples/notes-summary/notes/2026-10-07-planning.md +3 -0
  91. package/dist/packages/sdk/examples/notes-summary/notes-summary.method +54 -0
  92. package/dist/packages/sdk/examples/notes-summary/out/.keep +0 -0
  93. package/dist/packages/sdk/examples/notes-summary/read-notes.mjs +4 -0
  94. package/dist/packages/sdk/examples/notes-summary/save.mjs +7 -0
  95. package/dist/packages/sdk/examples/outbound-management/README.md +119 -0
  96. package/dist/packages/sdk/examples/outbound-management/TASK.md +5 -0
  97. package/dist/packages/sdk/examples/outbound-management/check_plan.py +16 -0
  98. package/dist/packages/sdk/examples/outbound-management/check_sources.py +14 -0
  99. package/dist/packages/sdk/examples/outbound-management/crm.py +153 -0
  100. package/dist/packages/sdk/examples/outbound-management/crm_test.py +141 -0
  101. package/dist/packages/sdk/examples/outbound-management/fixtures/README.md +15 -0
  102. package/dist/packages/sdk/examples/outbound-management/fixtures/crm.json +54 -0
  103. package/dist/packages/sdk/examples/outbound-management/fixtures/day-1/messages.json +29 -0
  104. package/dist/packages/sdk/examples/outbound-management/fixtures/day-2/messages.json +38 -0
  105. package/dist/packages/sdk/examples/outbound-management/fixtures/observations.json +12 -0
  106. package/dist/packages/sdk/examples/outbound-management/fixtures/plan.json +69 -0
  107. package/dist/packages/sdk/examples/outbound-management/fixtures/receipt.json +1 -0
  108. package/dist/packages/sdk/examples/outbound-management/fixtures/state.json +99 -0
  109. package/dist/packages/sdk/examples/outbound-management/fixtures/tasks.md +16 -0
  110. package/dist/packages/sdk/examples/outbound-management/inputs.json +4 -0
  111. package/dist/packages/sdk/examples/outbound-management/outbound.method +277 -0
  112. package/dist/packages/sdk/examples/outbound-management/read_crm.py +4 -0
  113. package/dist/packages/sdk/examples/outbound-management/save_day.py +4 -0
  114. package/dist/packages/sdk/examples/outbound-management/starter/crm.json +11 -0
  115. package/dist/packages/sdk/examples/social-briefing/README.md +41 -0
  116. package/dist/packages/sdk/examples/social-briefing/TASK.md +5 -0
  117. package/dist/packages/sdk/examples/social-briefing/inputs.json +5 -0
  118. package/dist/packages/sdk/examples/social-briefing/result.fixture.json +14 -0
  119. package/dist/packages/sdk/examples/social-briefing/social-briefing.method +110 -0
  120. package/dist/packages/sdk/examples/support-triage/README.md +77 -0
  121. package/dist/packages/sdk/examples/support-triage/TASK.md +3 -0
  122. package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/case.json +40 -0
  123. package/dist/packages/sdk/examples/support-triage/cases/double-charge-to-billing/recording.json +460 -0
  124. package/dist/packages/sdk/examples/support-triage/fake-helpdesk.mjs +38 -0
  125. package/dist/packages/sdk/examples/support-triage/helpdesk.mjs +33 -0
  126. package/dist/packages/sdk/examples/support-triage/inputs.json +1 -0
  127. package/dist/packages/sdk/examples/support-triage/read-ticket.mjs +8 -0
  128. package/dist/packages/sdk/examples/support-triage/route.mjs +4 -0
  129. package/dist/packages/sdk/examples/support-triage/sample-tickets.json +5 -0
  130. package/dist/packages/sdk/examples/support-triage/set-ticket.mjs +9 -0
  131. package/dist/packages/sdk/examples/support-triage/support-triage.method +165 -0
  132. package/dist/packages/sdk/examples/support-triage/triage.mjs +38 -0
  133. package/dist/packages/sdk/examples/support-triage/triage.test.mjs +68 -0
  134. package/dist/packages/sdk/observers/mail.delivery/fetch.mjs +87 -0
  135. package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce-by-recipient.json +1 -0
  136. package/dist/packages/sdk/observers/mail.delivery/fixtures/bounce.json +1 -0
  137. package/dist/packages/sdk/observers/mail.delivery/fixtures/delayed.json +1 -0
  138. package/dist/packages/sdk/observers/mail.delivery/fixtures/delivered.json +1 -0
  139. package/dist/packages/sdk/observers/mail.delivery/fixtures/empty.json +1 -0
  140. package/dist/packages/sdk/observers/mail.delivery/fixtures/other-message.json +1 -0
  141. package/dist/packages/sdk/observers/mail.delivery/judge.mjs +13 -0
  142. package/dist/packages/sdk/observers/mail.delivery/observer.json +14 -0
  143. package/dist/packages/sdk/package.json +232 -0
  144. package/dist/packages/sdk/src/authoring-example.d.ts +91 -0
  145. package/dist/packages/sdk/src/authoring-example.js +50 -0
  146. package/dist/packages/sdk/src/authoring-instructions.d.ts +20 -0
  147. package/dist/packages/sdk/src/authoring-instructions.js +151 -0
  148. package/dist/packages/sdk/src/authoring-prompt.d.ts +2 -0
  149. package/dist/packages/sdk/src/authoring-prompt.js +8 -0
  150. package/dist/packages/sdk/src/authoring.d.ts +10 -0
  151. package/dist/packages/sdk/src/authoring.js +287 -0
  152. package/dist/packages/sdk/src/bindings.d.ts +4 -0
  153. package/dist/packages/sdk/src/bindings.js +124 -0
  154. package/dist/packages/sdk/src/bridge.d.ts +2 -0
  155. package/dist/packages/sdk/src/bridge.js +23 -0
  156. package/dist/packages/sdk/src/browser-runtime/pyproject.toml +5 -0
  157. package/dist/packages/sdk/src/browser-runtime/tools.json +189 -0
  158. package/dist/packages/sdk/src/browser-runtime/uv.lock +3894 -0
  159. package/dist/packages/sdk/src/browser-service.py +127 -0
  160. package/dist/packages/sdk/src/browser.d.ts +38 -0
  161. package/dist/packages/sdk/src/browser.js +203 -0
  162. package/dist/packages/sdk/src/capabilities.d.ts +13 -0
  163. package/dist/packages/sdk/src/capabilities.js +68 -0
  164. package/dist/packages/sdk/src/classification-client.d.ts +4 -0
  165. package/dist/packages/sdk/src/classification-client.js +27 -0
  166. package/dist/packages/sdk/src/cloud.d.ts +23 -0
  167. package/dist/packages/sdk/src/cloud.js +43 -0
  168. package/dist/packages/sdk/src/computer-settings.d.ts +28 -0
  169. package/dist/packages/sdk/src/computer-settings.js +57 -0
  170. package/dist/packages/sdk/src/connect.d.ts +47 -0
  171. package/dist/packages/sdk/src/connect.js +203 -0
  172. package/dist/packages/sdk/src/current-inspection.d.ts +3 -0
  173. package/dist/packages/sdk/src/current-inspection.js +146 -0
  174. package/dist/packages/sdk/src/current-runtime.d.ts +14 -0
  175. package/dist/packages/sdk/src/current-runtime.js +167 -0
  176. package/dist/packages/sdk/src/doctor.d.ts +17 -0
  177. package/dist/packages/sdk/src/doctor.js +68 -0
  178. package/dist/packages/sdk/src/executable.d.ts +2 -0
  179. package/dist/packages/sdk/src/executable.js +22 -0
  180. package/dist/packages/sdk/src/explain-effects.d.ts +32 -0
  181. package/dist/packages/sdk/src/explain-effects.js +192 -0
  182. package/dist/packages/sdk/src/explain.d.ts +115 -0
  183. package/dist/packages/sdk/src/explain.js +441 -0
  184. package/dist/packages/sdk/src/files.d.ts +9 -0
  185. package/dist/packages/sdk/src/files.js +50 -0
  186. package/dist/packages/sdk/src/guard-keys.d.ts +5 -0
  187. package/dist/packages/sdk/src/guard-keys.js +42 -0
  188. package/dist/packages/sdk/src/hosted-models.d.ts +49 -0
  189. package/dist/packages/sdk/src/hosted-models.js +81 -0
  190. package/dist/packages/sdk/src/improve.d.ts +55 -0
  191. package/dist/packages/sdk/src/improve.js +159 -0
  192. package/dist/packages/sdk/src/index.d.ts +13 -0
  193. package/dist/packages/sdk/src/index.js +13 -0
  194. package/dist/packages/sdk/src/inspect.d.ts +6 -0
  195. package/dist/packages/sdk/src/inspect.js +17 -0
  196. package/dist/packages/sdk/src/link.d.ts +12 -0
  197. package/dist/packages/sdk/src/link.js +53 -0
  198. package/dist/packages/sdk/src/local-cli.d.ts +24 -0
  199. package/dist/packages/sdk/src/local-cli.js +146 -0
  200. package/dist/packages/sdk/src/local-setup.d.ts +15 -0
  201. package/dist/packages/sdk/src/local-setup.js +42 -0
  202. package/dist/packages/sdk/src/method-client.d.ts +30 -0
  203. package/dist/packages/sdk/src/method-client.js +250 -0
  204. package/dist/packages/sdk/src/method-files.d.ts +29 -0
  205. package/dist/packages/sdk/src/method-files.js +93 -0
  206. package/dist/packages/sdk/src/method-help.d.ts +19 -0
  207. package/dist/packages/sdk/src/method-help.js +404 -0
  208. package/dist/packages/sdk/src/method-issues.d.ts +86 -0
  209. package/dist/packages/sdk/src/method-issues.js +192 -0
  210. package/dist/packages/sdk/src/method-sync.d.ts +46 -0
  211. package/dist/packages/sdk/src/method-sync.js +293 -0
  212. package/dist/packages/sdk/src/method.d.ts +5 -0
  213. package/dist/packages/sdk/src/method.js +359 -0
  214. package/dist/packages/sdk/src/model-checks.d.ts +96 -0
  215. package/dist/packages/sdk/src/model-checks.js +181 -0
  216. package/dist/packages/sdk/src/outbox.d.ts +120 -0
  217. package/dist/packages/sdk/src/outbox.js +297 -0
  218. package/dist/packages/sdk/src/own-model-key.d.ts +22 -0
  219. package/dist/packages/sdk/src/own-model-key.js +76 -0
  220. package/dist/packages/sdk/src/prepare.d.ts +24 -0
  221. package/dist/packages/sdk/src/prepare.js +245 -0
  222. package/dist/packages/sdk/src/process.d.ts +15 -0
  223. package/dist/packages/sdk/src/process.js +107 -0
  224. package/dist/packages/sdk/src/production.d.ts +182 -0
  225. package/dist/packages/sdk/src/production.js +284 -0
  226. package/dist/packages/sdk/src/progress.d.ts +12 -0
  227. package/dist/packages/sdk/src/progress.js +48 -0
  228. package/dist/packages/sdk/src/proposals.d.ts +73 -0
  229. package/dist/packages/sdk/src/proposals.js +284 -0
  230. package/dist/packages/sdk/src/quality.d.ts +82 -0
  231. package/dist/packages/sdk/src/quality.js +294 -0
  232. package/dist/packages/sdk/src/result-files.d.ts +5 -0
  233. package/dist/packages/sdk/src/result-files.js +149 -0
  234. package/dist/packages/sdk/src/result-transfer.d.ts +21 -0
  235. package/dist/packages/sdk/src/result-transfer.js +37 -0
  236. package/dist/packages/sdk/src/run-data.d.ts +11 -0
  237. package/dist/packages/sdk/src/run-data.js +80 -0
  238. package/dist/packages/sdk/src/run-method.d.ts +2 -0
  239. package/dist/packages/sdk/src/run-method.js +2 -0
  240. package/dist/packages/sdk/src/run-saved.d.ts +5 -0
  241. package/dist/packages/sdk/src/run-saved.js +138 -0
  242. package/dist/packages/sdk/src/run-worker.d.ts +6 -0
  243. package/dist/packages/sdk/src/run-worker.js +131 -0
  244. package/dist/packages/sdk/src/secrets.d.ts +37 -0
  245. package/dist/packages/sdk/src/secrets.js +196 -0
  246. package/dist/packages/sdk/src/update.d.ts +5 -0
  247. package/dist/packages/sdk/src/update.js +39 -0
  248. package/dist/packages/sdk/src/versions.d.ts +58 -0
  249. package/dist/packages/sdk/src/versions.js +144 -0
  250. package/dist/packages/sdk/src/worker.d.ts +120 -0
  251. package/dist/packages/sdk/src/worker.js +443 -0
  252. package/dist/packages/workflow-language/src/execution.d.ts +1 -0
  253. package/dist/packages/workflow-language/src/execution.js +1 -0
  254. package/dist/packages/workflow-language/src/inspection.d.ts +990 -0
  255. package/dist/packages/workflow-language/src/inspection.js +130 -0
  256. package/dist/packages/workflow-language/src/result-files.d.ts +160 -0
  257. package/dist/packages/workflow-language/src/result-files.js +67 -0
  258. package/dist/packages/workflow-language/src/schema.d.ts +207 -0
  259. package/dist/packages/workflow-language/src/schema.js +56 -0
  260. package/dist/packages/workflow-language/src/validate.d.ts +11 -0
  261. package/dist/packages/workflow-language/src/validate.js +77 -0
  262. package/package.json +230 -4
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env python3
2
+ """Build the complete website from the writer's selected content.
3
+
4
+ Reads accepted prepared data and an Markdown briefing. Never rewrites either.
5
+ """
6
+ import argparse
7
+ import datetime as dt
8
+ import html
9
+ import json
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+ from urllib.parse import urlsplit
16
+ from briefing_markdown import parse
17
+ from briefing_sessions import social_sessions
18
+ from briefing_progress import progress
19
+
20
+ TEMPLATES=Path(__file__).resolve().parent/'reader'
21
+
22
+
23
+ def load(path):return json.loads(path.read_text())
24
+ def optional(path):return load(path) if path.exists() else []
25
+ def h(value):return html.escape(str(value),quote=True)
26
+ def put(path,text):
27
+ path.parent.mkdir(parents=True,exist_ok=True);path.write_text(text);path.chmod(0o600)
28
+ def stamp(value):return dt.datetime.fromisoformat(value)
29
+ def clock(value):return stamp(value).strftime('%I:%M %p').lstrip('0')
30
+ def quotes(text):
31
+ result=[];depth=0;start=0
32
+ for i,char in enumerate(text):
33
+ if char=='“':
34
+ if depth==0:start=i+1
35
+ depth+=1
36
+ elif char=='”' and depth:
37
+ depth-=1
38
+ if depth==0:result.append(text[start:i])
39
+ return result
40
+
41
+ def readable_time(e):
42
+ time=e['time']
43
+ return stamp(time['local']).strftime('%b %d · %I:%M %p').replace(' 0',' ') if time['local'] else time['day']
44
+ def host(url):return (urlsplit(url or '').hostname or '').removeprefix('www.').removeprefix('mobile.')
45
+ def safe_url(url):return url if urlsplit(url or '').scheme in ['http','https'] else ''
46
+
47
+ def render_markdown(texts,marked,source_urls=None):
48
+ # Original HTML is shown as source text. Links open HTTP(S), anchors, or local website pages.
49
+ code="""import {marked} from MARKED;
50
+ const escape=s=>s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;');
51
+ marked.use({renderer:{html(token){return escape(token.text)},link(token){let label=this.parser.parseInline(token.tokens);let href=token.href;if(href.startsWith('source:')){const ids=href.slice(7).split(',');return ids.map((id,i)=>{if(!Object.hasOwn(sources,id))throw Error('Unknown source record: '+id);return '<a target="_blank" rel="noopener noreferrer" href="'+escape(sources[id])+'">'+(i===0?label:'['+(i+1)+']')+'</a>'}).join(' ')}return /^(https?:|#|[a-zA-Z0-9_-]+[.]html(?:#|$))/.test(href)?'<a target="_blank" rel="noopener noreferrer" href="'+escape(href)+'">'+label+'</a>':label},image(token){return escape(token.text||'Image referenced in saved text')}}});
52
+ let chunks=[];for await(const c of process.stdin)chunks.push(c);const {texts:inputs,sources}=JSON.parse(Buffer.concat(chunks).toString());
53
+ process.stdout.write(JSON.stringify(Object.fromEntries(Object.entries(inputs).map(([key,value])=>[key,marked.parse(value)]))));
54
+ """.replace('MARKED',json.dumps(marked.resolve().as_uri()))
55
+ return json.loads(subprocess.run(['node','--input-type=module','-e',code],input=json.dumps({'texts':texts,'sources':source_urls or {}}),text=True,capture_output=True,check=True).stdout)
56
+
57
+
58
+ def convert_photo(original,target):
59
+ from PIL import Image, ImageOps
60
+ from pillow_heif import register_heif_opener
61
+ register_heif_opener()
62
+ with Image.open(original) as source:
63
+ preview=ImageOps.exif_transpose(source)
64
+ preview.thumbnail((1600,1600))
65
+ if preview.mode in ('RGBA','LA') or 'transparency' in preview.info:
66
+ rgba=preview.convert('RGBA');background=Image.new('RGB',rgba.size,'white');background.paste(rgba,mask=rgba.getchannel('A'));preview=background
67
+ preview.convert('RGB').save(target,format='JPEG',quality=85)
68
+
69
+
70
+ def build(prepared,briefing_file,out,marked,day=None,proposal=None):
71
+ proposal=proposal or parse(briefing_file.read_text(),day,marked)
72
+ dest=out
73
+ dest.mkdir(parents=True,exist_ok=True)
74
+ for name in ['reader.css','reader.js']:shutil.copyfile(TEMPLATES/name,dest/name)
75
+ raw={f.stem:load(f) for f in (prepared/'records').glob('*.json')}
76
+ records={cid:data['prepared'] for cid,data in raw.items()}
77
+ entries={r['entry_id']:r for name in ['timeline.jsonl','untimed.jsonl'] for r in map(json.loads,(prepared/name).read_text().splitlines())}
78
+ day=proposal['day']
79
+ title=proposal['title']
80
+ day_label=dt.date.fromisoformat(day).strftime('%b %d, %Y').replace(' 0',' ').upper()
81
+ source_ids={cid:r['source_record_ids'] for r in entries.values() for cid in r['source_record_ids']}
82
+ documents=load(prepared/'metadata/documents.json')
83
+ doc_by_record={cid:d for d in documents for cid in d['record_ids']}
84
+ source_ranges=proposal.get('source_ranges',{})
85
+ texts={cid:e['text'] for cid,e in records.items()}
86
+ for supplement in proposal.get('supplements',[]):texts[supplement['id']]=supplement['text']
87
+ for cid,ranges in source_ranges.items():
88
+ for name,(first,last) in ranges.items():texts[cid+':'+name]='\n'.join(texts[cid].splitlines()[first-1:last])
89
+ contexts=optional(prepared/'context.json')
90
+ for c in contexts:
91
+ data=load(prepared/c['source_json'])
92
+ for r in data['records']:texts['context-'+(r.get('message_id') or r['id'])]=r['text']
93
+ def document_page(doc):
94
+ return 'source-'+Path(doc['path']).stem+'.html'
95
+ source_urls={cid:document_page(doc)+'#'+cid for cid,doc in doc_by_record.items()}
96
+ source_urls.update({s['id']:s['id']+'.html' for s in proposal.get('supplements',[])})
97
+ rendered=render_markdown(texts,marked,source_urls)
98
+ media={}
99
+ for asset in optional(prepared/'media.json'):
100
+ original=prepared/asset['path'];name=Path(asset['path']).stem+'.jpg';target=dest/'media'/name;target.parent.mkdir(exist_ok=True)
101
+ if not target.exists():
102
+ convert_photo(original,target)
103
+ original_target=dest/asset['path'];original_target.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(original,original_target)
104
+ for cid in asset['records']:media[cid]={'png':'media/'+name,'original':asset['path']}
105
+ def photo(cid,caption):
106
+ asset=media[cid]
107
+ return f'<figure class="photo"><button data-photo="{h(asset["png"])}" aria-label="Enlarge photo"><img src="{h(asset["png"])}" alt="{h(caption)}" loading="lazy"></button><figcaption>{h(caption)} · <a href="{h(asset["original"])}">Original</a></figcaption></figure>'
108
+ groups=social_sessions(prepared)
109
+ browser_rows=[r for r in entries.values() if records[r['source_record_ids'][0]]['source'] in ['browser','google_takeout']]
110
+ navigation=' · '.join((['<a href="sessions.html">X and Instagram sessions</a>'] if groups else [])+(['<a href="browser.html">All browser visits</a>'] if browser_rows else []))
111
+ def page(title,body):
112
+ return f'<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{h(title)}</title><link rel="stylesheet" href="reader.css"></head><body><nav><a href="index.html">{h(day_label)}</a><span>{navigation}</span></nav><main>{body}</main><dialog><button data-close-photo>Close</button><img alt=""></dialog><script src="reader.js"></script></body></html>'
113
+ for supplement in proposal.get('supplements',[]):
114
+ sid=supplement['id'];filename=sid+'.html'
115
+ put(dest/filename,page(supplement['title'],'<h1>'+h(supplement['title'])+'</h1><div class="source-text">'+rendered[sid]+'</div>'))
116
+ put(dest/'data'/f'{sid}.json',json.dumps({'id':sid,'html':rendered[sid],'time':'','date':'','speaker':'','basis':'','document':filename,'reading_label':'Full supporting document','ranges':{name:rendered[sid+':'+name] for name in source_ranges.get(sid,{})}},ensure_ascii=False))
117
+ def speaker(e):
118
+ role=e['actor_role']
119
+ if e['source']=='imessage':return 'User' if role=='user_written' else e['attributes'].get('sender') or 'Not supplied'
120
+ return {'user_written':'User','user':'User','assistant':'Assistant','other_person':'Other person'}.get(role,'')
121
+ def record_html(cid):
122
+ e=records[cid];meta=' · '.join(filter(None,[readable_time(e),speaker(e)]))
123
+ return f'<article class="record" id="{h(cid)}"><div class="record-head">{h(meta)} <a href="records/{h(cid)}.json">Source JSON</a></div><div class="source-text">{rendered[cid]}</div>'+ (photo(cid,'Saved photo') if cid in media else '')+'</article>'
124
+ for cid,e in records.items():
125
+ doc=doc_by_record[cid];docname=document_page(doc)
126
+ basis=('Saved with this activity' if e['text_basis']=='dated_google_activity_title' else 'Browser saved title') if e['source'] in ['browser','google_takeout'] else ''
127
+ if e.get('post_text_partial'):basis+=' · partial saved title'
128
+ data={'id':cid,'html':rendered[cid],'time':readable_time(e),'other_sources':[c for c in dict.fromkeys(source_ids.get(cid,[])) if c!=cid],'date':e['time']['day'],'speaker':speaker(e),'basis':basis,'document':docname+'#'+cid,'conversation':doc['kind']=='conversation','reading_label':'Read full conversation' if doc['kind']=='conversation' else 'Read full note' if e['source'] in ['roam','apple_notes'] else 'Read full document','ranges':{name:rendered[cid+':'+name] for name in source_ranges.get(cid,{})}}
129
+ put(dest/'data'/f'{cid}.json',json.dumps(data,ensure_ascii=False))
130
+ target=dest/'records'/f'{cid}.json';target.parent.mkdir(exist_ok=True);shutil.copyfile(prepared/'records'/f'{cid}.json',target)
131
+ for doc in documents:
132
+ body='<h1>'+h(doc['title'])+'</h1>'
133
+ body+=''.join(record_html(cid) for cid in doc['record_ids'])
134
+ put(dest/document_page(doc),page(doc['title'],body))
135
+ for c in contexts:
136
+ data=load(prepared/c['source_json']);body='<h1>'+h(c['title'])+'</h1><p class="small">Earlier conversation</p>'
137
+ for r in data['records']:
138
+ cid='context-'+(r.get('message_id') or r['id']);body+=f'<article id="{h(cid)}" class="record"><div class="record-head">{h(r.get("occurred_at") or r.get("day") or "Time not supplied")} · {h(r.get("role") or "")}</div><div class="source-text">{rendered[cid]}</div></article>'
139
+ put(dest/('context-'+c['conversation_id']+'.html'),page(c['title'],body))
140
+ def cite(ids,key,text='',label=None,range_name=None):
141
+ return f'<button class="{("source-link" if label else "cite")}" data-ids="{h(json.dumps(ids))}" data-quotes="{h(json.dumps(quotes(text)))}" data-panel="{h(key)}"'+(f' data-range="{h(range_name)}"' if range_name else '')+f' aria-expanded="false" aria-label="{h(label or "Read supporting sources")}">{h(label or ("↗" if key.startswith("event-") else key))}</button>'
142
+ def selected(row):
143
+ sources=[(cid,records[cid]) for cid in row['source_record_ids']]
144
+ dated=[(cid,e) for cid,e in sources if e['text_basis']=='dated_google_activity_title']
145
+ if dated:
146
+ titles={re.sub(r'^Visited\s+','',e['text']) for cid,e in dated}
147
+ if len(titles)>1:return None
148
+ cid,e=next(((cid,e) for cid,e in dated if not e['text'].startswith('Visited ')),dated[0])
149
+ else:cid,e=sources[0]
150
+ url=e['url'] or row.get('url') or ''
151
+ is_instagram=e['platform']=='Instagram'
152
+ if e['source']=='browser' and not dated:
153
+ route=urlsplit(url).path
154
+ if not re.search(r'/status/\d+|/(?:p|reel)/[^/]+',route):return None
155
+ text=e.get('post_text') or (e['text'] if is_instagram and not e['text'].startswith('http') else '')
156
+ text=html.unescape(text)
157
+ if not text.strip() or text.strip().lower() in ['instagram','instagram photos and videos','home / x','x','twitter']:return None
158
+ return {'id':row['entry_id'],'cid':cid,'text':text,'author':e.get('post_author') or '','url':url,'partial':e.get('post_text_partial',False),'time':row['time'],'source_ids':row['source_record_ids'],'platform':e['platform']}
159
+ def post_row(row):
160
+ sel=selected(row);e=records[row['source_record_ids'][0]];url=(sel or {}).get('url') or row.get('url') or e['url'] or ''
161
+ ids=row['source_record_ids'];key=row['entry_id']+'-full'
162
+ title=(sel or {}).get('text') or row.get('url') or row.get('title') or e['event_kind']
163
+ who=(sel or {}).get('author','');body=h(title)
164
+ link=safe_url(url)
165
+ if sel:body='“'+h(title)+'”'
166
+ if link:body=f'<a class="original" href="{h(link)}" target="_blank" rel="noopener noreferrer">'+body+'</a>'
167
+ if sel and sel['partial']:body+='<span class="partial" title="The saved post text is incomplete." aria-label="The saved post text is incomplete."> …</span>'
168
+ if who:body='<strong>'+h(who)+'</strong> '+body
169
+ time=clock(row['time']) if row['time'] else 'Untimed'
170
+ return f'<li class="evidence-host" data-entry="{h(row["entry_id"])}"><div class="post"><time datetime="{h(row["time"])}">{h(time)}</time><div class="post-text">'+body+' '+cite(ids,key,'“'+title+'”',label=None)+'</div></div></li>'
171
+ for g in groups:
172
+ g['full'].sort(key=lambda r:(stamp(r['time']),records[r['source_record_ids'][0]]['source_order'],r['entry_id']))
173
+ def session_html(g):
174
+ label=g['platform']+' '+clock(g['start'])
175
+ if g['start']!=g['end']:label+='–'+clock(g['end'])
176
+ label+=f" · {len(g['full'])} records"
177
+ return f'<section class="session" id="{h(g["id"])}"><h3>{h(label)}</h3><div class="session-preview"><ol class="records">'+''.join(post_row(r) for r in g['social'] if selected(r))+'</ol></div><details class="full-session"><summary>Show all</summary><ol class="records">'+''.join(post_row(r) for r in g['full'])+'</ol></details></section>'
178
+ put(dest/'sessions.html',page('X and Instagram sessions','<h1>X and Instagram sessions</h1>'+(''.join(session_html(g) for g in groups) or '<p>No saved social sessions for this day.</p>')))
179
+ if browser_rows: put(dest/'browser.html',page('All browser visits','<h1>All browser visits</h1><ol class="records" style="max-height:none">'+''.join(post_row(r) for r in browser_rows)+'</ol>'))
180
+ library='<h1>Full documents</h1><ul class="simple-list">'+''.join('<li><a href="'+h(document_page(d))+'">'+h(d['title'])+'</a></li>' for d in documents)+'</ul>'
181
+ if contexts:library+='<h2>Earlier conversations</h2><ul>'+''.join('<li><a href="context-'+c['conversation_id']+'.html">'+h(c['title'])+'</a></li>' for c in contexts)+'</ul>'
182
+ put(dest/'documents.html',page('Full documents',library))
183
+ story=''
184
+ sessions_by_id={g['id']:g for g in groups}
185
+ for n,block in enumerate(proposal['blocks'],1):
186
+ if block.get('session'):
187
+ session=sessions_by_id.get(block['session'])
188
+ if session is None:
189
+ warning='Session link '+block['session']+' was not found. Linking to all saved social sessions.'
190
+ print(warning,file=sys.stderr)
191
+ progress(warning)
192
+ story+='<p>This session link could not be found. <a href="sessions.html">View all saved social sessions</a>.</p>'
193
+ else:
194
+ story+=session_html(session)
195
+ continue
196
+ body=block['html']
197
+ if block['type']=='paragraph':body=body.replace('<p>', '<p class="story-p">', 1)
198
+ for i,citation in enumerate(proposal['citations']):
199
+ label=citation['label']
200
+ button=cite(citation['citations'],f'{n}-{i}',text=block['text'],label='Source',range_name=citation.get('source_range'))
201
+ button=button.replace('>Source</button>', '>'+label+'</button>')
202
+ body=body.replace(f'<!--briefing-citation-{i}-->',button)
203
+ for i,asset in enumerate(proposal['photos']):
204
+ body=body.replace(f'<!--briefing-photo-{i}-->',photo(asset['id'],asset['caption']))
205
+ story+=f'<div class="evidence-host" id="p{n}">'+body+'</div>'
206
+ story+='<footer><a href="documents.html">Full documents</a></footer>'
207
+ put(dest/'index.html',page(title,story))
208
+ put(dest/'display-groups.json',json.dumps([{'id':g['id'],'parent':g['parent'],'platform':g['platform'],'start':g['start'],'end':g['end'],'full':[r['entry_id'] for r in g['full']]} for g in groups],indent=2))
209
+ print(json.dumps({'output':str(out),'blocks':len(proposal['blocks']),'display_groups':len(groups),'source_records':len(records)}))
210
+
211
+
212
+ if __name__=='__main__':
213
+ parser=argparse.ArgumentParser(description=__doc__)
214
+ for name in ['prepared','briefing','out','marked']:parser.add_argument('--'+name,type=Path,required=True)
215
+ args=parser.parse_args();build(args.prepared,args.briefing,args.out,args.marked)
@@ -0,0 +1,19 @@
1
+ {
2
+ "sample_output": {
3
+ "date": "2026-10-09",
4
+ "input": "sample/ (28 messages, fictional)",
5
+ "draft": "sample-report.md",
6
+ "made_by": "Written for this example from the sample records. The example's own scripts then ran on it: calculate_activity_times (briefing_times.py) made the time files, the write_briefing check (briefing_validation.py) passed, and render_and_save (briefing_render.py) built the website in sample-output.zip.",
7
+ "source_check": {
8
+ "status": "pass",
9
+ "reason": "Source links and supporting files are valid.",
10
+ "evidence": [
11
+ "29 citations checked.",
12
+ "2 supporting files checked."
13
+ ]
14
+ },
15
+ "report_sha256": "b37e90f1515c7a052f7b1cdb9188c7f0e070297194f2f1d3b00e37a662fef068",
16
+ "archive_sha256": "288f4428a1f13ce08a762f5e6e9e52ca50d1e5f528826aa12d9328a74b316d48"
17
+ },
18
+ "approved_report_sha256": "588d744b83c0ea53322c8c6d33b1eba50bfdc817b1ae5459118d9d409dce2e3c"
19
+ }
@@ -0,0 +1,158 @@
1
+ format: method/3.4
2
+ tools:
3
+ calculate_activity_times:
4
+ description: Calculate activity times and save time-plan.json and time-estimates.json in this run. Each successful call replaces both files, so supply the complete plan each time. Only the activities in the plan are calculated; choose the records for each. Before you count a message as work, check whether it is an automated prompt. Record in the plan why each record was selected or excluded. Gaps between records and media playback do not show continuous attention. Leave an activity unestimated when the records do not support an estimate.
5
+ in:
6
+ day:
7
+ type: text
8
+ description: Selected date in YYYY-MM-DD form.
9
+ timezone:
10
+ type: text
11
+ description: Selected named timezone.
12
+ plan:
13
+ type: text
14
+ description: "JSON with activities and not_estimated (short reasons for activities without an estimate). Each activity has id, label, reason, mode, and optional excluded: [{record_ids, reason}]. A field reference is {record_id, field}, where field is a dotted path in the prepared record, such as time.local, time.end, or attributes.msPlayed. Use points with points: [field references], gap_minutes, and optional compare_gaps. This counts first-to-last time in each group; a single point adds zero. Use intervals with intervals: [{start: field reference, end: field reference}]. Record connections between different endpoint records in the activity reason. Intervals are clipped to the day and overlaps count once. Use durations with durations: [field references] and unit: milliseconds, seconds, or minutes. Each selected duration counts once. Supply saved fields, not invented times or calculated totals."
15
+ out:
16
+ estimates:
17
+ type: text
18
+ description: Calculated durations and the records and assumptions used to calculate them.
19
+ files:
20
+ type: list
21
+ description: Supporting file references to return with the draft.
22
+ items:
23
+ type: file
24
+ run:
25
+ kind: run
26
+ runtime: python
27
+ entrypoint: briefing_times.py
28
+ effects: []
29
+ name: Write a daily briefing
30
+ goal: Turn one prepared day folder into a complete, cited daily briefing. Follow the approved example for writing and layout, and save the briefing with its website.
31
+ inputs:
32
+ day:
33
+ type: text
34
+ description: Date of the prepared records, in YYYY-MM-DD form.
35
+ timezone:
36
+ type: text
37
+ description: Timezone for those records, such as America/Chicago.
38
+ environment:
39
+ prepared_day:
40
+ type: files
41
+ description: Folder of prepared records for that day. Read-only.
42
+ steps:
43
+ open_inputs:
44
+ name: Check the input folders
45
+ in:
46
+ day: inputs.day
47
+ timezone: inputs.timezone
48
+ prepared_day: environment.prepared_day
49
+ do:
50
+ kind: run
51
+ runtime: python
52
+ entrypoint: briefing_check_inputs.py
53
+ out:
54
+ selected_day:
55
+ type: record
56
+ description: Prepared records, date, timezone, and source hash.
57
+ fields:
58
+ folder:
59
+ type: text
60
+ description: Absolute path to the prepared records.
61
+ day:
62
+ type: text
63
+ description: Selected date in YYYY-MM-DD form.
64
+ timezone:
65
+ type: text
66
+ description: Named timezone used to interpret this day.
67
+ start:
68
+ type: text
69
+ description: README path relative to the prepared folder.
70
+ source_digest:
71
+ type: text
72
+ description: Hash of the prepared files at the start of this run.
73
+ example:
74
+ type: record
75
+ description: The approved example and its complete text.
76
+ fields:
77
+ report:
78
+ type: text
79
+ description: Complete approved report.
80
+ session_links:
81
+ type: text
82
+ description: Links to saved social sessions for this day. A link on its own line displays that session in the story.
83
+ purpose: Checks the required record files, date, and timezone. Returns the prepared folder and its source hash, the complete approved report, and saved session links.
84
+ write_briefing:
85
+ name: Write the briefing
86
+ in:
87
+ selected_day: selected_day
88
+ example: example
89
+ session_links: session_links
90
+ do:
91
+ kind: agent
92
+ model: default
93
+ prompt: |
94
+ Write the story of {{selected_day.day}} from the records in {{selected_day.folder}}.
95
+ Follow the complete approved report in example.report for writing and detail.
96
+ Return Markdown with source links: [label](source:record-id) or [label](source:record-id#L10-L20).
97
+ Use calculate_activity_times for supported time estimates.
98
+ tools:
99
+ - calculate_activity_times
100
+ out:
101
+ draft:
102
+ type: text
103
+ description: The complete Markdown briefing.
104
+ supporting_files:
105
+ type: list
106
+ items:
107
+ type: file
108
+ description: File references returned by the calculator, or an empty list when none were made.
109
+ check:
110
+ kind: run
111
+ runtime: python
112
+ entrypoint: briefing_validation.py
113
+ reading:
114
+ check_name: Check source links
115
+ check: Checks source IDs, citation line ranges, and supporting file hashes, names, dates, and timezones. Returns pass when those references are valid.
116
+ render_and_save:
117
+ name: Render and save the briefing
118
+ in:
119
+ selected_day: selected_day
120
+ draft: draft
121
+ supporting_files: supporting_files
122
+ do:
123
+ kind: run
124
+ runtime: python
125
+ entrypoint: briefing_render.py
126
+ out:
127
+ saved_briefing:
128
+ type: file
129
+ description: Saved briefing receipt with its date, timezone, and file paths.
130
+ published_website:
131
+ type: file
132
+ description: Briefing website with source pages and supporting files.
133
+ format: method-website
134
+ purpose: Builds the briefing website and checks its local links and source hash. Saves the website and supporting files under their content hash, reuses an identical saved output, and returns the receipt and website manifest.
135
+ result:
136
+ briefing: saved_briefing
137
+ website: published_website
138
+ run_prompt: Run Write a daily briefing for the requested prepared day folder. Read the date and timezone from its records. When finished, open the briefing website and give me the run link.
139
+ files:
140
+ - briefing_files.py
141
+ - briefing_times.py
142
+ - briefing_sessions.py
143
+ - briefing_check_inputs.py
144
+ - briefing_artifacts.py
145
+ - briefing_manifest.py
146
+ - briefing_progress.py
147
+ - briefing_validation.py
148
+ - briefing_render.py
149
+ - briefing_website.py
150
+ - briefing_markdown.py
151
+ - reader/reader.css
152
+ - reader/reader.js
153
+ - package.json
154
+ - package-lock.json
155
+ - pyproject.toml
156
+ - uv.lock
157
+ - vendor/marked.mjs
158
+ - approved-report.md
@@ -0,0 +1 @@
1
+ {"day":"2026-05-11","timezone":"America/Chicago"}
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "method-daily-briefing-example",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "method-daily-briefing-example"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "method-daily-briefing-example",
3
+ "private": true
4
+ }
@@ -0,0 +1,8 @@
1
+ [project]
2
+ name = "method-daily-briefing"
3
+ version = "1.0.0"
4
+ requires-python = ">=3.11,<3.14"
5
+ dependencies = ["Pillow==12.0.0", "pillow-heif==1.1.1", "tzdata==2025.2"]
6
+
7
+ [tool.uv]
8
+ package = false
@@ -0,0 +1,35 @@
1
+ :root{--paper:#faf8f3;--ink:#232b28;--muted:#626e66;--accent:#23614b;--line:#d8ded6}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:17px/1.7 Georgia,serif}a,button{color:var(--accent)}a{text-underline-offset:3px}button,summary{font:12px/1.5 'Helvetica Neue',sans-serif;cursor:pointer}button{border:0;background:none;padding:0}a:focus-visible,button:focus-visible,summary:focus-visible{outline:2px solid #bd7336;outline-offset:3px}nav{max-width:786px;margin:auto;display:flex;justify-content:space-between;gap:16px;padding:18px 28px;font:12px/1.5 'Helvetica Neue',sans-serif}nav a{text-decoration:none}main{max-width:786px;margin:40px auto;padding:0 28px}.mast{margin-bottom:30px}.eyebrow{text-transform:uppercase;letter-spacing:.13em;font:11px/1.5 'Helvetica Neue',sans-serif;color:var(--accent)}h1{font:normal clamp(38px,6vw,60px)/1.08 Georgia,serif;letter-spacing:-.04em;margin:15px 0 24px}h2{font:normal 27px/1.3 Georgia,serif;margin:40px 0 18px;border-top:1px solid var(--line);padding-top:18px}h3{font:600 13px/1.5 'Helvetica Neue',sans-serif;margin:0 0 7px}.story-p,.tldr{margin:0 0 22px}.tldr{font-size:18px}.cite{font:11px 'Helvetica Neue',sans-serif;vertical-align:super;position:relative;white-space:nowrap;margin-left:3px}.cite::before{content:'';position:absolute;inset:-9px -6px}.passage-button{display:inline-block;margin:0 0 18px}.evidence{font-size:15px;line-height:1.55;margin:4px 0 24px}.evidence[hidden]{display:none}.evidence .close{display:block;margin:0 0 14px}.record{margin:0 0 22px;scroll-margin-top:20px}.record-head{font:12px/1.5 'Helvetica Neue',sans-serif;color:var(--muted);margin-bottom:8px}.record-head a{font-size:11px;margin-left:10px}.source-text p{margin:0 0 12px}.source-text ul,.source-text ol{padding-left:22px}.source-text pre{white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.5 ui-monospace,monospace}.source-text code{font-size:.85em}.source-text a{overflow-wrap:anywhere}.source-text img{max-width:100%}mark{background:#f0e2ac;color:inherit;padding:0}.session{margin:22px 0 26px;scroll-margin-top:15px}.session h3 span{font-weight:400;color:var(--muted);margin-left:7px}.session-link{text-decoration:none}.post{display:grid;grid-template-columns:68px minmax(0,1fr);gap:10px;margin:0 0 3px;padding:0}.post time{font:11px/1.65 'Helvetica Neue',sans-serif;color:var(--muted);padding-top:2px;font-variant-numeric:tabular-nums}.post .post-text{font:15px/1.45 Georgia,serif;margin:0;overflow-wrap:anywhere}.post strong{font:600 12px/1.45 'Helvetica Neue',sans-serif}.post .cite{vertical-align:baseline}.post details{display:inline}.post summary{display:inline;margin-left:5px}.full-session{margin-top:7px}.full-session>summary{color:var(--accent);width:fit-content}.records{list-style:none;margin:10px 0;padding:0;max-height:65vh;overflow:auto;overscroll-behavior:contain}.records li{padding:0;border:0}.original{color:inherit;text-decoration:none}.original:hover{text-decoration:underline}.navigation-only{font:13px/1.5 'Helvetica Neue',sans-serif;color:var(--muted)}.partial{font:11px/1.5 'Helvetica Neue',sans-serif;color:var(--muted)}.photo{margin:12px 0 25px}.photo button{display:block}.photo img{display:block;width:190px;max-width:100%;height:auto}.photo figcaption{font:11px/1.5 'Helvetica Neue',sans-serif;color:var(--muted);margin-top:6px}dialog{border:0;background:var(--paper);padding:18px;max-width:95vw;max-height:95vh}dialog::backdrop{background:#000a}dialog img{display:block;max-width:86vw;max-height:82vh;width:auto;height:auto}dialog button{margin:0 0 8px}.simple-list{padding:0;list-style:none}.simple-list li{margin:0 0 10px}.small{font:12px/1.5 'Helvetica Neue',sans-serif;color:var(--muted)}footer{margin:40px 0 20px;font:12px/1.5 'Helvetica Neue',sans-serif}code{font-size:.85em}html{scroll-behavior:auto}@media(max-width:600px){main{margin:26px auto;padding:0 18px}nav{padding:15px 18px}.post{grid-template-columns:60px minmax(0,1fr);gap:7px}h1{font-size:42px}}
2
+
3
+ .track-list{margin-bottom:25px}.track-list>summary{color:var(--accent);width:fit-content}.track-list .simple-list{font-size:15px;line-height:1.5}.track-list .simple-list>li{margin:0 0 3px}
4
+
5
+ .evidence{margin:0 0 18px;font-size:15px;line-height:1.45}
6
+ .evidence-scroll{max-height:65vh;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}
7
+ .evidence-scroll:focus-visible{outline:2px solid #bd7336;outline-offset:2px}
8
+ .evidence .record{display:grid;grid-template-columns:68px minmax(0,1fr);gap:10px;margin:0 0 9px;scroll-margin:0}
9
+ .evidence .record>time{font:11px/1.65 'Helvetica Neue',sans-serif;color:var(--muted);padding-top:2px}
10
+ .evidence .record.untimed{display:block}
11
+ .evidence .record.untimed>time{display:none}
12
+ .evidence .record-head{display:flex;align-items:baseline;gap:7px;margin:0 0 3px;font:12px/1.45 'Helvetica Neue',sans-serif}
13
+ .evidence .record-head a{margin:0;text-decoration:none}
14
+ .evidence .source-text p{margin:0 0 6px}
15
+ .evidence .source-text ul,.evidence .source-text ol{margin:0 0 6px;padding-left:20px}
16
+ .evidence .source-text h1,.evidence .source-text h2,.evidence .source-text h3{font:bold 15px/1.45 Georgia,serif;letter-spacing:0;margin:8px 0 4px;padding:0;border:0}
17
+ .evidence .source-text pre{margin:5px 0}
18
+ .evidence .source-text>:last-child{margin-bottom:0}
19
+ .evidence-host:has(>.evidence:not([hidden]))>.story-p{margin-bottom:8px}
20
+ .evidence-host:has(>.evidence:not([hidden]))>.passage-button{margin-bottom:5px}
21
+ @media(max-width:600px){.evidence .record{grid-template-columns:60px minmax(0,1fr);gap:7px}}
22
+
23
+ .evidence .record-head{float:left;margin-right:6px}.evidence .source-text>p:first-child{display:inline}.evidence .record.untimed .record-head{float:none;margin-right:0}
24
+
25
+ .source-text table{border-collapse:collapse;margin:6px 0 10px;text-align:left}
26
+ .source-text th,.source-text td{padding:0 12px 2px 0;vertical-align:top}
27
+ .source-text th:last-child,.source-text td:last-child{padding-right:0}
28
+
29
+ .source-link{font:inherit;text-decoration:underline;text-underline-offset:3px;display:inline;margin:0;padding:0}.evidence-host{overflow-wrap:anywhere}.evidence-host>pre{white-space:pre-wrap}.evidence-host>table{display:block;overflow-x:auto;border-collapse:collapse}.evidence-host>table th,.evidence-host>table td{padding:4px 12px;text-align:left}
30
+
31
+ .session-preview{max-height:15rem;overflow:hidden;mask-image:linear-gradient(#000 75%,transparent)}
32
+ .session-preview .records{max-height:none;overflow:visible;margin-bottom:0}
33
+ .session:has(>.full-session[open])>.session-preview{display:none}
34
+ .evidence-host>table{overflow-wrap:normal;word-break:normal;max-width:100%}
35
+ .evidence-host>table th,.evidence-host>table td{min-width:9rem}
@@ -0,0 +1,42 @@
1
+ const cache=new Map();
2
+ async function record(id){if(!cache.has(id))cache.set(id,fetch(`data/${id}.json`).then(r=>{if(!r.ok)throw Error('Source could not be opened');return r.json()}));return cache.get(id)}
3
+ function highlight(root,quotes){for(const quote of quotes){if(!quote.trim())continue;const walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT);const nodes=[];let joined='',node;while(node=walker.nextNode()){if(node.parentElement.closest('mark'))continue;nodes.push({node,start:joined.length});joined+=node.textContent}const normalized=[];let text='';for(let i=0;i<joined.length;i++){if(/\s/.test(joined[i])){if(text.endsWith(' '))continue;text+=' '}else{text+=joined[i]}normalized.push(i)}const q=quote.replace(/\s+/g,' ');const at=text.indexOf(q);if(at<0)continue;const start=normalized[at],end=normalized[at+q.length-1]+1;for(const item of nodes.reverse()){let a=Math.max(0,start-item.start),b=Math.min(item.node.length,end-item.start);if(b<=a)continue;const range=document.createRange();range.setStart(item.node,a);range.setEnd(item.node,b);const mark=document.createElement('mark');range.surroundContents(mark)}}}
4
+ function sourceElement(data,range){
5
+ const article=document.createElement('article');article.className='record';article.id=data.id;
6
+ const writing=/^(roam|apple_notes)-/.test(data.id);
7
+ const time=document.createElement('time');time.textContent=(data.time||'').split(' · ').at(-1);
8
+ if(!/AM|PM/.test(time.textContent))time.textContent='';
9
+ if(!time.textContent)article.classList.add('untimed');
10
+ const body=document.createElement('div');body.className='record-body';
11
+ const head=document.createElement('div');head.className='record-head';
12
+ if(!writing&&data.speaker){const speaker=document.createElement('strong');speaker.textContent=data.speaker==='User'?'You':data.speaker==='Assistant'?(data.id.startsWith('codex-')?'Codex':'ChatGPT'):data.speaker;head.append(speaker)}
13
+ const link=document.createElement('a');link.href=data.document;link.target='_blank';link.rel='noopener';link.textContent=writing?'Full note ↗':'↗';link.title=[data.reading_label,data.basis].filter(Boolean).join(' · ');link.setAttribute('aria-label',data.reading_label);head.append(link);
14
+ const text=document.createElement('div');text.className='source-text';text.innerHTML=range?data.ranges[range]:data.html;
15
+ body.append(head,text);article.append(time,body);return article;
16
+ }
17
+ async function openEvidence(button){
18
+ const host=button.closest('.evidence-host');let panel=host.querySelector(':scope > .evidence');
19
+ if(!panel){panel=document.createElement('div');panel.className='evidence';host.append(panel)}
20
+ const isOpen=!panel.hidden&&panel.dataset.owner===button.dataset.panel;
21
+ host.querySelectorAll('[data-ids]').forEach(b=>{b.setAttribute('aria-expanded','false');if(b.dataset.readLabel)b.textContent=b.dataset.readLabel});
22
+ panel.dataset.owner=button.dataset.panel;button.setAttribute('aria-expanded',String(!isOpen));
23
+ if(isOpen){panel.hidden=true;return}
24
+ panel.hidden=false;const request=Symbol();panel.request=request;panel.replaceChildren();
25
+ const scroll=document.createElement('div');scroll.className='evidence-scroll';scroll.tabIndex=0;scroll.setAttribute('role','region');scroll.setAttribute('aria-label','Saved sources');
26
+ panel.append(scroll);
27
+ try{
28
+ const ids=JSON.parse(button.dataset.ids),quotes=JSON.parse(button.dataset.quotes||'[]');
29
+ for(const id of ids){
30
+ const data=await record(id);if(panel.request!==request||panel.hidden)return;
31
+ const el=sourceElement(data,button.dataset.range);scroll.append(el);highlight(el.querySelector('.source-text'),quotes);
32
+ const mark=el.querySelector('mark');
33
+ if(mark){const a=el.querySelector('.record-head a'),url=new URL(a.href);url.searchParams.set('quote',quotes.find(q=>el.querySelector('.source-text').textContent.includes(q))||mark.textContent);a.href=url}
34
+ }
35
+ const first=scroll.querySelector('mark');
36
+ if(first)scroll.scrollTop=Math.max(0,first.getBoundingClientRect().top-scroll.getBoundingClientRect().top-30);
37
+ }catch(error){if(panel.request===request&&!panel.hidden)scroll.append(document.createTextNode(error.message))}
38
+ }
39
+
40
+ document.addEventListener('click',e=>{const button=e.target.closest('[data-ids]');if(button)openEvidence(button);const photo=e.target.closest('[data-photo]');if(photo){const dlg=document.querySelector('dialog');dlg.querySelector('img').src=photo.dataset.photo;dlg.querySelector('img').alt=photo.querySelector('img').alt;dlg.showModal()}});
41
+ document.querySelector('dialog')?.addEventListener('click',e=>{if(e.target.tagName==='DIALOG'||e.target.closest('[data-close-photo]'))e.currentTarget.close()});
42
+ function revealHash(){const id=decodeURIComponent(location.hash.slice(1));if(!id)return;const el=document.getElementById(id);if(el){if(el.classList.contains('session')){const details=el.querySelector('.full-session');if(details)details.open=true}const q=new URLSearchParams(location.search).get('quote');if(q)highlight(el,[q]);(el.querySelector('mark')||el).scrollIntoView({block:'start'})}}window.addEventListener('hashchange',revealHash);revealHash();