skilldrop-cli 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +3 -0
  2. package/catalogue.json +25 -2
  3. package/model-routing.json +20 -0
  4. package/package.json +1 -1
  5. package/packs/event-planner/pack.json +61 -0
  6. package/packs/event-planner/skills/run-of-show/SKILL.md +144 -0
  7. package/packs/event-planner/skills/run-of-show/evals/eval_queries.json +30 -0
  8. package/packs/event-planner/skills/run-of-show/evals/evals.json +103 -0
  9. package/packs/event-planner/skills/run-of-show/evals/files/1/agenda.md +16 -0
  10. package/packs/event-planner/skills/run-of-show/evals/files/2/schedule.csv +8 -0
  11. package/packs/event-planner/skills/run-of-show/evals/files/3/notes.txt +3 -0
  12. package/packs/event-planner/skills/run-of-show/evals/files/4/sessions_export.csv +25 -0
  13. package/packs/event-planner/skills/run-of-show/evals/files/4/venue_and_crew.txt +27 -0
  14. package/packs/event-planner/skills/run-of-show/evals/files/5/runsheet_v4.csv +28 -0
  15. package/packs/event-planner/skills/run-of-show/evals/files/5/venue_and_suppliers.txt +22 -0
  16. package/packs/event-planner/skills/run-of-show/evals/files/6/acts_from_booker.csv +26 -0
  17. package/packs/event-planner/skills/run-of-show/evals/files/6/venue_rooms_and_crew.txt +14 -0
  18. package/packs/event-planner/skills/run-of-show/evals/files/7/runsheet_draft_v4.csv +25 -0
  19. package/packs/event-planner/skills/run-of-show/evals/files/7/venue_and_fixed_times.txt +24 -0
  20. package/packs/event-planner/skills/run-of-show/examples/product-launch.md +77 -0
  21. package/packs/event-planner/skills/run-of-show/examples/product-launch.svg +58 -0
  22. package/packs/event-planner/skills/run-of-show/manifest.json +44 -0
  23. package/packs/event-planner/skills/run-of-show/scripts/show_check.py +295 -0
  24. package/packs/event-planner/skills/run-of-show/templates/show.json +15 -0
  25. package/packs/event-planner/skills/seating-plan/SKILL.md +155 -0
  26. package/packs/event-planner/skills/seating-plan/evals/eval_queries.json +30 -0
  27. package/packs/event-planner/skills/seating-plan/evals/evals.json +164 -0
  28. package/packs/event-planner/skills/seating-plan/evals/files/1/guests.csv +31 -0
  29. package/packs/event-planner/skills/seating-plan/evals/files/10/hall_notes.txt +13 -0
  30. package/packs/event-planner/skills/seating-plan/evals/files/10/quiz_night_registrations.csv +67 -0
  31. package/packs/event-planner/skills/seating-plan/evals/files/11/notes_for_the_plan.txt +21 -0
  32. package/packs/event-planner/skills/seating-plan/evals/files/11/rsvp_list.csv +77 -0
  33. package/packs/event-planner/skills/seating-plan/evals/files/11/table_plan_draft.csv +78 -0
  34. package/packs/event-planner/skills/seating-plan/evals/files/2/table-plan.txt +8 -0
  35. package/packs/event-planner/skills/seating-plan/evals/files/3/brief.txt +4 -0
  36. package/packs/event-planner/skills/seating-plan/evals/files/3/guests.csv +49 -0
  37. package/packs/event-planner/skills/seating-plan/evals/files/4/rsvp_export.csv +127 -0
  38. package/packs/event-planner/skills/seating-plan/evals/files/4/seating_notes.txt +21 -0
  39. package/packs/event-planner/skills/seating-plan/evals/files/5/organiser_notes.txt +10 -0
  40. package/packs/event-planner/skills/seating-plan/evals/files/5/registrations.csv +93 -0
  41. package/packs/event-planner/skills/seating-plan/evals/files/5/table_plan_v3.csv +94 -0
  42. package/packs/event-planner/skills/seating-plan/evals/files/6/bridgets_notes.txt +30 -0
  43. package/packs/event-planner/skills/seating-plan/evals/files/6/ticket_export.csv +117 -0
  44. package/packs/event-planner/skills/seating-plan/evals/files/6/venue_floorplan_notes.txt +14 -0
  45. package/packs/event-planner/skills/seating-plan/evals/files/7/keep_apart_CONFIDENTIAL.txt +9 -0
  46. package/packs/event-planner/skills/seating-plan/evals/files/7/prom_seating_v3.csv +101 -0
  47. package/packs/event-planner/skills/seating-plan/evals/files/7/ticket_list.csv +101 -0
  48. package/packs/event-planner/skills/seating-plan/evals/files/7/venue_info.txt +16 -0
  49. package/packs/event-planner/skills/seating-plan/evals/files/8/registrations_export.csv +147 -0
  50. package/packs/event-planner/skills/seating-plan/evals/files/8/room_layout.md +22 -0
  51. package/packs/event-planner/skills/seating-plan/evals/files/8/seating_requests.txt +31 -0
  52. package/packs/event-planner/skills/seating-plan/evals/files/9/guest_list.csv +84 -0
  53. package/packs/event-planner/skills/seating-plan/evals/files/9/notes.txt +16 -0
  54. package/packs/event-planner/skills/seating-plan/evals/files/9/table_plan.csv +85 -0
  55. package/packs/event-planner/skills/seating-plan/examples/wedding-dinner.md +61 -0
  56. package/packs/event-planner/skills/seating-plan/examples/wedding-dinner.svg +39 -0
  57. package/packs/event-planner/skills/seating-plan/manifest.json +44 -0
  58. package/packs/event-planner/skills/seating-plan/scripts/seating_check.py +324 -0
  59. package/packs/event-planner/skills/seating-plan/templates/seating.json +28 -0
  60. package/packs/experience-design/pack.json +5 -0
  61. package/packs/experience-design/skills/locale-check/SKILL.md +139 -0
  62. package/packs/experience-design/skills/locale-check/evals/eval_queries.json +38 -0
  63. package/packs/experience-design/skills/locale-check/evals/evals.json +104 -0
  64. package/packs/experience-design/skills/locale-check/evals/files/1/budget.json +1 -0
  65. package/packs/experience-design/skills/locale-check/evals/files/1/locales/ar.json +16 -0
  66. package/packs/experience-design/skills/locale-check/evals/files/1/locales/de.json +12 -0
  67. package/packs/experience-design/skills/locale-check/evals/files/1/locales/en.json +12 -0
  68. package/packs/experience-design/skills/locale-check/evals/files/1/locales/pl.json +12 -0
  69. package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values/strings.xml +12 -0
  70. package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values-es/strings.xml +11 -0
  71. package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values-fr/strings.xml +12 -0
  72. package/packs/experience-design/skills/locale-check/evals/files/3/po/ja.po +20 -0
  73. package/packs/experience-design/skills/locale-check/evals/files/3/po/messages.pot +23 -0
  74. package/packs/experience-design/skills/locale-check/evals/files/3/po/ru.po +22 -0
  75. package/packs/experience-design/skills/locale-check/evals/files/4/budgets.json +15 -0
  76. package/packs/experience-design/skills/locale-check/evals/files/4/locales/ar.json +265 -0
  77. package/packs/experience-design/skills/locale-check/evals/files/4/locales/de.json +265 -0
  78. package/packs/experience-design/skills/locale-check/evals/files/4/locales/en.json +265 -0
  79. package/packs/experience-design/skills/locale-check/evals/files/4/locales/fr.json +265 -0
  80. package/packs/experience-design/skills/locale-check/evals/files/4/locales/pl.json +265 -0
  81. package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values/strings.xml +77 -0
  82. package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values-es/strings.xml +63 -0
  83. package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values-ja/strings.xml +61 -0
  84. package/packs/experience-design/skills/locale-check/evals/files/6/po/cs.po +605 -0
  85. package/packs/experience-design/skills/locale-check/evals/files/6/po/marlin.pot +602 -0
  86. package/packs/experience-design/skills/locale-check/evals/files/6/po/pt_BR.po +602 -0
  87. package/packs/experience-design/skills/locale-check/evals/files/6/po/ru.po +608 -0
  88. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/en.lproj/Localizable.strings +160 -0
  89. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/en.lproj/Localizable.stringsdict +70 -0
  90. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/he.lproj/Localizable.strings +157 -0
  91. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/he.lproj/Localizable.stringsdict +86 -0
  92. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/it.lproj/Localizable.strings +160 -0
  93. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/it.lproj/Localizable.stringsdict +68 -0
  94. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/ko.lproj/Localizable.strings +160 -0
  95. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/ko.lproj/Localizable.stringsdict +62 -0
  96. package/packs/experience-design/skills/locale-check/examples/settings-release.md +107 -0
  97. package/packs/experience-design/skills/locale-check/manifest.json +45 -0
  98. package/packs/experience-design/skills/locale-check/scripts/locale_check.py +1105 -0
  99. package/packs/interior-designer/pack.json +60 -0
  100. package/packs/interior-designer/skills/furniture-layout/SKILL.md +182 -0
  101. package/packs/interior-designer/skills/furniture-layout/evals/eval_queries.json +11 -0
  102. package/packs/interior-designer/skills/furniture-layout/evals/evals.json +133 -0
  103. package/packs/interior-designer/skills/furniture-layout/evals/files/2/living-room-notes.md +24 -0
  104. package/packs/interior-designer/skills/furniture-layout/evals/files/3/office-brief.md +18 -0
  105. package/packs/interior-designer/skills/furniture-layout/evals/files/4/furniture_schedule.csv +15 -0
  106. package/packs/interior-designer/skills/furniture-layout/evals/files/4/site_survey.txt +22 -0
  107. package/packs/interior-designer/skills/furniture-layout/evals/files/5/layout_v2.csv +14 -0
  108. package/packs/interior-designer/skills/furniture-layout/evals/files/5/measurements.txt +16 -0
  109. package/packs/interior-designer/skills/furniture-layout/evals/files/6/access_consultant_email.txt +19 -0
  110. package/packs/interior-designer/skills/furniture-layout/evals/files/6/furniture_stock.csv +5 -0
  111. package/packs/interior-designer/skills/furniture-layout/evals/files/6/site_measurements.txt +14 -0
  112. package/packs/interior-designer/skills/furniture-layout/evals/files/7/room_214_layout.csv +11 -0
  113. package/packs/interior-designer/skills/furniture-layout/evals/files/7/room_and_brand_standard.txt +20 -0
  114. package/packs/interior-designer/skills/furniture-layout/evals/files/8/email_from_practice_manager.txt +44 -0
  115. package/packs/interior-designer/skills/furniture-layout/evals/files/8/furniture_we_are_keeping.csv +10 -0
  116. package/packs/interior-designer/skills/furniture-layout/evals/files/9/reading_room_schedule.csv +15 -0
  117. package/packs/interior-designer/skills/furniture-layout/evals/files/9/room_notes.txt +23 -0
  118. package/packs/interior-designer/skills/furniture-layout/examples/king-bed-bedroom.md +121 -0
  119. package/packs/interior-designer/skills/furniture-layout/examples/king-bed-bedroom.svg +57 -0
  120. package/packs/interior-designer/skills/furniture-layout/manifest.json +44 -0
  121. package/packs/interior-designer/skills/furniture-layout/scripts/layout_check.py +1130 -0
  122. package/packs/interior-designer/skills/furniture-layout/templates/layout.json +26 -0
  123. package/packs/operations-manager/pack.json +60 -0
  124. package/packs/operations-manager/skills/shift-roster/SKILL.md +169 -0
  125. package/packs/operations-manager/skills/shift-roster/evals/eval_queries.json +38 -0
  126. package/packs/operations-manager/skills/shift-roster/evals/evals.json +135 -0
  127. package/packs/operations-manager/skills/shift-roster/evals/files/1/shifts.csv +3 -0
  128. package/packs/operations-manager/skills/shift-roster/evals/files/1/staff.csv +8 -0
  129. package/packs/operations-manager/skills/shift-roster/evals/files/2/rota.csv +22 -0
  130. package/packs/operations-manager/skills/shift-roster/evals/files/2/staff.csv +6 -0
  131. package/packs/operations-manager/skills/shift-roster/evals/files/3/team.md +14 -0
  132. package/packs/operations-manager/skills/shift-roster/evals/files/4/availability_export_wk47.csv +23 -0
  133. package/packs/operations-manager/skills/shift-roster/evals/files/4/rota_notes.txt +20 -0
  134. package/packs/operations-manager/skills/shift-roster/evals/files/5/rota_draft_16-29nov.csv +107 -0
  135. package/packs/operations-manager/skills/shift-roster/evals/files/5/staff_list.csv +17 -0
  136. package/packs/operations-manager/skills/shift-roster/evals/files/6/availability_notes.txt +24 -0
  137. package/packs/operations-manager/skills/shift-roster/evals/files/6/peoplehub_staff_export.csv +14 -0
  138. package/packs/operations-manager/skills/shift-roster/evals/files/6/rota_template.csv +25 -0
  139. package/packs/operations-manager/skills/shift-roster/evals/files/7/leave_and_availability.txt +18 -0
  140. package/packs/operations-manager/skills/shift-roster/evals/files/7/rota_wc_30nov.csv +47 -0
  141. package/packs/operations-manager/skills/shift-roster/evals/files/7/staff_list.csv +15 -0
  142. package/packs/operations-manager/skills/shift-roster/evals/files/8/availability_form_responses.csv +22 -0
  143. package/packs/operations-manager/skills/shift-roster/evals/files/8/cover_requirements.csv +15 -0
  144. package/packs/operations-manager/skills/shift-roster/evals/files/8/staff_export.csv +29 -0
  145. package/packs/operations-manager/skills/shift-roster/evals/files/9/leave_and_restrictions.txt +16 -0
  146. package/packs/operations-manager/skills/shift-roster/evals/files/9/rota_export_07-13Dec.csv +85 -0
  147. package/packs/operations-manager/skills/shift-roster/evals/files/9/staff_list.csv +27 -0
  148. package/packs/operations-manager/skills/shift-roster/examples/cafe-week.md +72 -0
  149. package/packs/operations-manager/skills/shift-roster/examples/cafe-week.svg +123 -0
  150. package/packs/operations-manager/skills/shift-roster/manifest.json +34 -0
  151. package/packs/operations-manager/skills/shift-roster/scripts/roster_check.py +633 -0
  152. package/packs/operations-manager/skills/shift-roster/templates/roster.json +22 -0
@@ -0,0 +1,14 @@
1
+ Kettleby Folk & Word Festival - Sat 6 March 2027 - The Kettleby Exchange
2
+ Rooms / hours / changeovers (from the venue tech spec + my emails with them)
3
+
4
+ MAIN HALL - craft fair in there till 13:30, so first act on 14:00 at the earliest. Noise curfew 23:00 (last note by 23:00).
5
+ Changeover between acts: 30 min (band kit, line check). Crew: stage manager Pat Rourke, sound Jules Amadi, lights Sacha Bell
6
+ STUDIO - available 11:30 to 22:00. Changeover 15 min. Crew: stage manager Freya Lund, sound Benedikt Roth
7
+ LIBRARY ROOM - library is open to us 12:00-17:30 only (they lock up at half 5). Changeover 10 min. Crew: room host (volunteer) Anne Pickles
8
+ COURTYARD STAGE - street-use permit 12:00-15:30 only. Changeover 15 min. Crew: stage manager Tom Garbutt, sound Ivo Mertens
9
+
10
+ BSL interpreter: Ruth Penhaligon - one interpreter for the day. She's booked for the three sessions marked BSL
11
+ (the Writing the Land talk, the poetry slam, Kit Harrow).
12
+
13
+ Anyone in two places on the day (performers or Ruth) needs at least 15 minutes between finishing in one room and starting
14
+ in another - the Exchange is a rabbit warren.
@@ -0,0 +1,25 @@
1
+ Start,End,Room,Item,Speakers / talent,Crew needed,Notes
2
+ 08:00,09:30,Hall,Tech rehearsal,Imran Kestrel; Ana Ruiz; Gemma Thorne,"AV team x3, stage manager",
3
+ 09:00,10:00,Terrace,Arrival coffee,,Catering,
4
+ 10:00,10:40,Hall,Welcome & keynote,Imran Kestrel; Gemma Thorne,"AV team x3, stage manager, camera x2",embargo lifts 10:00
5
+ 10:30,10:50,Green Room,Press interview - Tech Weekly,Imran Kestrel,Press officer,
6
+ 10:40,17:30,Atrium,Demo zone (Lumio Fold 2 hands-on),,Demo crew x4,
7
+ 10:50,11:30,Hall,Product demo,Ana Ruiz,"AV team x3, stage manager, camera x2",
8
+ 10:55,11:15,Green Room,Press interview - Retail Insider,Owen Strand,Press officer,
9
+ 11:00,11:45,Studio B,Developer breakout,Tom Vickers; Hana Sato,AV x1,
10
+ 11:30,12:15,Studio B,Retail partners briefing,Owen Strand; Ines Duarte,AV x1,
11
+ 11:50,12:10,Green Room,Press interview - Gadget Desk,Ana Ruiz,Press officer,
12
+ 12:00,12:45,Hall,Customer stories panel,Gemma Thorne; Ifeoma Ndukwe; Lars Pettersen; Carys Bowen,"AV team x3, stage manager",
13
+ 12:15,12:35,Green Room,Press interview - DevWire,Tom Vickers,Press officer,
14
+ 12:45,13:40,Terrace,Lunch,,Catering,caterer can only do 12:45
15
+ 14:00,14:45,Studio B,Accessibility features deep-dive,Hana Sato,AV x1,
16
+ 14:00,15:00,Hall,Lumio Live (livestream),Imran Kestrel; Ana Ruiz; Gemma Thorne,"Pixelhaus broadcast crew, AV team x3, stage manager",FIXED - broadcast partner slot
17
+ 15:00,15:45,Studio B,Enterprise security briefing,Tom Vickers,AV x1,
18
+ 15:15,15:35,Green Room,Press interview - Business Daily,Imran Kestrel,Press officer,
19
+ 15:20,16:20,Hall,Partner panel,Gemma Thorne; Rohan Mehta; Ines Duarte,"AV team x3, stage manager",
20
+ 15:40,16:00,Green Room,Press interview - Mobile Matters,Ana Ruiz,Press officer,
21
+ 16:00,16:45,Studio B,Partner onboarding clinic,Owen Strand,AV x1,
22
+ 16:05,16:25,Green Room,Press interview - Access Today,Hana Sato,Press officer,
23
+ 16:40,17:25,Hall,Engineering Q&A,Tom Vickers; Ana Ruiz,"AV team x3, stage manager",
24
+ 17:45,18:45,Hall,Closing & customer awards,Imran Kestrel; Gemma Thorne,"AV team x3, stage manager, camera x2",6 awards
25
+ 18:00,18:45,Terrace,Drinks reception,,"Catering, bar",
@@ -0,0 +1,24 @@
1
+ Lumio Fold 2 launch - Thu 21 Jan 2027 - The Granary Rooms
2
+ (from the venue contract + our own fixed points)
3
+
4
+ VENUE
5
+ - Crew access from 08:00. Guests arrive 09:00.
6
+ - The HALL must be completely clear by 18:00 - the venue has an evening booking getting in from 18:00. No flex, it's in the contract.
7
+ - Studio B, Green Room and Atrium available until 18:30; Terrace until 19:00 (building closes 19:00).
8
+ - Hall seats 300 (all guests). Studio B seats 80. Green Room is for press only (holds 6).
9
+
10
+ TURNAROUND between sessions
11
+ - Hall: 20 min stage reset between sessions (AV company's minimum)
12
+ - Studio B: 10 min
13
+ - Green Room: 5 min between interviews
14
+ - Atrium/Terrace: n/a (catering + demo zone)
15
+
16
+ PEOPLE
17
+ - Speakers need at least 10 min to get from one room to another (and be mic'd).
18
+
19
+ FIXED - CAN'T MOVE
20
+ - Keynote at 10:00 (press embargo lifts 10:00, it's on the invite)
21
+ - Hall tech rehearsal until 09:30
22
+ - Lumio Live livestream in the Hall 14:00-15:00 (broadcast partner's slot)
23
+ - Lunch on the Terrace 12:45-13:40 (caterer)
24
+ - All Hall sessions are for the whole audience of 300, so they have to stay in the Hall
@@ -0,0 +1,77 @@
1
+ # Worked example: a product launch day
2
+
3
+ Input → output oracle for `run-of-show`. Shows an agenda that reads fine and has four clashes:
4
+ a keynote's stage setup colliding with doors in the same room, two handovers shorter than the
5
+ room needs, and the CEO due in the demo lab the minute her keynote ends.
6
+
7
+ ## Input
8
+
9
+ > Here's the launch day. Auditorium 8:00–18:00, needs 20 minutes to turn over between
10
+ > sessions; the demo lab is 9:00–17:00, 10 minutes. It's a five-minute walk between them, and
11
+ > we've told people we finish at 17:00.
12
+ >
13
+ > - 8:30–9:00 doors and coffee, auditorium
14
+ > - 9:00–9:45 launch keynote, Mara (CEO); AV needs 30 minutes on stage before it
15
+ > - 9:45–10:30 live demo of the setup flow, demo lab, Mara and Theo
16
+ > - 10:00–11:00 customer panel, auditorium, Ines hosting; the stage takes 25 minutes to strike after
17
+ > - 11:15–12:15 lunch, auditorium
18
+ > - 13:00–15:30 hands-on lab, demo lab, Theo
19
+ > - 16:45–17:15 closing remarks, Mara
20
+
21
+ ## The agenda as given, checked
22
+
23
+ ```
24
+ FAIL Auditorium: 0 min between Doors and coffee (08:30) and Launch keynote (09:00), 30 min needed for Launch keynote's setup
25
+ FAIL Auditorium: 15 min between Launch keynote (09:00) and Customer panel (10:00), 20 min needed for the room's turnaround
26
+ FAIL Auditorium: 15 min between Customer panel (10:00) and Lunch (11:15), 25 min needed for Customer panel's changeover
27
+ FAIL Mara Quinn (CEO) has 0 min to get from Auditorium to Demo lab between Launch keynote and Live demo: setup flow; the walk takes 5
28
+ WARN Auditorium is empty for 270 min between Lunch and Closing remarks
29
+ WARN Demo lab is empty for 150 min between Live demo: setup flow and Hands-on lab
30
+ WARN Closing remarks ends at 17:15, 15 min after the published end of 17:00
31
+ 4 failure(s)
32
+ ```
33
+
34
+ Each one has a small fix that moves as little as possible. Doors and coffee go to the foyer,
35
+ so the stage crew has the auditorium from 8:30. The demo starts five minutes later, so Mara
36
+ can walk over. The panel moves 5 minutes and lunch 15, so each handover gets the time the
37
+ room needs. Closing starts 15 minutes earlier, to end on time.
38
+
39
+ ## The reply
40
+
41
+ **No room or person is booked twice, and the day ends at 17:00 as published.** Five times
42
+ moved from your agenda, each by 15 minutes or less.
43
+
44
+ | Time | Room | Item | Who's needed |
45
+ |---|---|---|---|
46
+ | 08:30–09:00 | Foyer | Doors and coffee | Front of house |
47
+ | 09:00–09:45 | Auditorium | Launch keynote | Mara, AV crew (on stage from 08:30) |
48
+ | 09:50–10:35 | Demo lab | Live demo: setup flow | Mara, Theo |
49
+ | 10:05–11:05 | Auditorium | Customer panel | Ines, AV crew |
50
+ | 11:30–12:30 | Auditorium | Lunch | |
51
+ | 13:00–15:30 | Demo lab | Hands-on lab | Theo |
52
+ | 16:30–17:00 | Auditorium | Closing remarks | Mara |
53
+
54
+ **What moved, and why:**
55
+ - **Doors and coffee to the foyer.** The keynote stage needs 30 minutes and the room opened
56
+ for coffee at 8:30.
57
+ - **Live demo to 09:50.** Mara's keynote ends at 09:45, and the lab is a five-minute walk.
58
+ - **Panel to 10:05, lunch to 11:30.** The auditorium needs 20 minutes between sessions, and 25
59
+ after the panel to strike the stage.
60
+ - **Closing remarks to 16:30.** They ended at 17:15, after the time you've published.
61
+
62
+ **Assumption:** `[assumption]` the foyer is free from 08:00. If it isn't, the keynote moves to
63
+ 09:30 instead, and everything after it moves 30 minutes.
64
+
65
+ The auditorium sits empty from 12:30 to 16:30 while the hands-on lab runs. That's fine if
66
+ everyone is in the lab; if not, it's the obvious slot for a second session.
67
+
68
+ The timeline, one lane per room, is in `run-of-show.svg`.
69
+
70
+ ## Why this is the oracle
71
+
72
+ - **Setup, turnaround and changeover are checked as well as overlaps.** A plain read sees no
73
+ two sessions at the same time in one room, and calls the agenda clash-free.
74
+ - **People are checked across rooms, with the walk.** The CEO's back-to-back across two rooms
75
+ is the clash crews find on the day.
76
+ - **Each fix is the smallest move, and the reply lists them.** It doesn't silently re-time the
77
+ day or squeeze the stage strike to fit.
@@ -0,0 +1,58 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 316" width="1280" height="316" font-family="Helvetica, Arial, sans-serif">
2
+ <rect width="1280" height="316" fill="#ffffff"/>
3
+ <text x="20" y="28" font-size="16" font-weight="bold">Run of show</text>
4
+ <line x1="150.0" y1="60" x2="150.0" y2="252" stroke="#cfd8dc"/>
5
+ <text x="150.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">08:00</text>
6
+ <line x1="205.0" y1="60" x2="205.0" y2="252" stroke="#eceff1"/>
7
+ <line x1="260.0" y1="60" x2="260.0" y2="252" stroke="#cfd8dc"/>
8
+ <text x="260.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">09:00</text>
9
+ <line x1="315.0" y1="60" x2="315.0" y2="252" stroke="#eceff1"/>
10
+ <line x1="370.0" y1="60" x2="370.0" y2="252" stroke="#cfd8dc"/>
11
+ <text x="370.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">10:00</text>
12
+ <line x1="425.0" y1="60" x2="425.0" y2="252" stroke="#eceff1"/>
13
+ <line x1="480.0" y1="60" x2="480.0" y2="252" stroke="#cfd8dc"/>
14
+ <text x="480.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">11:00</text>
15
+ <line x1="535.0" y1="60" x2="535.0" y2="252" stroke="#eceff1"/>
16
+ <line x1="590.0" y1="60" x2="590.0" y2="252" stroke="#cfd8dc"/>
17
+ <text x="590.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">12:00</text>
18
+ <line x1="645.0" y1="60" x2="645.0" y2="252" stroke="#eceff1"/>
19
+ <line x1="700.0" y1="60" x2="700.0" y2="252" stroke="#cfd8dc"/>
20
+ <text x="700.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">13:00</text>
21
+ <line x1="755.0" y1="60" x2="755.0" y2="252" stroke="#eceff1"/>
22
+ <line x1="810.0" y1="60" x2="810.0" y2="252" stroke="#cfd8dc"/>
23
+ <text x="810.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">14:00</text>
24
+ <line x1="865.0" y1="60" x2="865.0" y2="252" stroke="#eceff1"/>
25
+ <line x1="920.0" y1="60" x2="920.0" y2="252" stroke="#cfd8dc"/>
26
+ <text x="920.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">15:00</text>
27
+ <line x1="975.0" y1="60" x2="975.0" y2="252" stroke="#eceff1"/>
28
+ <line x1="1030.0" y1="60" x2="1030.0" y2="252" stroke="#cfd8dc"/>
29
+ <text x="1030.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">16:00</text>
30
+ <line x1="1085.0" y1="60" x2="1085.0" y2="252" stroke="#eceff1"/>
31
+ <line x1="1140.0" y1="60" x2="1140.0" y2="252" stroke="#cfd8dc"/>
32
+ <text x="1140.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">17:00</text>
33
+ <line x1="1195.0" y1="60" x2="1195.0" y2="252" stroke="#eceff1"/>
34
+ <line x1="1250.0" y1="60" x2="1250.0" y2="252" stroke="#cfd8dc"/>
35
+ <text x="1250.0" y="57" font-size="10" text-anchor="middle" fill="#607d8b">18:00</text>
36
+ <text x="20" y="96" font-size="12" fill="#263238">Auditorium</text><line x1="150" y1="124" x2="1250.0" y2="124" stroke="#b0bec5"/>
37
+ <rect x="150.0" y="62" width="1100.0" height="60" fill="#f1f8e9" opacity="0.6"/>
38
+ <rect x="205.0" y="68" width="55.0" height="48" fill="#fff8e1" stroke="#ffb300" stroke-dasharray="3,2"/>
39
+ <rect x="260.0" y="66" width="82.5" height="52" rx="4" fill="#e3f2fd" stroke="#1565c0" stroke-width="1"/>
40
+ <text x="264.0" y="84" font-size="10" fill="#0d47a1">Launch keyno…</text><text x="264.0" y="98" font-size="9" fill="#546e7a">09:00–09:45</text>
41
+ <rect x="379.2" y="66" width="110.0" height="52" rx="4" fill="#e3f2fd" stroke="#1565c0" stroke-width="1"/>
42
+ <text x="383.2" y="84" font-size="10" fill="#0d47a1">Customer panel</text><text x="383.2" y="98" font-size="9" fill="#546e7a">10:05–11:05</text>
43
+ <rect x="535.0" y="66" width="110.0" height="52" rx="4" fill="#e3f2fd" stroke="#1565c0" stroke-width="1"/>
44
+ <text x="539.0" y="84" font-size="10" fill="#0d47a1">Lunch</text><text x="539.0" y="98" font-size="9" fill="#546e7a">11:30–12:30</text>
45
+ <rect x="1085.0" y="66" width="55.0" height="52" rx="4" fill="#e3f2fd" stroke="#1565c0" stroke-width="1"/>
46
+ <text x="1089.0" y="84" font-size="10" fill="#0d47a1">Closing…</text><text x="1089.0" y="98" font-size="9" fill="#546e7a">16:30–17:00</text>
47
+ <text x="20" y="160" font-size="12" fill="#263238">Demo lab</text><line x1="150" y1="188" x2="1250.0" y2="188" stroke="#b0bec5"/>
48
+ <rect x="260.0" y="126" width="880.0" height="60" fill="#f1f8e9" opacity="0.6"/>
49
+ <rect x="351.7" y="130" width="82.5" height="52" rx="4" fill="#e3f2fd" stroke="#1565c0" stroke-width="1"/>
50
+ <text x="355.7" y="148" font-size="10" fill="#0d47a1">Live demo: s…</text><text x="355.7" y="162" font-size="9" fill="#546e7a">09:50–10:35</text>
51
+ <rect x="700.0" y="130" width="275.0" height="52" rx="4" fill="#e3f2fd" stroke="#1565c0" stroke-width="1"/>
52
+ <text x="704.0" y="148" font-size="10" fill="#0d47a1">Hands-on lab</text><text x="704.0" y="162" font-size="9" fill="#546e7a">13:00–15:30</text>
53
+ <text x="20" y="224" font-size="12" fill="#263238">Foyer</text><line x1="150" y1="252" x2="1250.0" y2="252" stroke="#b0bec5"/>
54
+ <rect x="150.0" y="190" width="1100.0" height="60" fill="#f1f8e9" opacity="0.6"/>
55
+ <rect x="205.0" y="194" width="55.0" height="52" rx="4" fill="#e3f2fd" stroke="#1565c0" stroke-width="1"/>
56
+ <text x="209.0" y="212" font-size="10" fill="#0d47a1">Doors a…</text><text x="209.0" y="226" font-size="9" fill="#546e7a">08:30–09:00</text>
57
+ <text x="20" y="282" font-size="12" fill="#2e7d32">PASS: no clashes</text><text x="20" y="298" font-size="10" fill="#666">Dashed amber: setup before an item · green band: room open</text>
58
+ </svg>
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "run-of-show",
3
+ "version": "0.1.2",
4
+ "description": "Turn an event's agenda into a timed run of show — every session, speaker, changeover and break with its start, end, room and who's needed — and check it with a script for two things in one room at once, a speaker booked in two places, changeovers shorter than the turnaround and anything outside venue hours, drawn as a timeline per room. Use when an organiser asks for \"a run of show\", \"a running order\", \"the minute-by-minute for the day\", \"check the agenda for clashes\", or has sessions, speakers and rooms for a conference, launch, festival or wedding day.",
5
+ "entrypoint": "SKILL.md",
6
+ "deps": {
7
+ "npm": [],
8
+ "pip": []
9
+ },
10
+ "env": {
11
+ "required": [],
12
+ "optional": []
13
+ },
14
+ "related": [
15
+ "seating-plan"
16
+ ],
17
+ "handoff": [
18
+ {
19
+ "to": "seating-plan",
20
+ "when": "the day includes a seated meal or dinner",
21
+ "purpose": "Seat the dinner guests so every together, apart and access request holds.",
22
+ "fallback": "If seating-plan is absent, list each table with its guests and check by hand that no table is over its count and every keep-apart pair is split."
23
+ }
24
+ ],
25
+ "tags": [
26
+ "events",
27
+ "run-of-show",
28
+ "agenda",
29
+ "schedule",
30
+ "conference",
31
+ "timeline",
32
+ "svg"
33
+ ],
34
+ "permissions": {
35
+ "network": [],
36
+ "commands": [],
37
+ "files": "named-paths",
38
+ "notes": "Reads the show.json it is given and writes the SVG to the path passed with --svg."
39
+ },
40
+ "model": {
41
+ "tier": "standard",
42
+ "rationale": "Ordering a day around rooms and people is bounded judgment; the script decides whether anything clashes."
43
+ }
44
+ }
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env python3
2
+ """Check an event's run of show for clashes and draw it as a timeline. Stdlib only.
3
+
4
+ Reads a show.json: the rooms (opening hours, turnaround between items), the items (title,
5
+ room, start, end or duration, the people needed, setup minutes needed in the room before it
6
+ starts, changeover minutes needed after it), the travel minutes between rooms, and the
7
+ published end of the day. It fails a show where
8
+ - an item ends before it starts
9
+ - two items overlap in one room
10
+ - the gap between two items in a room is shorter than the turnaround, the next item's
11
+ setup or the last item's changeover
12
+ - an item, with its setup, runs outside its room's opening hours
13
+ - a person is needed in two places at once, or has less than the travel time between
14
+ rooms
15
+ and warns where
16
+ - a person works more than --max-hours without a break of --break minutes
17
+ - a room sits empty for more than --max-gap minutes between items
18
+ - the last item ends after the published end
19
+ Then it prints the running order, and with --svg draws one lane per room with each item as
20
+ a block at its times, failures in red.
21
+
22
+ Times are 24-hour "HH:MM". Items on several days carry a "day" (any label, such as
23
+ "2026-06-12" or "Day 2"); each day is checked on its own.
24
+
25
+ Usage:
26
+ python3 show_check.py show.json [--svg timeline.svg] [--json]
27
+ [--max-hours 3] [--break 15] [--max-gap 45]
28
+ Exit codes: 0 no failures, 1 a failure, 2 bad input.
29
+ """
30
+ import argparse
31
+ import json
32
+ import math
33
+ import re
34
+ import sys
35
+
36
+ TIME = re.compile(r"^([01]?\d|2[0-4]):([0-5]\d)$")
37
+
38
+
39
+ class BadInput(ValueError):
40
+ pass
41
+
42
+
43
+ def minutes(v, what):
44
+ """'09:30' -> 570. Raises BadInput."""
45
+ m = TIME.match(str(v).strip()) if v is not None else None
46
+ if not m or (int(m.group(1)) == 24 and m.group(2) != "00"):
47
+ raise BadInput(f"{what} must be a 24-hour time like 09:30, got {v!r}")
48
+ return int(m.group(1)) * 60 + int(m.group(2))
49
+
50
+
51
+ def hhmm(n):
52
+ return f"{int(n) // 60:02d}:{int(n) % 60:02d}"
53
+
54
+
55
+ def _mins(v, what):
56
+ if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v) or v < 0:
57
+ raise BadInput(f"{what} must be a number of minutes, got {v!r}")
58
+ return float(v)
59
+
60
+
61
+ def load(data):
62
+ if not isinstance(data, dict):
63
+ raise BadInput("the file must hold a JSON object")
64
+ default_turn = _mins(data.get("turnaround", 0), "turnaround")
65
+ rooms = {}
66
+ for i, r in enumerate(data.get("rooms") or []):
67
+ if not isinstance(r, dict) or not r.get("name"):
68
+ raise BadInput(f"room {i + 1} needs a name")
69
+ n = str(r["name"])
70
+ rooms[n] = {"name": n, "open": minutes(r["open"], f"room {n} open") if r.get("open") else None,
71
+ "close": minutes(r["close"], f"room {n} close") if r.get("close") else None,
72
+ "turnaround": _mins(r.get("turnaround", default_turn), f"room {n} turnaround")}
73
+ items = []
74
+ for i, it in enumerate(data.get("items") or []):
75
+ if not isinstance(it, dict) or not it.get("title"):
76
+ raise BadInput(f"item {i + 1} needs a title")
77
+ iid = str(it.get("id") or f"item {i + 1}")
78
+ room = str(it.get("room") or "")
79
+ if not room:
80
+ raise BadInput(f"{iid} ({it['title']}) needs a room")
81
+ if room not in rooms:
82
+ rooms[room] = {"name": room, "open": None, "close": None, "turnaround": default_turn}
83
+ start = minutes(it.get("start"), f"{iid} start")
84
+ if it.get("end") is not None:
85
+ end = minutes(it["end"], f"{iid} end")
86
+ elif it.get("duration") is not None:
87
+ end = start + _mins(it["duration"], f"{iid} duration")
88
+ else:
89
+ raise BadInput(f"{iid} ({it['title']}) needs an end or a duration")
90
+ people = it.get("people") or []
91
+ if isinstance(people, str):
92
+ people = [people]
93
+ items.append({"id": iid, "title": str(it["title"]), "room": room, "day": str(it.get("day") or ""),
94
+ "start": start, "end": end, "people": [str(p) for p in people],
95
+ "setup": _mins(it.get("setup", 0), f"{iid} setup"),
96
+ "changeover": _mins(it.get("changeover", 0), f"{iid} changeover")})
97
+ if not items:
98
+ raise BadInput("no items")
99
+ travel = {}
100
+ for i, t in enumerate(data.get("travel") or []):
101
+ if not isinstance(t, dict) or not isinstance(t.get("between"), list) or len(t["between"]) != 2:
102
+ raise BadInput(f"travel {i + 1} must be {{\"between\": [room, room], \"minutes\": n}}")
103
+ a, b = (str(x) for x in t["between"])
104
+ travel[frozenset((a, b))] = _mins(t.get("minutes"), f"travel {i + 1} minutes")
105
+ end = minutes(data["published_end"], "published_end") if data.get("published_end") else None
106
+ return {"rooms": rooms, "items": items, "travel": travel, "published_end": end}
107
+
108
+
109
+ def check(show, max_hours=3.0, brk=15.0, max_gap=45.0):
110
+ fails, warns, bad = [], [], set()
111
+ items = show["items"]
112
+ label = lambda it: f"{it['title']} ({hhmm(it['start'])}{', ' + it['day'] if it['day'] else ''})"
113
+ for it in items:
114
+ if it["end"] <= it["start"]:
115
+ fails.append(f"{label(it)} ends at {hhmm(it['end'])}, not after it starts")
116
+ bad.add(it["id"])
117
+ ok = [it for it in items if it["end"] > it["start"]]
118
+ groups = {}
119
+ for it in ok:
120
+ groups.setdefault((it["day"], it["room"]), []).append(it)
121
+ for (day, room), lane in groups.items():
122
+ r = show["rooms"][room]
123
+ lane.sort(key=lambda it: it["start"])
124
+ for it in lane:
125
+ if r["open"] is not None and it["start"] - it["setup"] < r["open"]:
126
+ need = f" with {it['setup']:.0f} min setup" if it["setup"] else ""
127
+ fails.append(f"{label(it)} starts{need} before {room} opens at {hhmm(r['open'])}")
128
+ bad.add(it["id"])
129
+ if r["close"] is not None and it["end"] > r["close"]:
130
+ fails.append(f"{label(it)} ends at {hhmm(it['end'])}, after {room} closes at {hhmm(r['close'])}")
131
+ bad.add(it["id"])
132
+ for a, b in zip(lane, lane[1:]):
133
+ gap = b["start"] - a["end"]
134
+ if gap < 0:
135
+ fails.append(f"{label(a)} and {label(b)} overlap in {room} by {-gap:.0f} min")
136
+ bad.update((a["id"], b["id"]))
137
+ continue
138
+ need = max(r["turnaround"], b["setup"], a["changeover"])
139
+ if gap < need:
140
+ why = ("the room's turnaround" if need == r["turnaround"] else
141
+ f"{b['title']}'s setup" if need == b["setup"] else f"{a['title']}'s changeover")
142
+ fails.append(f"{room}: {gap:.0f} min between {label(a)} and {label(b)}, "
143
+ f"{need:.0f} min needed for {why}")
144
+ bad.update((a["id"], b["id"]))
145
+ elif gap > max_gap:
146
+ warns.append(f"{room} is empty for {gap:.0f} min between {a['title']} and {b['title']}")
147
+ people = {}
148
+ for it in ok:
149
+ for p in it["people"]:
150
+ people.setdefault((it["day"], p), []).append(it)
151
+ for (day, p), mine in people.items():
152
+ mine.sort(key=lambda it: it["start"])
153
+ for a, b in zip(mine, mine[1:]):
154
+ gap = b["start"] - a["end"]
155
+ if gap < 0:
156
+ fails.append(f"{p} is needed at {label(a)} in {a['room']} and {label(b)} in {b['room']} at once")
157
+ bad.update((a["id"], b["id"]))
158
+ elif a["room"] != b["room"]:
159
+ t = show["travel"].get(frozenset((a["room"], b["room"])))
160
+ if t is not None and gap < t:
161
+ fails.append(f"{p} has {gap:.0f} min to get from {a['room']} to {b['room']} "
162
+ f"between {a['title']} and {b['title']}; the walk takes {t:.0f}")
163
+ bad.update((a["id"], b["id"]))
164
+ run_start, last_end = None, None
165
+ for it in mine:
166
+ if last_end is None or it["start"] - last_end >= brk:
167
+ run_start = it["start"]
168
+ last_end = max(last_end or it["end"], it["end"])
169
+ if last_end - run_start > max_hours * 60:
170
+ warns.append(f"{p} works from {hhmm(run_start)} to {hhmm(last_end)}"
171
+ f"{' on ' + day if day else ''} without a {brk:.0f}-minute break")
172
+ run_start = math.inf # one warning per stretch
173
+ # an inf run_start makes the difference negative until the next break resets it
174
+ if show["published_end"] is not None:
175
+ for day in sorted({it["day"] for it in ok}):
176
+ last = max((it for it in ok if it["day"] == day), key=lambda it: it["end"])
177
+ if last["end"] > show["published_end"]:
178
+ warns.append(f"{last['title']} ends at {hhmm(last['end'])}{', ' + day if day else ''}, "
179
+ f"{last['end'] - show['published_end']:.0f} min after the published end "
180
+ f"of {hhmm(show['published_end'])}")
181
+ return {"fails": fails, "warns": warns, "bad": sorted(bad)}
182
+
183
+
184
+ def order(show):
185
+ return sorted(show["items"], key=lambda it: (it["day"], it["start"], it["room"]))
186
+
187
+
188
+ def report(show, r):
189
+ out, day = [], None
190
+ for it in order(show):
191
+ if it["day"] != day and it["day"]:
192
+ out.append(it["day"])
193
+ day = it["day"]
194
+ who = f" — {', '.join(it['people'])}" if it["people"] else ""
195
+ out.append(f" {hhmm(it['start'])}–{hhmm(it['end'])} {it['room']}: {it['title']}{who}")
196
+ out += [f"FAIL {f}" for f in r["fails"]] + [f"WARN {w}" for w in r["warns"]]
197
+ out.append("PASS" if not r["fails"] else f"{len(r['fails'])} failure(s)")
198
+ return "\n".join(out) + "\n"
199
+
200
+
201
+ def esc(t):
202
+ return str(t).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
203
+
204
+
205
+ def svg(show, r, px=3.0, lane=64):
206
+ """One panel per day: a lane per room, a block per item, a tick every 30 minutes."""
207
+ days = sorted({it["day"] for it in show["items"]})
208
+ rooms = list(show["rooms"])
209
+ out_body, y = [], 50
210
+ width = 0
211
+ for day in days:
212
+ its = [it for it in show["items"] if it["day"] == day]
213
+ t0 = min(min(it["start"] for it in its), *(show["rooms"][it["room"]]["open"] or it["start"] for it in its))
214
+ t1 = max(max(it["end"] for it in its), *(show["rooms"][it["room"]]["close"] or it["end"] for it in its))
215
+ t0, t1 = 30 * math.floor(t0 / 30), 30 * math.ceil(t1 / 30)
216
+ scale = min(px, 1100.0 / max(t1 - t0, 30)) # a long day still fits a page
217
+ X = lambda t, t0=t0, scale=scale: 150 + (t - t0) * scale
218
+ width = max(width, X(t1) + 30)
219
+ if day:
220
+ out_body.append(f'<text x="20" y="{y:.0f}" font-size="14" font-weight="bold">{esc(day)}</text>')
221
+ y += 14
222
+ top = y + 10
223
+ for t in range(int(t0), int(t1) + 1, 30):
224
+ major = t % 60 == 0
225
+ out_body.append(f'<line x1="{X(t):.1f}" y1="{top:.0f}" x2="{X(t):.1f}" y2="{top + lane * len(rooms):.0f}" '
226
+ f'stroke="{"#cfd8dc" if major else "#eceff1"}"/>')
227
+ if major:
228
+ out_body.append(f'<text x="{X(t):.1f}" y="{top - 3:.0f}" font-size="10" text-anchor="middle" fill="#607d8b">{hhmm(t)}</text>')
229
+ for k, room in enumerate(rooms):
230
+ ly = top + k * lane
231
+ rm = show["rooms"][room]
232
+ out_body.append(f'<text x="20" y="{ly + lane / 2 + 4:.0f}" font-size="12" fill="#263238">{esc(room)}</text>'
233
+ f'<line x1="150" y1="{ly + lane:.0f}" x2="{X(t1):.1f}" y2="{ly + lane:.0f}" stroke="#b0bec5"/>')
234
+ if rm["open"] is not None and rm["close"] is not None:
235
+ out_body.append(f'<rect x="{X(rm["open"]):.1f}" y="{ly + 2:.0f}" width="{(rm["close"] - rm["open"]) * scale:.1f}" '
236
+ f'height="{lane - 4}" fill="#f1f8e9" opacity="0.6"/>')
237
+ for it in its:
238
+ if it["room"] != room:
239
+ continue
240
+ bad = it["id"] in r["bad"]
241
+ x0, w = X(it["start"]), max((it["end"] - it["start"]) * scale, 2)
242
+ if it["setup"]:
243
+ out_body.append(f'<rect x="{x0 - it["setup"] * scale:.1f}" y="{ly + 8:.0f}" width="{it["setup"] * scale:.1f}" '
244
+ f'height="{lane - 16}" fill="#fff8e1" stroke="#ffb300" stroke-dasharray="3,2"/>')
245
+ out_body.append(f'<rect x="{x0:.1f}" y="{ly + 6:.0f}" width="{w:.1f}" height="{lane - 12}" rx="4" '
246
+ f'fill="{"#ffebee" if bad else "#e3f2fd"}" stroke="{"#c62828" if bad else "#1565c0"}" '
247
+ f'stroke-width="{2.5 if bad else 1}"/>')
248
+ title = it["title"] if len(it["title"]) * 6 < w - 6 else it["title"][: max(int((w - 10) / 6), 1)] + "…"
249
+ out_body.append(f'<text x="{x0 + 4:.1f}" y="{ly + 24:.0f}" font-size="10" fill="#0d47a1">{esc(title)}</text>'
250
+ f'<text x="{x0 + 4:.1f}" y="{ly + 38:.0f}" font-size="9" fill="#546e7a">'
251
+ f'{hhmm(it["start"])}–{hhmm(it["end"])}{" ✗" if bad else ""}</text>')
252
+ y = top + lane * len(rooms) + 30
253
+ status = "PASS: no clashes" if not r["fails"] else f"{len(r['fails'])} clash(es): items marked ✗ in red"
254
+ out_body.append(f'<text x="20" y="{y:.0f}" font-size="12" fill="{"#2e7d32" if not r["fails"] else "#c62828"}">{esc(status)}</text>'
255
+ f'<text x="20" y="{y + 16:.0f}" font-size="10" fill="#666">Dashed amber: setup before an item · '
256
+ f'green band: room open</text>')
257
+ H, W = y + 34, max(width, 520)
258
+ head = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W:.0f} {H:.0f}" width="{W:.0f}" height="{H:.0f}" '
259
+ 'font-family="Helvetica, Arial, sans-serif">', f'<rect width="{W:.0f}" height="{H:.0f}" fill="#ffffff"/>',
260
+ '<text x="20" y="28" font-size="16" font-weight="bold">Run of show</text>']
261
+ return "\n".join(head + out_body + ["</svg>"]) + "\n"
262
+
263
+
264
+ def main(argv=None):
265
+ ap = argparse.ArgumentParser(description="Check a run of show for clashes and draw it as a timeline.")
266
+ ap.add_argument("show")
267
+ ap.add_argument("--svg", help="write the timeline here")
268
+ ap.add_argument("--json", action="store_true", help="print the result as JSON")
269
+ ap.add_argument("--max-hours", type=float, default=3.0, help="hours a person may work without a break (default 3)")
270
+ ap.add_argument("--break", dest="brk", type=float, default=15.0, help="minutes that count as a break (default 15)")
271
+ ap.add_argument("--max-gap", type=float, default=45.0, help="minutes a room may sit empty between items (default 45)")
272
+ args = ap.parse_args(argv)
273
+ try:
274
+ with open(args.show, encoding="utf-8") as fh:
275
+ show = load(json.load(fh))
276
+ except (OSError, ValueError) as e:
277
+ print(f"FAIL cannot read {args.show}: {e}")
278
+ return 2
279
+ r = check(show, args.max_hours, args.brk, args.max_gap)
280
+ if args.json:
281
+ print(json.dumps({"pass": not r["fails"], "fails": r["fails"], "warns": r["warns"],
282
+ "items": [dict(it, start=hhmm(it["start"]), end=hhmm(it["end"])) for it in order(show)]},
283
+ indent=1))
284
+ else:
285
+ print(report(show, r), end="")
286
+ if args.svg:
287
+ with open(args.svg, "w", encoding="utf-8") as fh:
288
+ fh.write(svg(show, r))
289
+ if not args.json:
290
+ print(f"\nWrote {args.svg}")
291
+ return 1 if r["fails"] else 0
292
+
293
+
294
+ if __name__ == "__main__":
295
+ sys.exit(main())
@@ -0,0 +1,15 @@
1
+ {
2
+ "published_end": "17:30",
3
+ "turnaround": 10,
4
+ "rooms": [
5
+ {"name": "Main hall", "open": "08:00", "close": "18:00", "turnaround": 15},
6
+ {"name": "Studio B", "open": "09:00", "close": "17:30"}
7
+ ],
8
+ "travel": [{"between": ["Main hall", "Studio B"], "minutes": 5}],
9
+ "items": [
10
+ {"id": "doors", "title": "Doors and registration", "room": "Main hall", "start": "08:30", "end": "09:00", "people": ["Front of house"]},
11
+ {"id": "key", "title": "Opening keynote", "room": "Main hall", "start": "09:15", "duration": 45, "people": ["Ana Ruiz", "AV crew"], "setup": 15},
12
+ {"id": "ws1", "title": "Workshop: pricing", "room": "Studio B", "start": "10:10", "end": "11:00", "people": ["Ana Ruiz"]},
13
+ {"id": "panel", "title": "Customer panel", "room": "Main hall", "start": "10:15", "end": "11:15", "people": ["Joe Park", "AV crew"], "changeover": 20}
14
+ ]
15
+ }