skilldrop-cli 1.5.0 → 1.7.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.
- package/README.md +4 -1
- package/catalogue.json +25 -2
- package/model-routing.json +20 -0
- package/package.json +1 -1
- package/packs/civil-architect/pack.json +60 -0
- package/packs/civil-architect/skills/floor-plan-layout/SKILL.md +103 -0
- package/packs/civil-architect/skills/floor-plan-layout/evals/eval_queries.json +30 -0
- package/packs/civil-architect/skills/floor-plan-layout/evals/evals.json +57 -0
- package/packs/civil-architect/skills/floor-plan-layout/evals/files/4/adu-programme.md +12 -0
- package/packs/civil-architect/skills/floor-plan-layout/examples/ground-floor.md +65 -0
- package/packs/civil-architect/skills/floor-plan-layout/examples/ground-floor.svg +135 -0
- package/packs/civil-architect/skills/floor-plan-layout/manifest.json +34 -0
- package/packs/civil-architect/skills/floor-plan-layout/scripts/plan_check.py +449 -0
- package/packs/civil-architect/skills/floor-plan-layout/templates/plan.json +38 -0
- package/packs/event-planner/pack.json +61 -0
- package/packs/event-planner/skills/run-of-show/SKILL.md +141 -0
- package/packs/event-planner/skills/run-of-show/evals/eval_queries.json +30 -0
- package/packs/event-planner/skills/run-of-show/evals/evals.json +103 -0
- package/packs/event-planner/skills/run-of-show/evals/files/1/agenda.md +16 -0
- package/packs/event-planner/skills/run-of-show/evals/files/2/schedule.csv +8 -0
- package/packs/event-planner/skills/run-of-show/evals/files/3/notes.txt +3 -0
- package/packs/event-planner/skills/run-of-show/evals/files/4/sessions_export.csv +25 -0
- package/packs/event-planner/skills/run-of-show/evals/files/4/venue_and_crew.txt +27 -0
- package/packs/event-planner/skills/run-of-show/evals/files/5/runsheet_v4.csv +28 -0
- package/packs/event-planner/skills/run-of-show/evals/files/5/venue_and_suppliers.txt +22 -0
- package/packs/event-planner/skills/run-of-show/evals/files/6/acts_from_booker.csv +26 -0
- package/packs/event-planner/skills/run-of-show/evals/files/6/venue_rooms_and_crew.txt +14 -0
- package/packs/event-planner/skills/run-of-show/evals/files/7/runsheet_draft_v4.csv +25 -0
- package/packs/event-planner/skills/run-of-show/evals/files/7/venue_and_fixed_times.txt +24 -0
- package/packs/event-planner/skills/run-of-show/examples/product-launch.md +77 -0
- package/packs/event-planner/skills/run-of-show/examples/product-launch.svg +58 -0
- package/packs/event-planner/skills/run-of-show/manifest.json +44 -0
- package/packs/event-planner/skills/run-of-show/scripts/show_check.py +295 -0
- package/packs/event-planner/skills/run-of-show/templates/show.json +15 -0
- package/packs/event-planner/skills/seating-plan/SKILL.md +152 -0
- package/packs/event-planner/skills/seating-plan/evals/eval_queries.json +30 -0
- package/packs/event-planner/skills/seating-plan/evals/evals.json +134 -0
- package/packs/event-planner/skills/seating-plan/evals/files/1/guests.csv +31 -0
- package/packs/event-planner/skills/seating-plan/evals/files/2/table-plan.txt +8 -0
- package/packs/event-planner/skills/seating-plan/evals/files/3/brief.txt +4 -0
- package/packs/event-planner/skills/seating-plan/evals/files/3/guests.csv +49 -0
- package/packs/event-planner/skills/seating-plan/evals/files/4/rsvp_export.csv +127 -0
- package/packs/event-planner/skills/seating-plan/evals/files/4/seating_notes.txt +21 -0
- package/packs/event-planner/skills/seating-plan/evals/files/5/organiser_notes.txt +10 -0
- package/packs/event-planner/skills/seating-plan/evals/files/5/registrations.csv +93 -0
- package/packs/event-planner/skills/seating-plan/evals/files/5/table_plan_v3.csv +94 -0
- package/packs/event-planner/skills/seating-plan/evals/files/6/bridgets_notes.txt +30 -0
- package/packs/event-planner/skills/seating-plan/evals/files/6/ticket_export.csv +117 -0
- package/packs/event-planner/skills/seating-plan/evals/files/6/venue_floorplan_notes.txt +14 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/keep_apart_CONFIDENTIAL.txt +9 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/prom_seating_v3.csv +101 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/ticket_list.csv +101 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/venue_info.txt +16 -0
- package/packs/event-planner/skills/seating-plan/evals/files/8/registrations_export.csv +147 -0
- package/packs/event-planner/skills/seating-plan/evals/files/8/room_layout.md +22 -0
- package/packs/event-planner/skills/seating-plan/evals/files/8/seating_requests.txt +31 -0
- package/packs/event-planner/skills/seating-plan/evals/files/9/guest_list.csv +84 -0
- package/packs/event-planner/skills/seating-plan/evals/files/9/notes.txt +16 -0
- package/packs/event-planner/skills/seating-plan/evals/files/9/table_plan.csv +85 -0
- package/packs/event-planner/skills/seating-plan/examples/wedding-dinner.md +61 -0
- package/packs/event-planner/skills/seating-plan/examples/wedding-dinner.svg +39 -0
- package/packs/event-planner/skills/seating-plan/manifest.json +44 -0
- package/packs/event-planner/skills/seating-plan/scripts/seating_check.py +324 -0
- package/packs/event-planner/skills/seating-plan/templates/seating.json +28 -0
- package/packs/experience-design/pack.json +5 -0
- package/packs/experience-design/skills/locale-check/SKILL.md +139 -0
- package/packs/experience-design/skills/locale-check/evals/eval_queries.json +38 -0
- package/packs/experience-design/skills/locale-check/evals/evals.json +104 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/budget.json +1 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/ar.json +16 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/de.json +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/en.json +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/pl.json +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values/strings.xml +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values-es/strings.xml +11 -0
- package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values-fr/strings.xml +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/3/po/ja.po +20 -0
- package/packs/experience-design/skills/locale-check/evals/files/3/po/messages.pot +23 -0
- package/packs/experience-design/skills/locale-check/evals/files/3/po/ru.po +22 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/budgets.json +15 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/ar.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/de.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/en.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/fr.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/pl.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values/strings.xml +77 -0
- package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values-es/strings.xml +63 -0
- package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values-ja/strings.xml +61 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/cs.po +605 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/marlin.pot +602 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/pt_BR.po +602 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/ru.po +608 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/en.lproj/Localizable.strings +160 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/en.lproj/Localizable.stringsdict +70 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/he.lproj/Localizable.strings +157 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/he.lproj/Localizable.stringsdict +86 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/it.lproj/Localizable.strings +160 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/it.lproj/Localizable.stringsdict +68 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/ko.lproj/Localizable.strings +160 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/ko.lproj/Localizable.stringsdict +62 -0
- package/packs/experience-design/skills/locale-check/examples/settings-release.md +107 -0
- package/packs/experience-design/skills/locale-check/manifest.json +45 -0
- package/packs/experience-design/skills/locale-check/scripts/locale_check.py +1105 -0
- package/packs/operations-manager/pack.json +60 -0
- package/packs/operations-manager/skills/shift-roster/SKILL.md +166 -0
- package/packs/operations-manager/skills/shift-roster/evals/eval_queries.json +38 -0
- package/packs/operations-manager/skills/shift-roster/evals/evals.json +135 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/1/shifts.csv +3 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/1/staff.csv +8 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/2/rota.csv +22 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/2/staff.csv +6 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/3/team.md +14 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/4/availability_export_wk47.csv +23 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/4/rota_notes.txt +20 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/5/rota_draft_16-29nov.csv +107 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/5/staff_list.csv +17 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/6/availability_notes.txt +24 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/6/peoplehub_staff_export.csv +14 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/6/rota_template.csv +25 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/7/leave_and_availability.txt +18 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/7/rota_wc_30nov.csv +47 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/7/staff_list.csv +15 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/8/availability_form_responses.csv +22 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/8/cover_requirements.csv +15 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/8/staff_export.csv +29 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/9/leave_and_restrictions.txt +16 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/9/rota_export_07-13Dec.csv +85 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/9/staff_list.csv +27 -0
- package/packs/operations-manager/skills/shift-roster/examples/cafe-week.md +72 -0
- package/packs/operations-manager/skills/shift-roster/examples/cafe-week.svg +123 -0
- package/packs/operations-manager/skills/shift-roster/manifest.json +34 -0
- package/packs/operations-manager/skills/shift-roster/scripts/roster_check.py +633 -0
- package/packs/operations-manager/skills/shift-roster/templates/roster.json +22 -0
|
@@ -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.1",
|
|
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("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
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
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: seating-plan
|
|
3
|
+
description: Seat guests at tables for a wedding, gala, conference dinner or awards night, and check the chart with a script — every guest seated once, no table over capacity, people who must sit together together, people who must be kept apart apart, wheelchair users at accessible places — drawn as a room plan with names at each table. Use when an organiser asks to "make the seating plan", "who sits where", "seat these 120 guests", "check my table plan", or has a guest list with groups and keep-apart requests.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# seating-plan
|
|
7
|
+
|
|
8
|
+
A table plan for a seated event, where every rule the organiser gave holds at once: each guest
|
|
9
|
+
has exactly one seat, no table is over its count, households and groups sit together, the
|
|
10
|
+
people who must be kept apart are, and wheelchair users are at tables with step-free access.
|
|
11
|
+
With 120 guests and a dozen requests, a plan that looks right usually breaks one of them. The
|
|
12
|
+
agent decides who sits where and why; a script checks every rule on every table and draws the
|
|
13
|
+
room, so a broken request is found before the organiser prints place cards.
|
|
14
|
+
|
|
15
|
+
## How to respond
|
|
16
|
+
|
|
17
|
+
**These rules come before the steps and outrank them:**
|
|
18
|
+
|
|
19
|
+
- **Their file comes back in exactly its own shape.** Fixing their plan, return their file
|
|
20
|
+
with the same columns, column order, headers and row order, and only the cells a rule
|
|
21
|
+
forces changed, ready for the venue to print from. Building from a guest list, hand back
|
|
22
|
+
their columns plus only the table (and seat) columns the plan needs. Every change goes in
|
|
23
|
+
the reply, not the file: the guest, what moved and why. Add a change column, or any other
|
|
24
|
+
column, only when they ask for one. Given a pasted list, reply with a table. Write every
|
|
25
|
+
deliverable the request names, such as place-card order or the caterer's list.
|
|
26
|
+
- **Working files stay out of sight.** The checker's input and any intermediate file go in a
|
|
27
|
+
temporary folder outside the working directory, or are deleted before you reply. Draw the
|
|
28
|
+
room only when a drawing is asked for or the venue needs a floor plan, and then once. Never
|
|
29
|
+
leave anything running in the background.
|
|
30
|
+
- **Carry every field through, without reshaping their file.** Every detail the person gave
|
|
31
|
+
per guest (meal choice, dietary need, party, notes) reaches the reply or the deliverable it
|
|
32
|
+
belongs in, such as the caterer's list, and each conflict between them is flagged: a vegan
|
|
33
|
+
guest who chose the beef, a child in a seat marked for a speaker.
|
|
34
|
+
- **Soft notes are rules.** "Keep the grandparents near the door", "not by the speakers":
|
|
35
|
+
build them in, and say which you couldn't meet.
|
|
36
|
+
- **Fixing their plan means minimal changes.** Move only the guests a rule forces, list each
|
|
37
|
+
move with the rule, and keep every other seat as they placed it.
|
|
38
|
+
- **A passing check is the floor, not the reply.** The reply is about their guests, in their
|
|
39
|
+
words: never file formats, tools or paths.
|
|
40
|
+
|
|
41
|
+
1. **Collect the guest list and the rules.** Take them from the files the organiser names (a
|
|
42
|
+
guest list CSV or spreadsheet export, an RSVP export, a pasted list) or the message:
|
|
43
|
+
- **Guests:** name, household or party, and needs: wheelchair, near an exit, dietary
|
|
44
|
+
(dietary and meal choice are carried through to the deliverable, not seating rules)
|
|
45
|
+
- **Tables:** how many, seats each, round or long, which are step-free, and where they
|
|
46
|
+
stand, if a room layout exists
|
|
47
|
+
- **Requests:** who must sit together, who must be kept apart, fixed seats (the head table,
|
|
48
|
+
a sponsor's table)
|
|
49
|
+
|
|
50
|
+
Ask at most two questions, only about a rule that changes the plan. With no guests, emit
|
|
51
|
+
`BLOCKED: need the guest list`.
|
|
52
|
+
|
|
53
|
+
2. **Write the plan file** in the shape of [`templates/seating.json`](templates/seating.json):
|
|
54
|
+
`tables` (id, capacity, shape, `at` in metres, `accessible`), `guests` (id, name, party,
|
|
55
|
+
needs, dietary), `together`, `apart`, `fixed`, and `assignments` as table → guest ids.
|
|
56
|
+
Seat in this order, because each step narrows the next:
|
|
57
|
+
1. fixed seats;
|
|
58
|
+
2. wheelchair users and anyone who must be near an exit, at the tables that allow it;
|
|
59
|
+
3. together groups and households, whole;
|
|
60
|
+
4. the apart pairs, kept on different tables;
|
|
61
|
+
5. everyone else, filling tables evenly rather than leaving one half empty.
|
|
62
|
+
|
|
63
|
+
3. **Run the checker and fix every FAIL before showing anything.** Keep `seating.json` in a
|
|
64
|
+
temporary folder; a drawing, when wanted, goes in the working directory as
|
|
65
|
+
`seating-plan.svg`, never inside this folder.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Claude Code
|
|
69
|
+
python3 "${CLAUDE_SKILL_DIR}/scripts/seating_check.py" "$TMPDIR/seating.json"
|
|
70
|
+
# Other IDEs (from the skill folder); add --svg only when a drawing is wanted
|
|
71
|
+
python3 scripts/seating_check.py /tmp/seating/seating.json --svg seating-plan.svg
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Exit 0 means every rule holds; 1 means one is broken; 2 means the file is malformed. Move
|
|
75
|
+
guests and rerun until it exits 0. A WARN (a household split because it is bigger than a
|
|
76
|
+
table, a half-empty table, someone who came alone at a table of one family) stands only
|
|
77
|
+
with a sentence saying why.
|
|
78
|
+
|
|
79
|
+
4. **When the rules can't all hold, say which and stop guessing.** Name the conflict and the
|
|
80
|
+
smallest change that resolves it: ✅ *"The Moss and Park families (14 people) must sit
|
|
81
|
+
together, and Ed and Bo must be kept apart, but Ed is in the Park group and Bo in the Moss:
|
|
82
|
+
with 10-seat tables they can't share one, so one family splits. Splitting the Parks 6 + 4
|
|
83
|
+
keeps every other request."* Never break a request quietly to make the plan pass.
|
|
84
|
+
|
|
85
|
+
5. **Reply for the organiser, in this order:**
|
|
86
|
+
1. **The plan:** in their file's format when they gave one, otherwise a table of each
|
|
87
|
+
table and who sits there, step-free tables marked.
|
|
88
|
+
2. **What couldn't be met,** with the reason and the choice it needs from them.
|
|
89
|
+
3. **The assumptions,** each tagged `[assumption]`: a seat count or a layout they didn't give.
|
|
90
|
+
4. **The caterer's list:** meal choice and dietary need per guest and per table, with each
|
|
91
|
+
conflict flagged.
|
|
92
|
+
5. **The files:** their plan in their format, and the drawing if one was made.
|
|
93
|
+
|
|
94
|
+
Write for someone planning an event: no file formats, no tool or script names, no paths
|
|
95
|
+
inside this folder. Say *"every request is met"*, not what checked it.
|
|
96
|
+
|
|
97
|
+
**Checking a plan the organiser already made:** run the same checker on their plan, report
|
|
98
|
+
each problem with the fix that disturbs the fewest guests, and hand back their file with
|
|
99
|
+
those fixes made and noted. Don't reseat the room unless they ask.
|
|
100
|
+
|
|
101
|
+
**Non-interactive runs** (subagent, CI, headless): with no table size, use 10-seat rounds; with
|
|
102
|
+
no layout, draw tables in a grid; with no accessible tables named, mark the ones nearest an
|
|
103
|
+
exit, each tagged `[assumption]` at the top of the reply. With no guest list, emit
|
|
104
|
+
`BLOCKED: need the guest list`.
|
|
105
|
+
|
|
106
|
+
## Useful references in this skill
|
|
107
|
+
|
|
108
|
+
- [`templates/seating.json`](templates/seating.json) — the file shape: tables, guests, requests and assignments
|
|
109
|
+
- [`scripts/seating_check.py`](scripts/seating_check.py) — checks every guest seated once, capacity, together, apart, access and fixed seats; draws the room
|
|
110
|
+
- [`examples/wedding-dinner.md`](examples/wedding-dinner.md) — worked example: a 24-guest wedding where the brief itself conflicts, and the reply names it
|
|
111
|
+
|
|
112
|
+
The order-of-the-day timings, the speeches and the changeovers belong to `run-of-show`.
|
|
113
|
+
|
|
114
|
+
## Quality bar
|
|
115
|
+
|
|
116
|
+
- **Every guest has exactly one seat, and no table is over its count.** The checker exits 0 before the plan is shown.
|
|
117
|
+
- **Every together group and every apart pair holds,** or the reply names the one that can't and why.
|
|
118
|
+
- **Every wheelchair user is at a step-free table,** and every fixed seat is where it was fixed.
|
|
119
|
+
- **Tables fill evenly:** no half-empty table without a reason.
|
|
120
|
+
- **The reply leads with who sits where,** in a table the organiser could print.
|
|
121
|
+
- **Nothing is invented:** no guest, request or table that wasn't given.
|
|
122
|
+
- **Their file comes back in its exact columns and row order,** with every change listed in the reply, not in the file.
|
|
123
|
+
- **Every detail they gave per guest reaches the reply or the deliverable it belongs in,** and each conflict is flagged.
|
|
124
|
+
- **Their notes are honoured,** and their own plan changes only where a rule forces it.
|
|
125
|
+
- **No working file is left in their folder.**
|
|
126
|
+
|
|
127
|
+
## When to use this skill
|
|
128
|
+
|
|
129
|
+
- ✅ A seating chart for a wedding, gala, awards dinner or conference dinner
|
|
130
|
+
- ✅ Checking an existing table plan before the place cards are printed
|
|
131
|
+
- ✅ Reseating after late RSVPs or a new keep-apart request
|
|
132
|
+
- ✅ A plan for 20 guests or 400
|
|
133
|
+
|
|
134
|
+
## When NOT to use this skill
|
|
135
|
+
|
|
136
|
+
- ❌ The order of the day, speeches and changeovers: that's `run-of-show`
|
|
137
|
+
- ❌ Theatre or classroom seating with numbered rows and no groups: it's a seat count, not a plan
|
|
138
|
+
- ❌ Furniture placement and clearances in a room: that's a layout drawing
|
|
139
|
+
- ❌ Choosing the venue or the menu
|
|
140
|
+
|
|
141
|
+
## Anti-patterns to avoid
|
|
142
|
+
|
|
143
|
+
- ❌ **Showing a plan before every rule is checked.** A keep-apart pair at table 7 is the one guests notice.
|
|
144
|
+
- ❌ **Breaking a request quietly to make the numbers fit.** Name the conflict and let the organiser choose.
|
|
145
|
+
- ❌ **Seating in guest-list order.** Fixed seats, access needs and groups go first, or they end up split.
|
|
146
|
+
- ❌ **One half-empty table at the back** while others are full. Spread the space.
|
|
147
|
+
- ❌ **Mentioning files, formats or tools in the reply.** The organiser wants names at tables.
|
|
148
|
+
- ❌ **An extra "change" or "dietary" column in the venue's file.** They print from its exact columns; the changes and the meals go in the reply or the caterer's list.
|
|
149
|
+
- ❌ **Treating dietary needs as a seating rule.** They go to the caterer, per guest and table.
|
|
150
|
+
- ❌ **Dropping a column the person gave.** The meal choices are what the venue needs next.
|
|
151
|
+
- ❌ **A JSON file and a drawing nobody asked for** left beside their guest list.
|
|
152
|
+
- ❌ **Telling them the fixes instead of making them.** Hand back the corrected file.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "make the seating plan for our wedding, 120 guests, here's the list",
|
|
4
|
+
"should_trigger": true
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"query": "who sits where at the gala? keep the board apart from the press",
|
|
8
|
+
"should_trigger": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "check my table plan before I print the place cards",
|
|
12
|
+
"should_trigger": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"query": "seat these conference dinner guests by company, tables of 10",
|
|
16
|
+
"should_trigger": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "write the minute-by-minute for the wedding day",
|
|
20
|
+
"should_trigger": false
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"query": "draw a floor plan for the venue's new function room, 12 x 18 m",
|
|
24
|
+
"should_trigger": false
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "map the guest's journey from booking a ticket to arriving at the gala",
|
|
28
|
+
"should_trigger": false
|
|
29
|
+
}
|
|
30
|
+
]
|