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,61 @@
1
+ # Worked example: a 24-guest wedding dinner
2
+
3
+ Input → output oracle for `seating-plan`. Shows a first draft that broke three requests, a
4
+ conflict in the brief itself (the parents' fixed seats against "keep each household
5
+ together"), and how the reply names it instead of hiding it.
6
+
7
+ ## Input
8
+
9
+ > We're seating our wedding dinner: a head table for four and two round tables of ten, so 24
10
+ > seats for 24 guests. The head table is us (Asha and Leo) plus Asha's mum Meera and Leo's dad
11
+ > Ian. Please keep each household together. Asha's grandmother Nani uses a wheelchair, and only
12
+ > the head table and table 1 are step-free. Priya and Dan must not be at the same table.
13
+ > Dietary: Nani, Omar and Ravi are vegetarian; Tess has a nut allergy. Guest list attached.
14
+
15
+ ## The first draft, checked
16
+
17
+ Seating households in list order, with the parents fixed at the head table, gave:
18
+
19
+ ```
20
+ FAIL must sit together but are split: Meera Verma (Head), Raj Verma (T1), Nani Verma (T1)
21
+ FAIL must sit together but are split: Ian Grant (Head), Sue Grant (T1), Kit Grant (T1)
22
+ FAIL Priya Nair and Dan Moore must be kept apart but both sit at table T1
23
+ 3 failure(s)
24
+ ```
25
+
26
+ Two of the three can't be fixed by moving people. Meera and Ian are fixed at the head table,
27
+ which seats four, and their households are three each. Keeping both households whole means
28
+ leaving the parents off the head table. That is the couple's choice, so the reply asks; it
29
+ doesn't decide. The third was a straight mistake: Dan moves to table 2 and Jon Park takes his
30
+ seat.
31
+
32
+ ## The reply
33
+
34
+ **Every request is met except one that can't be, below.**
35
+
36
+ | Table | Guests |
37
+ |---|---|
38
+ | Head (step-free) | Asha Verma, Leo Grant, Meera Verma, Ian Grant |
39
+ | Table 1 (step-free) | Raj Verma, Nani Verma, Sue Grant, Kit Grant, Omar Diaz, Lia Diaz, Ben Cole, Ada Cole, Priya Nair, Jon Park |
40
+ | Table 2 | Tess Wu, Sam Wu, Nora Bell, Hal Bell, Zoe Lim, Max Lim, Eve Lim, Ravi Shah, Uma Shah, Dan Moore |
41
+
42
+ **One request can't hold as written.** With Meera and Ian at the head table, the Verma and
43
+ Grant households can't each sit together: the head table seats four. Raj and Nani, and Sue and
44
+ Kit, are together at table 1, the table nearest you. If you'd rather keep each household whole,
45
+ the head table becomes just the two of you, and Meera and Ian join their families at table 1.
46
+
47
+ **Caterer's list:** table 1, 2 vegetarian (Nani, Omar); table 2, 1 vegetarian (Ravi) and 1 nut
48
+ allergy (Tess).
49
+
50
+ Nani is at table 1, which is step-free. Priya and Dan are at different tables. The room plan
51
+ with names at each table is in `seating-plan.svg`.
52
+
53
+ ## Why this is the oracle
54
+
55
+ - **Every rule is checked before the plan is shown,** and the draft's three failures never
56
+ reach the couple.
57
+ - **A conflict in the brief is named, with the choice it needs.** A plain pass seats the parents
58
+ at the head table and calls the households "together", or quietly drops the head-table
59
+ request.
60
+ - **The reply is for the couple:** names at tables, the caterer's list, and the one decision
61
+ that's theirs. No file formats, no tooling.
@@ -0,0 +1,39 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 510" width="640" height="510" font-family="Helvetica, Arial, sans-serif">
2
+ <rect width="640" height="510" fill="#ffffff"/>
3
+ <text x="40" y="30" font-size="16" font-weight="bold">Seating plan: 24 of 24 guests, 3 tables</text>
4
+ <rect x="40" y="60" width="560" height="400" fill="#fafafa" stroke="#444" stroke-width="2"/>
5
+ <rect x="26" y="374" width="28" height="12" fill="#2e7d32"/><text x="40" y="402" font-size="10" text-anchor="middle" fill="#2e7d32">EXIT</text>
6
+ <rect x="586" y="374" width="28" height="12" fill="#2e7d32"/><text x="600" y="402" font-size="10" text-anchor="middle" fill="#2e7d32">EXIT</text>
7
+ <rect x="275" y="105" width="91" height="39" fill="#eceff1" stroke="#455a64" stroke-width="1.5"/>
8
+ <text x="320" y="122" font-size="13" font-weight="bold" text-anchor="middle" fill="#455a64">Head</text><text x="320" y="137" font-size="10" text-anchor="middle" fill="#555">4/4 · accessible</text>
9
+ <text x="320" y="98" font-size="9" text-anchor="middle" fill="#222">Asha Verma</text>
10
+ <text x="375" y="127" font-size="9" text-anchor="start" fill="#222">Leo Grant</text>
11
+ <text x="320" y="156" font-size="9" text-anchor="middle" fill="#222">Meera Verma</text>
12
+ <text x="265" y="127" font-size="9" text-anchor="end" fill="#222">Ian Grant</text>
13
+ <circle cx="180" cy="300" r="54" fill="#eceff1" stroke="#455a64" stroke-width="1.5"/>
14
+ <text x="180" y="298" font-size="13" font-weight="bold" text-anchor="middle" fill="#455a64">T1</text><text x="180" y="313" font-size="10" text-anchor="middle" fill="#555">10/10 · accessible</text>
15
+ <text x="180" y="241" font-size="9" text-anchor="middle" fill="#222">Raj Verma</text>
16
+ <text x="216" y="253" font-size="9" text-anchor="start" fill="#222">Nani Verma</text>
17
+ <text x="239" y="284" font-size="9" text-anchor="start" fill="#222">Sue Grant</text>
18
+ <text x="239" y="322" font-size="9" text-anchor="start" fill="#222">Kit Grant</text>
19
+ <text x="216" y="353" font-size="9" text-anchor="start" fill="#222">Omar Diaz</text>
20
+ <text x="180" y="365" font-size="9" text-anchor="middle" fill="#222">Lia Diaz</text>
21
+ <text x="144" y="353" font-size="9" text-anchor="end" fill="#222">Ben Cole</text>
22
+ <text x="121" y="322" font-size="9" text-anchor="end" fill="#222">Ada Cole</text>
23
+ <text x="121" y="284" font-size="9" text-anchor="end" fill="#222">Priya Nair</text>
24
+ <text x="144" y="253" font-size="9" text-anchor="end" fill="#222">Jon Park</text>
25
+ <circle cx="460" cy="300" r="54" fill="#eceff1" stroke="#455a64" stroke-width="1.5"/>
26
+ <text x="460" y="298" font-size="13" font-weight="bold" text-anchor="middle" fill="#455a64">T2</text><text x="460" y="313" font-size="10" text-anchor="middle" fill="#555">10/10</text>
27
+ <text x="460" y="241" font-size="9" text-anchor="middle" fill="#222">Tess Wu</text>
28
+ <text x="496" y="253" font-size="9" text-anchor="start" fill="#222">Sam Wu</text>
29
+ <text x="519" y="284" font-size="9" text-anchor="start" fill="#222">Nora Bell</text>
30
+ <text x="519" y="322" font-size="9" text-anchor="start" fill="#222">Hal Bell</text>
31
+ <text x="496" y="353" font-size="9" text-anchor="start" fill="#222">Zoe Lim</text>
32
+ <text x="460" y="365" font-size="9" text-anchor="middle" fill="#222">Max Lim</text>
33
+ <text x="424" y="353" font-size="9" text-anchor="end" fill="#222">Eve Lim</text>
34
+ <text x="401" y="322" font-size="9" text-anchor="end" fill="#222">Ravi Shah</text>
35
+ <text x="401" y="284" font-size="9" text-anchor="end" fill="#222">Uma Shah</text>
36
+ <text x="424" y="253" font-size="9" text-anchor="end" fill="#222">Dan Moore</text>
37
+ <text x="40" y="488" font-size="12" fill="#2e7d32">PASS: every rule met</text>
38
+ <text x="40" y="506" font-size="10" fill="#666">Positions in metres, 1 m = 40 px · names run clockwise from the top of each table</text>
39
+ </svg>
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "seating-plan",
3
+ "version": "0.1.3",
4
+ "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.",
5
+ "entrypoint": "SKILL.md",
6
+ "deps": {
7
+ "npm": [],
8
+ "pip": []
9
+ },
10
+ "env": {
11
+ "required": [],
12
+ "optional": []
13
+ },
14
+ "related": [
15
+ "run-of-show"
16
+ ],
17
+ "handoff": [
18
+ {
19
+ "to": "run-of-show",
20
+ "when": "the seating is settled and the day's timings are next",
21
+ "purpose": "Time the day around the dinner: doors, speeches, courses and the changeovers between them.",
22
+ "fallback": "If run-of-show is absent, list the day's items with a start, end and room each, and check by hand that no room or person is booked twice."
23
+ }
24
+ ],
25
+ "tags": [
26
+ "events",
27
+ "seating",
28
+ "wedding",
29
+ "gala",
30
+ "table-plan",
31
+ "svg",
32
+ "constraints"
33
+ ],
34
+ "permissions": {
35
+ "network": [],
36
+ "commands": [],
37
+ "files": "named-paths",
38
+ "notes": "Reads the seating.json it is given and writes the SVG to the path passed with --svg."
39
+ },
40
+ "model": {
41
+ "tier": "standard",
42
+ "rationale": "Assigning guests against overlapping rules is bounded judgment; the script decides whether every rule holds."
43
+ }
44
+ }
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env python3
2
+ """Check a table seating plan against its rules and draw the room. Stdlib only.
3
+
4
+ Reads a seating.json: the tables (capacity, shape, position in metres, accessible or not),
5
+ the guests (name, party or household, needs), the rules (groups that sit together, pairs
6
+ kept apart, fixed seats) and the assignments (table -> guests). It fails a plan where
7
+ - a guest is unseated, or seated twice
8
+ - a table holds more guests than its capacity
9
+ - a together group is split across tables
10
+ - an apart pair shares a table
11
+ - a wheelchair user sits at a table not marked accessible
12
+ - a guest with a fixed seat is not at that table
13
+ and warns where
14
+ - a household or party that would fit at one table is split
15
+ - a table is under half full, or empty
16
+ - a guest who came alone sits at a table where everyone else is one other party
17
+ - a guest who needs to be near an exit is further than --exit-distance from one (when
18
+ the room's exits are given)
19
+ Then it lists every table with its guests and dietary needs, and with --svg draws the room:
20
+ tables at their positions, labelled with their fill, names around each, failures in red.
21
+
22
+ Usage:
23
+ python3 seating_check.py seating.json [--svg room.svg] [--json] [--exit-distance 8]
24
+ Exit codes: 0 no failures, 1 a failure, 2 bad input.
25
+ """
26
+ import argparse
27
+ import json
28
+ import math
29
+ import sys
30
+
31
+
32
+ class BadInput(ValueError):
33
+ pass
34
+
35
+
36
+ def _num(v, what):
37
+ if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v):
38
+ raise BadInput(f"{what} must be a number, got {v!r}")
39
+ return float(v)
40
+
41
+
42
+ def _point(v, what):
43
+ if not isinstance(v, (list, tuple)) or len(v) != 2:
44
+ raise BadInput(f"{what} must be [x, y] in metres")
45
+ return (_num(v[0], what + " x"), _num(v[1], what + " y"))
46
+
47
+
48
+ def load(data):
49
+ """Validate the raw JSON and return a normalised plan. Raises BadInput."""
50
+ if not isinstance(data, dict):
51
+ raise BadInput("the file must hold a JSON object")
52
+ tables, guests = {}, {}
53
+ for i, t in enumerate(data.get("tables") or []):
54
+ if not isinstance(t, dict) or not t.get("id"):
55
+ raise BadInput(f"table {i + 1} needs an id")
56
+ tid = str(t["id"])
57
+ if tid in tables:
58
+ raise BadInput(f"two tables are called {tid!r}")
59
+ cap = _num(t.get("capacity", 10), f"table {tid} capacity")
60
+ if cap < 1 or cap != int(cap):
61
+ raise BadInput(f"table {tid} capacity must be a whole number of seats")
62
+ shape = str(t.get("shape", "round"))
63
+ if shape not in ("round", "rect"):
64
+ raise BadInput(f"table {tid} shape must be round or rect")
65
+ tables[tid] = {"id": tid, "capacity": int(cap), "shape": shape,
66
+ "at": _point(t["at"], f"table {tid} position") if "at" in t else None,
67
+ "accessible": bool(t.get("accessible", False)), "label": str(t.get("label", ""))}
68
+ if not tables:
69
+ raise BadInput("no tables")
70
+ for i, g in enumerate(data.get("guests") or []):
71
+ if not isinstance(g, dict) or not g.get("id"):
72
+ raise BadInput(f"guest {i + 1} needs an id")
73
+ gid = str(g["id"])
74
+ if gid in guests:
75
+ raise BadInput(f"two guests have the id {gid!r}")
76
+ needs = g.get("needs") or []
77
+ if isinstance(needs, str):
78
+ needs = [needs]
79
+ guests[gid] = {"id": gid, "name": str(g.get("name") or gid), "party": str(g.get("party") or ""),
80
+ "needs": [str(n).lower() for n in needs], "dietary": str(g.get("dietary") or "")}
81
+ if not guests:
82
+ raise BadInput("no guests")
83
+
84
+ def known(gid, where):
85
+ if str(gid) not in guests:
86
+ raise BadInput(f"{where} names a guest {gid!r} who is not on the guest list")
87
+ return str(gid)
88
+
89
+ together = []
90
+ for i, grp in enumerate(data.get("together") or []):
91
+ if not isinstance(grp, list) or len(grp) < 2:
92
+ raise BadInput(f"together group {i + 1} must list two or more guests")
93
+ together.append([known(x, f"together group {i + 1}") for x in grp])
94
+ apart = []
95
+ for i, pair in enumerate(data.get("apart") or []):
96
+ if not isinstance(pair, list) or len(pair) != 2:
97
+ raise BadInput(f"apart pair {i + 1} must list exactly two guests")
98
+ apart.append([known(x, f"apart pair {i + 1}") for x in pair])
99
+ fixed = {}
100
+ if not isinstance(data.get("fixed") or {}, dict):
101
+ raise BadInput("fixed must map a guest id to a table id")
102
+ for gid, tid in (data.get("fixed") or {}).items():
103
+ if str(tid) not in tables:
104
+ raise BadInput(f"the fixed seat for {gid} names a table {tid!r} that does not exist")
105
+ fixed[known(gid, "fixed")] = str(tid)
106
+
107
+ raw = data.get("assignments") or {}
108
+ if not isinstance(raw, dict):
109
+ raise BadInput("assignments must map each table id to a list of guest ids")
110
+ seats = {tid: [] for tid in tables}
111
+ for tid, gl in raw.items():
112
+ if str(tid) not in tables:
113
+ raise BadInput(f"assignments name a table {tid!r} that does not exist")
114
+ if not isinstance(gl, list):
115
+ raise BadInput(f"assignments for table {tid} must be a list of guest ids")
116
+ seats[str(tid)] += [known(g, f"table {tid}") for g in gl]
117
+ exits = [_point(e, f"exit {i + 1}") for i, e in enumerate(data.get("exits") or [])]
118
+ room = data.get("room") or {}
119
+ if not isinstance(room, dict):
120
+ raise BadInput("room must be {\"width\": metres, \"depth\": metres}")
121
+ return {"tables": tables, "guests": guests, "together": together, "apart": apart, "fixed": fixed,
122
+ "seats": seats, "exits": exits,
123
+ "room": (_num(room["width"], "room width"), _num(room["depth"], "room depth"))
124
+ if room.get("width") and room.get("depth") else None}
125
+
126
+
127
+ def check(plan, exit_distance=8.0):
128
+ tables, guests, seats = plan["tables"], plan["guests"], plan["seats"]
129
+ fails, warns, bad_tables = [], [], set()
130
+ where = {}
131
+ for tid, gl in seats.items():
132
+ for g in gl:
133
+ where.setdefault(g, []).append(tid)
134
+ name = lambda g: guests[g]["name"]
135
+ for g in guests:
136
+ at = where.get(g, [])
137
+ if not at:
138
+ fails.append(f"{name(g)} has no seat")
139
+ elif len(at) > 1:
140
+ fails.append(f"{name(g)} is seated more than once ({', '.join(at)})")
141
+ bad_tables.update(at)
142
+ table_of = {g: at[0] for g, at in where.items() if len(at) == 1}
143
+ for tid, gl in seats.items():
144
+ t = tables[tid]
145
+ if len(gl) > t["capacity"]:
146
+ fails.append(f"table {tid} has {len(gl)} guests for {t['capacity']} seats")
147
+ bad_tables.add(tid)
148
+ for grp in plan["together"]:
149
+ at = {table_of.get(g) for g in grp if g in table_of}
150
+ if len(at) > 1:
151
+ fails.append("must sit together but are split: " + ", ".join(f"{name(g)} ({table_of.get(g, 'unseated')})" for g in grp))
152
+ bad_tables.update(t for t in at if t)
153
+ for a, b in plan["apart"]:
154
+ if a in table_of and table_of.get(a) == table_of.get(b):
155
+ fails.append(f"{name(a)} and {name(b)} must be kept apart but both sit at table {table_of[a]}")
156
+ bad_tables.add(table_of[a])
157
+ for g, tid in table_of.items():
158
+ if "wheelchair" in guests[g]["needs"] and not tables[tid]["accessible"]:
159
+ fails.append(f"{name(g)} uses a wheelchair but table {tid} is not marked accessible")
160
+ bad_tables.add(tid)
161
+ for g, tid in plan["fixed"].items():
162
+ if table_of.get(g) != tid:
163
+ fails.append(f"{name(g)} has a fixed seat at table {tid} but sits at {table_of.get(g, 'no table')}")
164
+ bad_tables.add(tid)
165
+
166
+ biggest = max(t["capacity"] for t in tables.values())
167
+ parties = {}
168
+ for g, info in guests.items():
169
+ if info["party"]:
170
+ parties.setdefault(info["party"], []).append(g)
171
+ for party, members in sorted(parties.items()):
172
+ at = sorted({table_of[g] for g in members if g in table_of})
173
+ if len(at) > 1 and len(members) <= biggest:
174
+ warns.append(f"the {party} party ({len(members)}) is split across tables {', '.join(at)}")
175
+ for tid, gl in seats.items():
176
+ cap = tables[tid]["capacity"]
177
+ if not gl:
178
+ warns.append(f"table {tid} is empty")
179
+ elif len(gl) < cap / 2:
180
+ warns.append(f"table {tid} is under half full ({len(gl)} of {cap})")
181
+ in_group = {g for grp in plan["together"] for g in grp}
182
+ for g, tid in table_of.items():
183
+ p = guests[g]["party"]
184
+ alone = (not p or len(parties.get(p, [])) == 1) and g not in in_group
185
+ others = [o for o in seats[tid] if o != g]
186
+ other_parties = {guests[o]["party"] for o in others}
187
+ if alone and len(others) >= 2 and len(other_parties) == 1 and "" not in other_parties:
188
+ warns.append(f"{name(g)} came alone and is the only guest at table {tid} not in the {other_parties.pop()} party")
189
+ if plan["exits"]:
190
+ for g, tid in table_of.items():
191
+ t = tables[tid]
192
+ if "near exit" in guests[g]["needs"] and t["at"]:
193
+ d = min(math.dist(t["at"], e) for e in plan["exits"])
194
+ if d > exit_distance:
195
+ warns.append(f"{name(g)} needs to be near an exit but table {tid} is {d:.1f} m from the nearest")
196
+ return {"fails": fails, "warns": warns, "bad_tables": sorted(bad_tables), "table_of": table_of,
197
+ "seated": len(table_of), "guests": len(guests),
198
+ "seats": sum(t["capacity"] for t in tables.values())}
199
+
200
+
201
+ def table_rows(plan, r):
202
+ rows = []
203
+ for tid, t in plan["tables"].items():
204
+ gl = plan["seats"][tid]
205
+ diet = {}
206
+ for g in gl:
207
+ d = plan["guests"][g]["dietary"]
208
+ if d:
209
+ diet[d] = diet.get(d, 0) + 1
210
+ rows.append({"table": tid, "shape": t["shape"], "capacity": t["capacity"], "accessible": t["accessible"],
211
+ "seated": len(gl), "guests": [plan["guests"][g]["name"] for g in gl], "dietary": diet,
212
+ "ok": tid not in r["bad_tables"]})
213
+ return rows
214
+
215
+
216
+ def report(plan, r):
217
+ out = [f"{r['seated']} of {r['guests']} guests seated, {len(plan['tables'])} tables, {r['seats']} seats."]
218
+ for row in table_rows(plan, r):
219
+ flags = ("accessible, " if row["accessible"] else "") + f"{row['seated']}/{row['capacity']}"
220
+ diet = ("; " + ", ".join(f"{n} {d}" for d, n in sorted(row["dietary"].items()))) if row["dietary"] else ""
221
+ out.append(f" {row['table']} ({row['shape']}, {flags}{diet}): {', '.join(row['guests']) or '-'}")
222
+ out += [f"FAIL {f}" for f in r["fails"]] + [f"WARN {w}" for w in r["warns"]]
223
+ out.append("PASS" if not r["fails"] else f"{len(r['fails'])} failure(s)")
224
+ return "\n".join(out) + "\n"
225
+
226
+
227
+ def esc(t):
228
+ return str(t).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
229
+
230
+
231
+ def layout(plan):
232
+ """Positions for every table: given ones, or a grid for tables without one."""
233
+ pos, n = {}, 0
234
+ for tid, t in plan["tables"].items():
235
+ if t["at"]:
236
+ pos[tid] = t["at"]
237
+ cols = max(1, math.ceil(math.sqrt(len(plan["tables"]))))
238
+ for tid, t in plan["tables"].items():
239
+ if tid not in pos:
240
+ pos[tid] = (2.5 + 4.0 * (n % cols), 2.5 + 4.0 * (n // cols))
241
+ n += 1
242
+ return pos
243
+
244
+
245
+ def svg(plan, r, scale=40):
246
+ pos = layout(plan)
247
+ radius = lambda t: 0.45 + 0.09 * t["capacity"] # metres, so a 10-seat round is about 1.35 m
248
+ xs = [p[0] for p in pos.values()] + [e[0] for e in plan["exits"]]
249
+ ys = [p[1] for p in pos.values()] + [e[1] for e in plan["exits"]]
250
+ w, d = plan["room"] or (max(xs) + 3, max(ys) + 3)
251
+ W, H = w * scale + 80, d * scale + 110
252
+ P = lambda x, y: (40 + x * scale, 60 + y * scale)
253
+ out = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W:.0f} {H:.0f}" width="{W:.0f}" height="{H:.0f}" '
254
+ 'font-family="Helvetica, Arial, sans-serif">',
255
+ f'<rect width="{W:.0f}" height="{H:.0f}" fill="#ffffff"/>',
256
+ f'<text x="40" y="30" font-size="16" font-weight="bold">Seating plan: {r["seated"]} of {r["guests"]} guests, '
257
+ f'{len(plan["tables"])} tables</text>',
258
+ f'<rect x="40" y="60" width="{w * scale:.0f}" height="{d * scale:.0f}" fill="#fafafa" stroke="#444" stroke-width="2"/>']
259
+ for i, e in enumerate(plan["exits"]):
260
+ x, y = P(*e)
261
+ out.append(f'<rect x="{x - 14:.0f}" y="{y - 6:.0f}" width="28" height="12" fill="#2e7d32"/>'
262
+ f'<text x="{x:.0f}" y="{y + 22:.0f}" font-size="10" text-anchor="middle" fill="#2e7d32">EXIT</text>')
263
+ for tid, t in plan["tables"].items():
264
+ x, y = P(*pos[tid])
265
+ rad = radius(t) * scale
266
+ bad = tid in r["bad_tables"]
267
+ stroke, fill = ("#c62828", "#ffebee") if bad else ("#455a64", "#eceff1")
268
+ if t["shape"] == "round":
269
+ out.append(f'<circle cx="{x:.0f}" cy="{y:.0f}" r="{rad:.0f}" fill="{fill}" stroke="{stroke}" stroke-width="{3 if bad else 1.5}"/>')
270
+ else:
271
+ out.append(f'<rect x="{x - rad * 1.4:.0f}" y="{y - rad * 0.6:.0f}" width="{rad * 2.8:.0f}" height="{rad * 1.2:.0f}" '
272
+ f'fill="{fill}" stroke="{stroke}" stroke-width="{3 if bad else 1.5}"/>')
273
+ gl = plan["seats"][tid]
274
+ mark = " ✗" if bad else ""
275
+ acc = " · accessible" if t["accessible"] else ""
276
+ out.append(f'<text x="{x:.0f}" y="{y - 2:.0f}" font-size="13" font-weight="bold" text-anchor="middle" '
277
+ f'fill="{stroke}">{esc(tid)}{mark}</text>'
278
+ f'<text x="{x:.0f}" y="{y + 13:.0f}" font-size="10" text-anchor="middle" fill="#555">{len(gl)}/{t["capacity"]}{acc}</text>')
279
+ rx, ry = (rad + 8, rad + 8) if t["shape"] == "round" else (rad * 1.4 + 10, rad * 0.6 + 10)
280
+ for k, g in enumerate(gl):
281
+ a = -math.pi / 2 + 2 * math.pi * k / max(len(gl), 1)
282
+ gx, gy = x + rx * math.cos(a), y + ry * math.sin(a)
283
+ anchor = "start" if math.cos(a) > 0.2 else ("end" if math.cos(a) < -0.2 else "middle")
284
+ out.append(f'<text x="{gx:.0f}" y="{gy + 3:.0f}" font-size="9" text-anchor="{anchor}" fill="#222">'
285
+ f'{esc(plan["guests"][g]["name"])}</text>')
286
+ ly = 60 + d * scale + 28
287
+ status = "PASS: every rule met" if not r["fails"] else f"{len(r['fails'])} rule(s) broken: tables marked ✗ in red"
288
+ out.append(f'<text x="40" y="{ly:.0f}" font-size="12" fill="{"#2e7d32" if not r["fails"] else "#c62828"}">{esc(status)}</text>')
289
+ out.append(f'<text x="40" y="{ly + 18:.0f}" font-size="10" fill="#666">Positions in metres, '
290
+ f'1 m = {scale} px · names run clockwise from the top of each table</text>')
291
+ out.append("</svg>")
292
+ return "\n".join(out) + "\n"
293
+
294
+
295
+ def main(argv=None):
296
+ ap = argparse.ArgumentParser(description="Check a seating plan against its rules and draw the room.")
297
+ ap.add_argument("seating")
298
+ ap.add_argument("--svg", help="write the room plan here")
299
+ ap.add_argument("--json", action="store_true", help="print the result as JSON")
300
+ ap.add_argument("--exit-distance", type=float, default=8.0,
301
+ help="metres from an exit that counts as near one (default 8)")
302
+ args = ap.parse_args(argv)
303
+ try:
304
+ with open(args.seating, encoding="utf-8") as fh:
305
+ plan = load(json.load(fh))
306
+ except (OSError, ValueError) as e: # BadInput and JSONDecodeError are ValueErrors
307
+ print(f"FAIL cannot read {args.seating}: {e}")
308
+ return 2
309
+ r = check(plan, args.exit_distance)
310
+ if args.json:
311
+ print(json.dumps({"pass": not r["fails"], "fails": r["fails"], "warns": r["warns"],
312
+ "seated": r["seated"], "guests": r["guests"], "tables": table_rows(plan, r)}, indent=1))
313
+ else:
314
+ print(report(plan, r), end="")
315
+ if args.svg:
316
+ with open(args.svg, "w", encoding="utf-8") as fh:
317
+ fh.write(svg(plan, r))
318
+ if not args.json:
319
+ print(f"\nWrote {args.svg}")
320
+ return 1 if r["fails"] else 0
321
+
322
+
323
+ if __name__ == "__main__":
324
+ sys.exit(main())
@@ -0,0 +1,28 @@
1
+ {
2
+ "room": {"width": 14, "depth": 10},
3
+ "exits": [[0, 5], [14, 9]],
4
+ "tables": [
5
+ {"id": "Head", "capacity": 8, "shape": "rect", "at": [7, 1.5], "accessible": true},
6
+ {"id": "T1", "capacity": 10, "shape": "round", "at": [3, 5], "accessible": true},
7
+ {"id": "T2", "capacity": 10, "shape": "round", "at": [7, 5.5]},
8
+ {"id": "T3", "capacity": 10, "shape": "round", "at": [11, 5]}
9
+ ],
10
+ "guests": [
11
+ {"id": "g1", "name": "Mira Osei", "party": "Couple"},
12
+ {"id": "g2", "name": "Tom Hale", "party": "Couple"},
13
+ {"id": "g3", "name": "Ruth Osei", "party": "Osei", "needs": ["wheelchair"], "dietary": "vegetarian"},
14
+ {"id": "g4", "name": "Kwame Osei", "party": "Osei"},
15
+ {"id": "g5", "name": "Dana Hale", "party": "Hale", "needs": ["near exit"]},
16
+ {"id": "g6", "name": "Pete Hale", "party": "Hale", "dietary": "gluten-free"},
17
+ {"id": "g7", "name": "Lena Brook", "party": ""}
18
+ ],
19
+ "together": [["g3", "g4"]],
20
+ "apart": [["g4", "g6"]],
21
+ "fixed": {"g1": "Head", "g2": "Head"},
22
+ "assignments": {
23
+ "Head": ["g1", "g2"],
24
+ "T1": ["g3", "g4", "g7"],
25
+ "T2": [],
26
+ "T3": ["g5", "g6"]
27
+ }
28
+ }
@@ -57,6 +57,11 @@
57
57
  "you_get": "A blueprint table and Mermaid diagram with fail points, waits, pains traced to causes, and 2–3 ranked fixes.",
58
58
  "decide": "Which fix becomes committed work."
59
59
  },
60
+ {
61
+ "skill": "locale-check",
62
+ "you_do": "Hand over the source strings and the translated files before release.",
63
+ "you_get": "Whether the release is safe to ship, and each failure by locale with the corrected string or a note for the translator."
64
+ },
60
65
  {
61
66
  "skill": "doc-critique",
62
67
  "you_do": "Run it on any of the specs before you share them.",