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.
Files changed (133) hide show
  1. package/README.md +4 -1
  2. package/catalogue.json +25 -2
  3. package/model-routing.json +20 -0
  4. package/package.json +1 -1
  5. package/packs/civil-architect/pack.json +60 -0
  6. package/packs/civil-architect/skills/floor-plan-layout/SKILL.md +103 -0
  7. package/packs/civil-architect/skills/floor-plan-layout/evals/eval_queries.json +30 -0
  8. package/packs/civil-architect/skills/floor-plan-layout/evals/evals.json +57 -0
  9. package/packs/civil-architect/skills/floor-plan-layout/evals/files/4/adu-programme.md +12 -0
  10. package/packs/civil-architect/skills/floor-plan-layout/examples/ground-floor.md +65 -0
  11. package/packs/civil-architect/skills/floor-plan-layout/examples/ground-floor.svg +135 -0
  12. package/packs/civil-architect/skills/floor-plan-layout/manifest.json +34 -0
  13. package/packs/civil-architect/skills/floor-plan-layout/scripts/plan_check.py +449 -0
  14. package/packs/civil-architect/skills/floor-plan-layout/templates/plan.json +38 -0
  15. package/packs/event-planner/pack.json +61 -0
  16. package/packs/event-planner/skills/run-of-show/SKILL.md +141 -0
  17. package/packs/event-planner/skills/run-of-show/evals/eval_queries.json +30 -0
  18. package/packs/event-planner/skills/run-of-show/evals/evals.json +103 -0
  19. package/packs/event-planner/skills/run-of-show/evals/files/1/agenda.md +16 -0
  20. package/packs/event-planner/skills/run-of-show/evals/files/2/schedule.csv +8 -0
  21. package/packs/event-planner/skills/run-of-show/evals/files/3/notes.txt +3 -0
  22. package/packs/event-planner/skills/run-of-show/evals/files/4/sessions_export.csv +25 -0
  23. package/packs/event-planner/skills/run-of-show/evals/files/4/venue_and_crew.txt +27 -0
  24. package/packs/event-planner/skills/run-of-show/evals/files/5/runsheet_v4.csv +28 -0
  25. package/packs/event-planner/skills/run-of-show/evals/files/5/venue_and_suppliers.txt +22 -0
  26. package/packs/event-planner/skills/run-of-show/evals/files/6/acts_from_booker.csv +26 -0
  27. package/packs/event-planner/skills/run-of-show/evals/files/6/venue_rooms_and_crew.txt +14 -0
  28. package/packs/event-planner/skills/run-of-show/evals/files/7/runsheet_draft_v4.csv +25 -0
  29. package/packs/event-planner/skills/run-of-show/evals/files/7/venue_and_fixed_times.txt +24 -0
  30. package/packs/event-planner/skills/run-of-show/examples/product-launch.md +77 -0
  31. package/packs/event-planner/skills/run-of-show/examples/product-launch.svg +58 -0
  32. package/packs/event-planner/skills/run-of-show/manifest.json +44 -0
  33. package/packs/event-planner/skills/run-of-show/scripts/show_check.py +295 -0
  34. package/packs/event-planner/skills/run-of-show/templates/show.json +15 -0
  35. package/packs/event-planner/skills/seating-plan/SKILL.md +152 -0
  36. package/packs/event-planner/skills/seating-plan/evals/eval_queries.json +30 -0
  37. package/packs/event-planner/skills/seating-plan/evals/evals.json +134 -0
  38. package/packs/event-planner/skills/seating-plan/evals/files/1/guests.csv +31 -0
  39. package/packs/event-planner/skills/seating-plan/evals/files/2/table-plan.txt +8 -0
  40. package/packs/event-planner/skills/seating-plan/evals/files/3/brief.txt +4 -0
  41. package/packs/event-planner/skills/seating-plan/evals/files/3/guests.csv +49 -0
  42. package/packs/event-planner/skills/seating-plan/evals/files/4/rsvp_export.csv +127 -0
  43. package/packs/event-planner/skills/seating-plan/evals/files/4/seating_notes.txt +21 -0
  44. package/packs/event-planner/skills/seating-plan/evals/files/5/organiser_notes.txt +10 -0
  45. package/packs/event-planner/skills/seating-plan/evals/files/5/registrations.csv +93 -0
  46. package/packs/event-planner/skills/seating-plan/evals/files/5/table_plan_v3.csv +94 -0
  47. package/packs/event-planner/skills/seating-plan/evals/files/6/bridgets_notes.txt +30 -0
  48. package/packs/event-planner/skills/seating-plan/evals/files/6/ticket_export.csv +117 -0
  49. package/packs/event-planner/skills/seating-plan/evals/files/6/venue_floorplan_notes.txt +14 -0
  50. package/packs/event-planner/skills/seating-plan/evals/files/7/keep_apart_CONFIDENTIAL.txt +9 -0
  51. package/packs/event-planner/skills/seating-plan/evals/files/7/prom_seating_v3.csv +101 -0
  52. package/packs/event-planner/skills/seating-plan/evals/files/7/ticket_list.csv +101 -0
  53. package/packs/event-planner/skills/seating-plan/evals/files/7/venue_info.txt +16 -0
  54. package/packs/event-planner/skills/seating-plan/evals/files/8/registrations_export.csv +147 -0
  55. package/packs/event-planner/skills/seating-plan/evals/files/8/room_layout.md +22 -0
  56. package/packs/event-planner/skills/seating-plan/evals/files/8/seating_requests.txt +31 -0
  57. package/packs/event-planner/skills/seating-plan/evals/files/9/guest_list.csv +84 -0
  58. package/packs/event-planner/skills/seating-plan/evals/files/9/notes.txt +16 -0
  59. package/packs/event-planner/skills/seating-plan/evals/files/9/table_plan.csv +85 -0
  60. package/packs/event-planner/skills/seating-plan/examples/wedding-dinner.md +61 -0
  61. package/packs/event-planner/skills/seating-plan/examples/wedding-dinner.svg +39 -0
  62. package/packs/event-planner/skills/seating-plan/manifest.json +44 -0
  63. package/packs/event-planner/skills/seating-plan/scripts/seating_check.py +324 -0
  64. package/packs/event-planner/skills/seating-plan/templates/seating.json +28 -0
  65. package/packs/experience-design/pack.json +5 -0
  66. package/packs/experience-design/skills/locale-check/SKILL.md +139 -0
  67. package/packs/experience-design/skills/locale-check/evals/eval_queries.json +38 -0
  68. package/packs/experience-design/skills/locale-check/evals/evals.json +104 -0
  69. package/packs/experience-design/skills/locale-check/evals/files/1/budget.json +1 -0
  70. package/packs/experience-design/skills/locale-check/evals/files/1/locales/ar.json +16 -0
  71. package/packs/experience-design/skills/locale-check/evals/files/1/locales/de.json +12 -0
  72. package/packs/experience-design/skills/locale-check/evals/files/1/locales/en.json +12 -0
  73. package/packs/experience-design/skills/locale-check/evals/files/1/locales/pl.json +12 -0
  74. package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values/strings.xml +12 -0
  75. package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values-es/strings.xml +11 -0
  76. package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values-fr/strings.xml +12 -0
  77. package/packs/experience-design/skills/locale-check/evals/files/3/po/ja.po +20 -0
  78. package/packs/experience-design/skills/locale-check/evals/files/3/po/messages.pot +23 -0
  79. package/packs/experience-design/skills/locale-check/evals/files/3/po/ru.po +22 -0
  80. package/packs/experience-design/skills/locale-check/evals/files/4/budgets.json +15 -0
  81. package/packs/experience-design/skills/locale-check/evals/files/4/locales/ar.json +265 -0
  82. package/packs/experience-design/skills/locale-check/evals/files/4/locales/de.json +265 -0
  83. package/packs/experience-design/skills/locale-check/evals/files/4/locales/en.json +265 -0
  84. package/packs/experience-design/skills/locale-check/evals/files/4/locales/fr.json +265 -0
  85. package/packs/experience-design/skills/locale-check/evals/files/4/locales/pl.json +265 -0
  86. package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values/strings.xml +77 -0
  87. package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values-es/strings.xml +63 -0
  88. package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values-ja/strings.xml +61 -0
  89. package/packs/experience-design/skills/locale-check/evals/files/6/po/cs.po +605 -0
  90. package/packs/experience-design/skills/locale-check/evals/files/6/po/marlin.pot +602 -0
  91. package/packs/experience-design/skills/locale-check/evals/files/6/po/pt_BR.po +602 -0
  92. package/packs/experience-design/skills/locale-check/evals/files/6/po/ru.po +608 -0
  93. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/en.lproj/Localizable.strings +160 -0
  94. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/en.lproj/Localizable.stringsdict +70 -0
  95. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/he.lproj/Localizable.strings +157 -0
  96. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/he.lproj/Localizable.stringsdict +86 -0
  97. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/it.lproj/Localizable.strings +160 -0
  98. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/it.lproj/Localizable.stringsdict +68 -0
  99. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/ko.lproj/Localizable.strings +160 -0
  100. package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/ko.lproj/Localizable.stringsdict +62 -0
  101. package/packs/experience-design/skills/locale-check/examples/settings-release.md +107 -0
  102. package/packs/experience-design/skills/locale-check/manifest.json +45 -0
  103. package/packs/experience-design/skills/locale-check/scripts/locale_check.py +1105 -0
  104. package/packs/operations-manager/pack.json +60 -0
  105. package/packs/operations-manager/skills/shift-roster/SKILL.md +166 -0
  106. package/packs/operations-manager/skills/shift-roster/evals/eval_queries.json +38 -0
  107. package/packs/operations-manager/skills/shift-roster/evals/evals.json +135 -0
  108. package/packs/operations-manager/skills/shift-roster/evals/files/1/shifts.csv +3 -0
  109. package/packs/operations-manager/skills/shift-roster/evals/files/1/staff.csv +8 -0
  110. package/packs/operations-manager/skills/shift-roster/evals/files/2/rota.csv +22 -0
  111. package/packs/operations-manager/skills/shift-roster/evals/files/2/staff.csv +6 -0
  112. package/packs/operations-manager/skills/shift-roster/evals/files/3/team.md +14 -0
  113. package/packs/operations-manager/skills/shift-roster/evals/files/4/availability_export_wk47.csv +23 -0
  114. package/packs/operations-manager/skills/shift-roster/evals/files/4/rota_notes.txt +20 -0
  115. package/packs/operations-manager/skills/shift-roster/evals/files/5/rota_draft_16-29nov.csv +107 -0
  116. package/packs/operations-manager/skills/shift-roster/evals/files/5/staff_list.csv +17 -0
  117. package/packs/operations-manager/skills/shift-roster/evals/files/6/availability_notes.txt +24 -0
  118. package/packs/operations-manager/skills/shift-roster/evals/files/6/peoplehub_staff_export.csv +14 -0
  119. package/packs/operations-manager/skills/shift-roster/evals/files/6/rota_template.csv +25 -0
  120. package/packs/operations-manager/skills/shift-roster/evals/files/7/leave_and_availability.txt +18 -0
  121. package/packs/operations-manager/skills/shift-roster/evals/files/7/rota_wc_30nov.csv +47 -0
  122. package/packs/operations-manager/skills/shift-roster/evals/files/7/staff_list.csv +15 -0
  123. package/packs/operations-manager/skills/shift-roster/evals/files/8/availability_form_responses.csv +22 -0
  124. package/packs/operations-manager/skills/shift-roster/evals/files/8/cover_requirements.csv +15 -0
  125. package/packs/operations-manager/skills/shift-roster/evals/files/8/staff_export.csv +29 -0
  126. package/packs/operations-manager/skills/shift-roster/evals/files/9/leave_and_restrictions.txt +16 -0
  127. package/packs/operations-manager/skills/shift-roster/evals/files/9/rota_export_07-13Dec.csv +85 -0
  128. package/packs/operations-manager/skills/shift-roster/evals/files/9/staff_list.csv +27 -0
  129. package/packs/operations-manager/skills/shift-roster/examples/cafe-week.md +72 -0
  130. package/packs/operations-manager/skills/shift-roster/examples/cafe-week.svg +123 -0
  131. package/packs/operations-manager/skills/shift-roster/manifest.json +34 -0
  132. package/packs/operations-manager/skills/shift-roster/scripts/roster_check.py +633 -0
  133. package/packs/operations-manager/skills/shift-roster/templates/roster.json +22 -0
@@ -0,0 +1,84 @@
1
+ Name,Party,RSVP,Access needs
2
+ Maureen Ashdown,Maureen & Clive,Yes,
3
+ Clive Ashdown,Maureen & Clive,Yes,
4
+ Graham Petherick,,Yes,
5
+ Lorraine Petherick,,Yes,
6
+ Tom Ashdown,Tom & Leanne,Yes,
7
+ Becky Price,Becky & Gareth,Yes,
8
+ Leanne Ashdown,Tom & Leanne,Yes,
9
+ Gareth Price,Becky & Gareth,Yes,
10
+ Joan Fairbrook,,Yes,Wheelchair
11
+ Priyanka Yelverton,Priyanka & Yara,Yes,
12
+ Yara Okwuosa,Priyanka & Yara,Yes,
13
+ Amaya Larkspur,Amaya & Oren,Yes,
14
+ Oren Thistlewood,Amaya & Oren,Yes,
15
+ Tobias Dimitriou,Tobias & Wendell,Yes,
16
+ Wendell Thistlewood,Tobias & Wendell,Yes,
17
+ Isolde Kowalczyk,Isolde & Sunniva,Yes,Wheelchair
18
+ Sunniva Bellweather,Isolde & Sunniva,Yes,
19
+ Emeka Haugen,Emeka & Cosima,Yes,
20
+ Cosima Rendall,Emeka & Cosima,Yes,
21
+ Tova Quintero,Tova & Oren,Yes,
22
+ Oren Ravensworth,Tova & Oren,Yes,
23
+ Gideon Vexley,Gideon & Isolde,Yes,
24
+ Isolde Rendall,Gideon & Isolde,Yes,
25
+ Lysander Pilbeam,Lysander & Xanthe,Yes,
26
+ Xanthe Dunmore,Lysander & Xanthe,Yes,
27
+ Peregrine Okwuosa,Peregrine & Zubair,Yes,
28
+ Zubair Dimitriou,Peregrine & Zubair,Yes,
29
+ Marguerite Haugen,Marguerite & Thaddeus,Yes,
30
+ Thaddeus Gallagher,Marguerite & Thaddeus,Yes,
31
+ Ginevra Pilbeam,Ginevra & Zelda,Yes,
32
+ Zelda Kilbride,Ginevra & Zelda,Yes,
33
+ Ottoline Calloway,Ottoline & Keziah,Yes,
34
+ Keziah Iwu,Ottoline & Keziah,Yes,
35
+ Valentin Bellweather,Valentin & Wendell,Yes,
36
+ Wendell Nakashima,Valentin & Wendell,Yes,
37
+ Isolde Fairbairn,Isolde & Bram,Yes,
38
+ Bram Inchbald,Isolde & Bram,Yes,
39
+ Zubair Pendragon,Zubair & Delphine,Yes,
40
+ Delphine Vexley,Zubair & Delphine,Yes,
41
+ Zubair Bhattacharya,Zubair & Emeka,Yes,
42
+ Emeka Dimitriou,Zubair & Emeka,Yes,
43
+ Bram Greythorn,Bram & Ottoline,Yes,
44
+ Ottoline Inchbald,Bram & Ottoline,Yes,
45
+ Lysander Thistlewood,Lysander & Gideon,Yes,
46
+ Gideon Glenister,Lysander & Gideon,Yes,
47
+ Anouk Copperfield,Anouk & Hana,Yes,
48
+ Hana Stroud,Anouk & Hana,Yes,
49
+ Keziah Kowalczyk,Keziah & Xanthe,Yes,
50
+ Xanthe Greythorn,Keziah & Xanthe,Yes,
51
+ Barnaby Calloway,Barnaby & Ursula,Yes,
52
+ Ursula Stroud,Barnaby & Ursula,Yes,
53
+ Oren Glenister,Oren & Jethro,Yes,
54
+ Jethro Pilbeam,Oren & Jethro,Yes,
55
+ Ilse Kowalczyk,Kowalczyk family,Yes,
56
+ Vashti Kowalczyk,Kowalczyk family,Yes,
57
+ Rufaro Kowalczyk,Kowalczyk family,Yes,
58
+ Delphine Kowalczyk,Kowalczyk family,Yes,
59
+ Noor Eastwick,Eastwick family,Yes,
60
+ Lysander Eastwick,Eastwick family,Yes,
61
+ Elowen Eastwick,Eastwick family,Yes,
62
+ Gideon Eastwick,Eastwick family,Yes,
63
+ Yusuf Elstree,Elstree family,Yes,
64
+ Anouk Elstree,Elstree family,Yes,
65
+ Thaddeus Elstree,Elstree family,Yes,
66
+ Fintan Elstree,Elstree family,Yes,
67
+ Fintan Rendall,,Yes,Wheelchair
68
+ Lysander Dunmore,,Yes,
69
+ Noor Underhill,,Yes,
70
+ Benedikt Ormsby,,Yes,
71
+ Marguerite Drummond,,Yes,
72
+ Hamish Moreau,,Yes,
73
+ Dashiell Gallagher,,Yes,
74
+ Chidi Mossop,,Yes,
75
+ Vashti Copperfield,,Yes,
76
+ Mireille Nakashima,,Yes,
77
+ Kalinda Gallagher,,Yes,
78
+ Soren Hartigan,,Yes,
79
+ Sunniva Jardine,,Yes,
80
+ Hana Kowalczyk,,Yes,
81
+ Rosamund Gallagher,,Yes,
82
+ Leopold Jardine,,Yes,
83
+ Chidi Kestrel,,Yes,
84
+ Nnamdi Jankowski,,Yes,
@@ -0,0 +1,16 @@
1
+ Maureen's do - The Orangery, Larkmoor Court Hotel - Sat 23 Jan 2027, 7pm
2
+
3
+ Room: top table (Table 1) is a long table against the window, seats 8.
4
+ Tables 2-9 are rounds of 10. Tables 8 and 9 are out on the raised terrace bit -
5
+ three steps up, no ramp, so no wheelchairs out there.
6
+
7
+ Couples and families sit together (see Party column on the guest list).
8
+
9
+ KEEP APART:
10
+ - Graham Petherick and Lorraine Petherick (divorce still going through - do NOT seat together)
11
+ - Noor Underhill and Lysander Dunmore (fell out badly at work, both still coming)
12
+ - Tova Quintero and Thaddeus Gallagher
13
+ - Ilse Kowalczyk and Marguerite Drummond
14
+
15
+ Maureen said she wants on the top table with her: Clive, her mum Joan, Tom and Leanne,
16
+ and "both Graham and Lorraine - they were my first team and I want them both up there with me".
@@ -0,0 +1,85 @@
1
+ Table,Seat,Guest
2
+ 1,1,Joan Fairbrook
3
+ 1,2,Tom Ashdown
4
+ 1,3,Leanne Ashdown
5
+ 1,4,Maureen Ashdown
6
+ 1,5,Clive Ashdown
7
+ 1,6,Graham Petherick
8
+ 1,7,Lorraine Petherick
9
+ 2,1,Zubair Pendragon
10
+ 2,2,Delphine Vexley
11
+ 2,3,Anouk Copperfield
12
+ 2,4,Hana Stroud
13
+ 2,5,Amaya Larkspur
14
+ 2,6,Oren Thistlewood
15
+ 2,7,Chidi Mossop
16
+ 2,8,Hana Kowalczyk
17
+ 2,9,Leopold Jardine
18
+ 2,10,Soren Hartigan
19
+ 2,11,Marguerite Drummond
20
+ 3,1,Isolde Kowalczyk
21
+ 3,2,Sunniva Bellweather
22
+ 3,3,Priyanka Yelverton
23
+ 3,4,Yara Okwuosa
24
+ 3,5,Tobias Dimitriou
25
+ 3,6,Wendell Thistlewood
26
+ 3,7,Sunniva Jardine
27
+ 3,8,Vashti Copperfield
28
+ 3,9,Benedikt Ormsby
29
+ 4,1,Noor Eastwick
30
+ 4,2,Lysander Eastwick
31
+ 4,3,Elowen Eastwick
32
+ 4,4,Gideon Eastwick
33
+ 4,5,Marguerite Haugen
34
+ 4,6,Thaddeus Gallagher
35
+ 4,7,Valentin Bellweather
36
+ 4,8,Wendell Nakashima
37
+ 4,9,Hamish Moreau
38
+ 5,1,Barnaby Calloway
39
+ 5,2,Ursula Stroud
40
+ 5,3,Lysander Thistlewood
41
+ 5,4,Gideon Glenister
42
+ 5,5,Becky Price
43
+ 5,6,Gareth Price
44
+ 5,7,Tova Quintero
45
+ 5,8,Oren Ravensworth
46
+ 5,9,Mireille Nakashima
47
+ 5,10,Rosamund Gallagher
48
+ 6,1,Yusuf Elstree
49
+ 6,2,Anouk Elstree
50
+ 6,3,Thaddeus Elstree
51
+ 6,4,Fintan Elstree
52
+ 6,5,Gideon Vexley
53
+ 6,6,Isolde Rendall
54
+ 6,7,Ottoline Calloway
55
+ 6,8,Keziah Iwu
56
+ 6,9,Hamish Moreau
57
+ 7,1,Lysander Pilbeam
58
+ 7,2,Xanthe Dunmore
59
+ 7,3,Ginevra Pilbeam
60
+ 7,4,Zelda Kilbride
61
+ 7,5,Keziah Kowalczyk
62
+ 7,6,Xanthe Greythorn
63
+ 7,7,Nnamdi Jankowski
64
+ 7,8,Kalinda Gallagher
65
+ 7,9,Dashiell Gallagher
66
+ 7,10,Chidi Kestrel
67
+ 8,1,Oren Glenister
68
+ 8,2,Jethro Pilbeam
69
+ 8,3,Zubair Bhattacharya
70
+ 8,4,Emeka Dimitriou
71
+ 8,5,Emeka Haugen
72
+ 8,6,Cosima Rendall
73
+ 8,7,Isolde Fairbairn
74
+ 8,8,Bram Inchbald
75
+ 8,9,Lysander Dunmore
76
+ 8,10,Noor Underhill
77
+ 9,1,Fintan Rendall
78
+ 9,2,Ilse Kowalczyk
79
+ 9,3,Vashti Kowalczyk
80
+ 9,4,Rufaro Kowalczyk
81
+ 9,5,Delphine Kowalczyk
82
+ 9,6,Bram Greythorn
83
+ 9,7,Ottoline Inchbald
84
+ 9,8,Peregrine Okwuosa
85
+ 9,9,Zubair Dimitriou
@@ -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.2",
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())