@litfamily/litopencode 1.0.15 → 1.0.17

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 (118) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README-Ko-KR.md +11 -11
  3. package/README.md +11 -11
  4. package/dist/activation-routing.js +1 -1
  5. package/dist/auto-handoff.js +34 -1
  6. package/dist/features.js +2 -2
  7. package/dist/skills.js +6 -4
  8. package/docs/assets/readme/badge-version.svg +1 -1
  9. package/docs/reference-Ko-KR.md +1 -1
  10. package/package.json +1 -1
  11. package/skills/frontend-ui-ux/data/THIRD-PARTY-NOTICE.txt +1 -3
  12. package/skills/lit-docx/SKILL.md +80 -19
  13. package/skills/lit-docx/references/components.md +253 -0
  14. package/skills/lit-docx/references/direction-step.md +234 -0
  15. package/skills/lit-docx/references/examples/01-report-ko.md +160 -0
  16. package/skills/lit-docx/references/examples/02-brief-ko.md +58 -0
  17. package/skills/lit-docx/references/examples/03-manual-ko.md +107 -0
  18. package/skills/lit-docx/references/examples/04-proposal-ko.md +93 -0
  19. package/skills/lit-docx/references/examples/05-memo-ko.md +42 -0
  20. package/skills/lit-docx/references/examples/06-journal-en.md +84 -0
  21. package/skills/lit-docx/references/examples/07-report-en.md +137 -0
  22. package/skills/lit-docx/references/examples/08-journal-ko.md +93 -0
  23. package/skills/lit-docx/references/examples/09-brief-en.md +64 -0
  24. package/skills/lit-docx/references/page-composition.md +306 -0
  25. package/skills/lit-docx/references/tonalities/brief.md +104 -0
  26. package/skills/lit-docx/references/tonalities/journal.md +101 -0
  27. package/skills/lit-docx/references/tonalities/manual.md +111 -0
  28. package/skills/lit-docx/references/tonalities/memo.md +105 -0
  29. package/skills/lit-docx/references/tonalities/proposal.md +101 -0
  30. package/skills/lit-docx/references/tonalities/report.md +126 -0
  31. package/skills/lit-docx/run.py +2 -1
  32. package/skills/lit-docx/scripts/convert_md_to_docx.py +384 -67
  33. package/skills/lit-docx/scripts/craft_docx.py +204 -1
  34. package/skills/lit-docx/scripts/docx_design.py +2445 -0
  35. package/skills/lit-docx/scripts/docx_layout.py +959 -0
  36. package/skills/lit-docx/scripts/slop_lint.py +23 -6
  37. package/skills/lit-docx/templates/tonalities/brief.yaml +29 -0
  38. package/skills/lit-docx/templates/tonalities/journal.yaml +26 -0
  39. package/skills/lit-docx/templates/tonalities/manual.yaml +27 -0
  40. package/skills/lit-docx/templates/tonalities/memo.yaml +27 -0
  41. package/skills/lit-docx/templates/tonalities/proposal.yaml +27 -0
  42. package/skills/lit-docx/templates/tonalities/report.yaml +27 -0
  43. package/skills/lit-pptx/NOTICE.md +1 -1
  44. package/skills/lit-pptx/SKILL.md +108 -19
  45. package/skills/lit-pptx/pretendard-font/public/static/Pretendard-Bold.otf +0 -0
  46. package/skills/lit-pptx/pretendard-font/public/static/Pretendard-Regular.otf +0 -0
  47. package/skills/lit-pptx/references/density-and-fill.md +283 -0
  48. package/skills/lit-pptx/references/direction-step.md +249 -0
  49. package/skills/lit-pptx/references/examples/01-ops-review-ledger-ko.md +173 -0
  50. package/skills/lit-pptx/references/examples/02-launch-pitch-signal-en.md +153 -0
  51. package/skills/lit-pptx/references/examples/03-field-survey-atlas-ko.md +120 -0
  52. package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +132 -0
  53. package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +162 -0
  54. package/skills/lit-pptx/references/examples/06-policy-briefing-gazette-ko.md +176 -0
  55. package/skills/lit-pptx/references/examples/07-annual-story-studio-en.md +131 -0
  56. package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +182 -0
  57. package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +147 -0
  58. package/skills/lit-pptx/references/examples/10-investor-pitch-signal-ko.md +136 -0
  59. package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +177 -0
  60. package/skills/lit-pptx/references/examples/12-exhibition-proposal-studio-ko.md +149 -0
  61. package/skills/lit-pptx/references/layout-families.md +470 -0
  62. package/skills/lit-pptx/references/title-treatments.md +271 -0
  63. package/skills/lit-pptx/references/tonalities/atlas.md +132 -0
  64. package/skills/lit-pptx/references/tonalities/chalk.md +132 -0
  65. package/skills/lit-pptx/references/tonalities/gazette.md +137 -0
  66. package/skills/lit-pptx/references/tonalities/ledger.md +151 -0
  67. package/skills/lit-pptx/references/tonalities/night.md +148 -0
  68. package/skills/lit-pptx/references/tonalities/paper.md +136 -0
  69. package/skills/lit-pptx/references/tonalities/signal.md +148 -0
  70. package/skills/lit-pptx/references/tonalities/studio.md +149 -0
  71. package/skills/lit-pptx/scripts/compile-deck.js +68 -21
  72. package/skills/lit-pptx/scripts/craft-thresholds.json +45 -1
  73. package/skills/lit-pptx/scripts/craft_extras.py +37 -8
  74. package/skills/lit-pptx/scripts/deck_output.py +654 -0
  75. package/skills/lit-pptx/scripts/embed_fonts.py +2 -2
  76. package/skills/lit-pptx/scripts/layout_inventory.py +35 -2
  77. package/skills/lit-pptx/scripts/lib/directive-parser.js +2 -2
  78. package/skills/lit-pptx/scripts/lib/font-map.js +4 -4
  79. package/skills/lit-pptx/scripts/lib/grid-resolver.js +521 -0
  80. package/skills/lit-pptx/scripts/lib/layout-resolver.js +403 -21
  81. package/skills/lit-pptx/scripts/lib/markdown-loader.js +6 -4
  82. package/skills/lit-pptx/scripts/lib/render-adapter-html.js +3 -9
  83. package/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +500 -113
  84. package/skills/lit-pptx/scripts/lib/render-pack.js +3558 -0
  85. package/skills/lit-pptx/scripts/lib/slide-ast.js +55 -24
  86. package/skills/lit-pptx/scripts/lib/spec-validator.js +2 -2
  87. package/skills/lit-pptx/scripts/lib/template-registry.js +335 -11
  88. package/skills/lit-pptx/scripts/qa_deck.py +48 -13
  89. package/skills/lit-pptx/scripts/validate_pptx.py +17 -5
  90. package/skills/lit-pptx/specs/template-enrollment-contract.md +4 -4
  91. package/skills/lit-pptx/templates/boilerplate_professional.css +13 -13
  92. package/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +1 -1
  93. package/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +5 -2
  94. package/skills/lit-pptx/templates/enrolled/AZURE-PRO/capabilities.yaml +20 -20
  95. package/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +1 -11
  96. package/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +10 -7
  97. package/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/capabilities.yaml +17 -17
  98. package/skills/lit-pptx/templates/enrolled/BOILERPLATE-PRETENDARD/template.yaml +8 -8
  99. package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +105 -0
  100. package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +110 -0
  101. package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +106 -0
  102. package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +107 -0
  103. package/skills/lit-pptx/templates/tonalities/night/pack.yaml +106 -0
  104. package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +111 -0
  105. package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +107 -0
  106. package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +105 -0
  107. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +1 -1
  108. package/skills/lit-typographic-motion/engine/stage/capture.mjs +2 -2
  109. package/skills/lit-typographic-motion/engine/treatment.mjs +1 -1
  110. package/skills/lit-typographic-motion/fonts/pins.json +2 -2
  111. package/skills/lit-typographic-motion/gate.mjs +1 -1
  112. package/skills/lit-typographic-motion/references/stage.md +1 -1
  113. package/skills/lit-typographic-motion/references/style-bibles.md +2 -2
  114. package/skills/lit-typographic-motion/references/treatment.md +1 -1
  115. package/skills/lit-typographic-motion/references/type-craft.md +3 -3
  116. package/skills/managed-skill-manifest.json +288 -44
  117. package/skills/lit-pptx/pretendard-font/public/static/LitOpenCodeSans-Bold.otf +0 -0
  118. package/skills/lit-pptx/pretendard-font/public/static/LitOpenCodeSans-Regular.otf +0 -0
@@ -0,0 +1,149 @@
1
+ # Studio
2
+
3
+ Studio is a magazine spread. The 12-column grid is visible as fine column hairlines, titles live in
4
+ a side rail or open a page at large size, splits are asymmetric, real numbers are set prominently on
5
+ tinted panels, and a dark ochre accent marks the one thing to look at. Pack file:
6
+ `templates/tonalities/studio/pack.yaml`.
7
+
8
+ ## Use it for
9
+
10
+ - Annual reviews and impact reports told as a story with photographs and a few charts.
11
+ - Exhibition, programme and place proposals (전시 기획, 공간 제안).
12
+ - Public lectures and editorial narratives where images and figures alternate.
13
+
14
+ ## Wrong for
15
+
16
+ - Dense data reviews: editorial white space starves tables. Studio lists no table family.
17
+ - Korean reading briefs: gazette.
18
+ - Decks with no photographs and few numbers: the editorial grid then frames nothing.
19
+
20
+ ## Palette roles
21
+
22
+ | Role | Colour | Used for |
23
+ |---|---|---|
24
+ | ground / surface | white / warm pale grey `#F2F2F0` | background, rail, number tint |
25
+ | ink / ink-muted | near-black `#141414` / grey `#525252` | text |
26
+ | line | warm grey `#D2D2CE` | column hairlines, rules |
27
+ | accent | dark ochre `#8A5A00` | the one thing to look at |
28
+ | accent-deep / accent-tint | deep brown `#4D3200` / pale sand `#F5ECD9` | deep text / number panel tint |
29
+ | field / on-field | near-black / white | closing field, caption panel |
30
+ | positive / negative | green `#1E7A3F` / red `#B3261E` | signed deltas |
31
+ | series | ochre, ink, grey, slate blue | charts |
32
+
33
+ ## Type
34
+
35
+ Pretendard throughout. Compact ramp at density 10; display tracks tight (−2). Figures proportional.
36
+
37
+ ## Dials
38
+
39
+ Density 10, variance 8: up to five treatments, all five of the pack's. Studio decks show the most
40
+ title variety of the eight.
41
+
42
+ ## Title treatments by role
43
+
44
+ | Role | Default treatment |
45
+ |---|---|
46
+ | content | side-rail |
47
+ | data | top-plain-large |
48
+ | data-takeaway | side-rail |
49
+ | sequence | side-rail |
50
+ | image | side-rail |
51
+ | statement | statement |
52
+ | reference | side-rail |
53
+ | definition | side-rail |
54
+
55
+ An image slide takes the side rail: the picture across columns 5-12 and its facts and source in the
56
+ rail under the title, an asymmetric split that differs from the bottom title Atlas or Paper would
57
+ give the same slide.
58
+
59
+ Treatments: side-rail, statement, top-plain-large, bottom-anchor, overlay. The statement is drawn
60
+ `rules`: the sentence on the open page between a hairline above it and one under its support line
61
+ (the earlier offset rail beside it stood empty). Studio has no
62
+ kicker-numeral; sequences take the side rail.
63
+
64
+ ## Families and variants
65
+
66
+ Families: asymmetric-feature, big-number, image-split, image-full, photo-grid, text-two-column,
67
+ chart-insight, kpi-row, timeline, comparison, statement.
68
+
69
+ Covers: cover-split-image (takes the first image in the deck), cover-rail, cover-numeral. Sections:
70
+ section-numeral, section-rail. Closings: closing-contact-split, closing-summary-list.
71
+
72
+ Display devices: no cover device of its own (covers draw their variant plainly), statement `rules`,
73
+ big number on a tint, closing step in a `band`.
74
+
75
+ ## Tables and charts
76
+
77
+ - Tables `open`: no header fill, one rule under the header, no banding, numbers right-aligned,
78
+ totals bold. Studio lists no table family, so tables appear only as KPI rows and closing asks.
79
+ - Charts: no gridlines, direct labels, the accent on one series, annotation on. Column hairlines run
80
+ in the gutter beside a chart's takeaways.
81
+ - Images may bleed and crop; no frame.
82
+
83
+ ## Decoration vocabulary
84
+
85
+ column-hairlines, numeral, rail-fill, colour-field, caption-band.
86
+
87
+ ## Fill behaviour
88
+
89
+ `fill-order`: change-family, step-up (off at density 10), anchor-visual, distribute.
90
+
91
+ ## Writing for Studio
92
+
93
+ - Alternate: an image slide, a numbers slide, a text slide. Studio's variance rewards a deck whose
94
+ slides genuinely differ in job.
95
+ - `text-two-column` needs four to six groups with two or three sub-points each to fill both columns
96
+ at density 10.
97
+ - Large numerals must be real figures with a basis row; the engine caps them at title size anyway.
98
+ - Timeline events get a sixth of the width each; keep them short (`Homework help in all 25 rooms`
99
+ rather than a full clause).
100
+ - When OF-111 reports a repeated layout, the usual fix is a `title:` key on one repeated slide
101
+ (`top-plain-large` on a chart slide, `bottom-anchor` on a timeline, `side-rail` on a KPI row).
102
+
103
+ ## Avoid
104
+
105
+ - Large numerals that are not real figures.
106
+ - Column hairlines through text.
107
+ - Two side-rail slides with the same split in a row.
108
+ - Centred body text.
109
+
110
+ ## A sample fragment
111
+
112
+ ```text
113
+ ---
114
+ tonality: studio
115
+ title: Harbour promenade proposal 2027
116
+ date: 2026-10-06
117
+ notice: Sample data and stand-in photos
118
+ ---
119
+ layout: asymmetric-feature
120
+
121
+ ## The promenade today
122
+
123
+ ![North pier at dusk, 2026](assets/example-b.png)
124
+
125
+ - **Length** 2.4 km from the ferry terminal to the old customs house
126
+ - **Visitors** about 9,000 on a summer weekend day, 1,200 on a winter weekday
127
+ - **Seating** 46 benches, none shaded, none with backs
128
+ - **Lighting** 31% of the route under 10 lx after dark
129
+ - Source: Example City visitor counts and lighting survey, 2026
130
+ ---
131
+ layout: kpi-row
132
+ title: side-rail
133
+
134
+ ## What the first phase delivers
135
+
136
+ | Shaded seats | Lit route | Trees | Cost |
137
+ |---|---|---|---|
138
+ | 180 | 100% | 64 | 3.8M EUR |
139
+ | was 0 | above 10 lx | native species | phase 1 of 2 |
140
+
141
+ - Phase 1 covers the 1.1 km between the terminal and the market hall
142
+ - Source: Example City outline design, September 2026
143
+ ```
144
+
145
+ ## Examples
146
+
147
+ - `examples/07-annual-story-studio-en.md`: English reading-rooms annual review, 9 slides.
148
+ - `examples/12-exhibition-proposal-studio-ko.md`: Korean architecture photo exhibition proposal,
149
+ 9 slides.
@@ -86,8 +86,8 @@ function compileDeck(source) {
86
86
  // registering the file and reading its internal name, which is the failure this
87
87
  // export exists to prevent.
88
88
  const BUNDLED_FONTS = [
89
- { match: /LitOpenCode Sans/i, dir: "pretendard-font/public/static",
90
- files: ["LitOpenCodeSans-Regular.otf", "LitOpenCodeSans-Bold.otf"] },
89
+ { match: /Pretendard/i, dir: "pretendard-font/public/static",
90
+ files: ["Pretendard-Regular.otf", "Pretendard-Bold.otf"] },
91
91
  { match: /에이투지체/, dir: "fonts/a2z-font",
92
92
  files: ["A2Z-Regular.otf", "A2Z-Bold.otf"] },
93
93
  ];
@@ -143,11 +143,23 @@ async function resolveAndRender(ast, templateName, outputs, options = {}) {
143
143
  throw new Error("layout-resolver module not available. Cannot resolve layouts.");
144
144
  }
145
145
 
146
- const templateObj = templateRegistry.loadTemplate(templateName);
146
+ // A tonality (flag or frontmatter) is a design direction read from a pack; without one the named
147
+ // template loads as a legacy pack and keeps its own look.
148
+ const meta = (ast.deck && ast.deck.metadata) || {};
149
+ const tonality = options.tonality || meta.tonality || null;
150
+ const templateObj = tonality
151
+ ? templateRegistry.loadTonality(tonality, { density: meta.density, variance: meta.variance, canvas: meta.canvas, faces: options.faces || meta.faces })
152
+ : templateRegistry.loadTemplate(templateName);
153
+ if (tonality) {
154
+ const d = templateObj.pack.dials;
155
+ console.log(`Tonality ${templateObj.pack.id} · density ${d.density} · variance ${d.variance} · ${d.canvas} ${templateObj.pack.grid.name} grid` +
156
+ (templateName && templateName !== tonality ? ` (template ${templateName} not used)` : ""));
157
+ }
158
+ const can = (capability) => templateRegistry.hasCapability(templateObj, capability);
147
159
 
148
- // Font override (azure-style templates): swap the whole font system regardless
149
- // of which template was chosen. Applied before accent (independent of colour).
150
- if (options.font && templateObj.template && templateObj.template.render_style === "azure") {
160
+ // Font override (templates with the font-swap capability): swap the whole font system
161
+ // regardless of which template was chosen. Applied before accent (independent of colour).
162
+ if (options.font && can("font-swap")) {
151
163
  const { applyFont, KEYS } = require("./lib/font-map");
152
164
  if (KEYS.includes(options.font)) {
153
165
  applyFont(templateObj, options.font);
@@ -158,10 +170,10 @@ async function resolveAndRender(ast, templateName, outputs, options = {}) {
158
170
  const resolved = layoutResolver.resolve(ast, templateObj);
159
171
  if (options.sourceDir) resolved.sourceDir = options.sourceDir;
160
172
 
161
- // Custom accent recolor (azure-style templates only): derive a WCAG-safe
173
+ // Custom accent recolor (templates with the recolor capability): derive a WCAG-safe
162
174
  // palette from the accent hex, override template tokens + hardcoded decoration
163
175
  // colors, and regenerate the gradient circle PNGs in the new hue.
164
- if (options.accent && templateObj.template && templateObj.template.render_style === "azure") {
176
+ if (options.accent && can("recolor")) {
165
177
  const { derivePalette, defaultToDerivedMap } = require("./lib/derive-palette");
166
178
  const { genGradientAssets } = require("./lib/gen-gradient-assets");
167
179
  const os = require("os");
@@ -199,11 +211,11 @@ async function resolveAndRender(ast, templateName, outputs, options = {}) {
199
211
  console.log(`Accent ${options.accent} → primary ${derived.primary} (white-contrast ${derived._ratios.whiteOnPrimary}:1)`);
200
212
  }
201
213
 
202
- // Blurred gradient-mesh background (azure-style): ambient wash on cover/closing/
214
+ // Blurred gradient-mesh background (mesh-background capability): ambient wash on cover/closing/
203
215
  // content (light) and a deep textured wash on section dividers (dark). Uses the
204
216
  // ACTIVE palette (so it respects --accent). The dark mesh sits ABOVE the base
205
217
  // section fill rect, so qa_deck still resolves the (dark) fill behind white text.
206
- if (options.bg === "mesh" && templateObj.template && templateObj.template.render_style === "azure") {
218
+ if (options.bg === "mesh" && can("mesh-background")) {
207
219
  const { genMeshBg } = require("./lib/gen-bg");
208
220
  const os = require("os");
209
221
  const dims = templateObj.template.dimensions || {};
@@ -277,8 +289,36 @@ if (require.main === module) {
277
289
  }
278
290
  process.exit(0);
279
291
  }
292
+ if (args[0] === "--list-tonalities") {
293
+ const reg = require("./lib/template-registry");
294
+ for (const id of reg.listTonalities()) {
295
+ const p = reg.loadPack(id);
296
+ console.log(`${id} — ${p.intent} [density ${p.density}, variance ${p.variance}; ${p.treatments.join(", ")}]`);
297
+ }
298
+ for (const name of reg.listTemplates()) console.log(`${name} (legacy template)`);
299
+ process.exit(0);
300
+ }
280
301
  if (args[0] === "--list-layouts" && args[1]) {
281
302
  const reg = require("./lib/template-registry");
303
+ if (reg.listTonalities().includes(String(args[1]).toLowerCase())) {
304
+ // A tonality lists its families with the titles each can take there, and its variants.
305
+ const G = require("./lib/grid-resolver");
306
+ const p = reg.loadPack(args[1]);
307
+ console.log(`${p.id} — ${p.intent}`);
308
+ console.log(`treatments: ${p.treatments.join(", ")}`);
309
+ console.log("layout families:");
310
+ for (const f of p["layout-families"]) {
311
+ console.log(` ${f} [${G.familyAllows(f).filter((t) => p.treatments.includes(t)).join(", ")}]`);
312
+ }
313
+ console.log(`covers: ${p.covers.join(", ")}`);
314
+ console.log(`sections: ${p.sections.join(", ")}`);
315
+ console.log(`closings: ${p.closings.join(", ")}`);
316
+ process.exit(0);
317
+ }
318
+ if (!reg.listTemplates().includes(args[1])) {
319
+ console.error(`Unknown tonality or template "${args[1]}". Tonalities: ${reg.listTonalities().join(", ")}; legacy templates: ${reg.listTemplates().join(", ")}`);
320
+ process.exit(1);
321
+ }
282
322
  const t = reg.loadTemplate(args[1]);
283
323
  const layouts = (t.mapping && t.mapping.layouts) || {};
284
324
  const caps = (t.capabilities && t.capabilities.supported_blocks) || {};
@@ -294,13 +334,16 @@ if (require.main === module) {
294
334
  if (args.length === 0) {
295
335
  console.error("Usage:");
296
336
  console.error(" node compile-deck.js --list-templates");
297
- console.error(" node compile-deck.js --list-layouts <template>");
337
+ console.error(" node compile-deck.js --list-tonalities");
338
+ console.error(" node compile-deck.js --list-layouts <template or tonality>");
298
339
  console.error(" node compile-deck.js <input.md> [--ast out.json]");
299
340
  console.error(" node compile-deck.js <input.md> --template <name> --html out.html");
300
341
  console.error(" node compile-deck.js <input.md> --template <name> --pptx out.pptx");
342
+ console.error(" node compile-deck.js <input.md> --tonality <id> --pptx out.pptx (or tonality: in the frontmatter)");
343
+ console.error(" node compile-deck.js <input.md> --tonality <id> --faces a2z --pptx out.pptx (A2Z faces on request; Pretendard by default)");
301
344
  console.error(" node compile-deck.js <input.md> --template <name> --ast out.json --html out.html --pptx out.pptx");
302
- console.error(" node compile-deck.js <input.md> --template <name> --pptx out.pptx --boilerplate");
303
345
  console.error(" node compile-deck.js <input.md> --template <name> --export-viz-context ctx.json");
346
+ console.error(" node compile-deck.js <input.md> --template <name> --pptx out.pptx --boilerplate");
304
347
  process.exit(1);
305
348
  }
306
349
 
@@ -309,12 +352,14 @@ if (require.main === module) {
309
352
  let templateName = null;
310
353
  let htmlPath = null;
311
354
  let pptxPath = null;
312
- let boilerplate = false;
313
355
  let embedFonts = false;
356
+ let facesFlag = null;
314
357
  let accent = null;
315
358
  let font = null;
316
359
  let bg = null;
317
360
  let vizContextPath = null;
361
+ let tonalityFlag = null;
362
+ let boilerplate = false;
318
363
 
319
364
  for (let i = 1; i < args.length; i++) {
320
365
  if (args[i] === "--embed-fonts") { embedFonts = true; continue; }
@@ -322,6 +367,8 @@ if (require.main === module) {
322
367
  if (args[i] === "--font" && args[i + 1]) { font = args[i + 1]; i++; continue; }
323
368
  if (args[i] === "--bg" && args[i + 1]) { bg = args[i + 1]; i++; continue; }
324
369
  if (args[i] === "--export-viz-context" && args[i + 1]) { vizContextPath = args[i + 1]; i++; continue; }
370
+ if (args[i] === "--tonality" && args[i + 1]) { tonalityFlag = args[i + 1]; i++; continue; }
371
+ if (args[i] === "--faces" && args[i + 1]) { facesFlag = args[i + 1]; i++; continue; }
325
372
  if (args[i] === "--ast" && args[i + 1]) {
326
373
  astPath = args[i + 1];
327
374
  i++;
@@ -339,9 +386,12 @@ if (require.main === module) {
339
386
  }
340
387
  }
341
388
 
342
- // If --html or --pptx is requested, --template is required
343
- if ((htmlPath || pptxPath || vizContextPath) && !templateName) {
344
- console.error("Error: --template is required when using --html, --pptx or --export-viz-context");
389
+ const source = fs.readFileSync(inputPath, "utf-8");
390
+
391
+ // A render needs a design: --template, --tonality, or a tonality: line in the frontmatter.
392
+ const frontTonality = (source.match(/^---[ \t]*\n([\s\S]*?)\n---/) || ["", ""])[1].match(/^tonality:[ \t]*(\S+)/m);
393
+ if ((htmlPath || pptxPath || vizContextPath) && !templateName && !tonalityFlag && !frontTonality) {
394
+ console.error("Error: --template or --tonality (or tonality: in the frontmatter) is required when using --html, --pptx or --export-viz-context");
345
395
  process.exit(1);
346
396
  }
347
397
 
@@ -350,8 +400,6 @@ if (require.main === module) {
350
400
  console.error("Error: --template is required when using --boilerplate with --pptx");
351
401
  process.exit(1);
352
402
  }
353
-
354
- const source = fs.readFileSync(inputPath, "utf-8");
355
403
  const sourceDir = path.dirname(path.resolve(inputPath));
356
404
 
357
405
  (async () => {
@@ -380,7 +428,7 @@ if (require.main === module) {
380
428
  }
381
429
 
382
430
  // Resolve and render if template + output formats specified
383
- if (templateName && (htmlPath || pptxPath || vizContextPath)) {
431
+ if (htmlPath || pptxPath || vizContextPath) {
384
432
  // When --boilerplate flag is set, use Python boilerplate engine for PPTX
385
433
  if (boilerplate && pptxPath) {
386
434
  // Write AST to a temp file, then invoke Python compile_boilerplate
@@ -403,7 +451,6 @@ if (require.main === module) {
403
451
  // Run Python boilerplate engine for PPTX
404
452
  await new Promise((resolve, reject) => {
405
453
  const pyArgs = [pyScript, tmpAstPath, "--output", pptxPath];
406
- const scriptRoot = path.resolve(scriptDir, "..");
407
454
  execFile("python3", pyArgs, { cwd: scriptDir }, (error, stdout, stderr) => {
408
455
  // Clean up temp file
409
456
  try { fs.unlinkSync(tmpAstPath); } catch (_) {}
@@ -422,7 +469,7 @@ if (require.main === module) {
422
469
  html: htmlPath,
423
470
  pptx: pptxPath,
424
471
  vizContext: vizContextPath,
425
- }, { sourceDir, accent: effectiveAccent, font: effectiveFont, bg: effectiveBg });
472
+ }, { sourceDir, accent: effectiveAccent, font: effectiveFont, bg: effectiveBg, tonality: tonalityFlag, faces: facesFlag });
426
473
  }
427
474
 
428
475
  // Optional: embed bundled fonts into the PPTX for a self-contained deck.
@@ -22,5 +22,49 @@
22
22
  "groupRatioMin": 2,
23
23
  "equalRowToleranceIn": 0.05,
24
24
  "emptyBandMedium": 0.25,
25
- "emptyBandHigh": 0.35
25
+ "emptyBandHigh": 0.35,
26
+ "outputMinSlides": 8,
27
+ "outputMinTreatments": 3,
28
+ "outputVarietyShare": 0.6,
29
+ "outputVarietyCap": 5,
30
+ "outputMaxShare": 0.4,
31
+ "outputBandFloorPt": 486,
32
+ "outputFooterTopPt": 492,
33
+ "outputBandDeckMax": 0.2,
34
+ "outputBandCaps": [
35
+ [
36
+ 2,
37
+ 0.28
38
+ ],
39
+ [
40
+ 4,
41
+ 0.24
42
+ ],
43
+ [
44
+ 6,
45
+ 0.2
46
+ ],
47
+ [
48
+ 8,
49
+ 0.16
50
+ ],
51
+ [
52
+ 10,
53
+ 0.12
54
+ ]
55
+ ],
56
+ "outputGeometryTolPt": 2,
57
+ "outputSizeTolPt": 0.5,
58
+ "outputAnchorTolPt": 1,
59
+ "outputWideBlockShare": 0.7,
60
+ "outputEvenColumns": 0.85,
61
+ "outputRegionFloorGapPt": 12,
62
+ "outputRegionEmptyShare": 0.4,
63
+ "outputSiblingMinDiffer": 1,
64
+ "outputSiblingMinDifferLong": 2,
65
+ "outputSiblingLongDeck": 4,
66
+ "figureGroundLuminanceMax": 0.25,
67
+ "figureNearWhite": 235,
68
+ "figureLightShare": 0.5,
69
+ "figureMinArea": 0.03
26
70
  }
@@ -11,11 +11,15 @@ from pptx import Presentation
11
11
  from pptx.enum.shapes import MSO_SHAPE_TYPE
12
12
  from pptx.enum.text import PP_ALIGN
13
13
 
14
+ from deck_output import check_figures, check_labels, check_output, check_run_ins
15
+
14
16
  T = json.loads(Path(__file__).with_name("craft-thresholds.json").read_text())
15
17
  EMU = 914400
16
18
  NUMERIC = re.compile(r"^[\d\s,.%+\-±()$€£¥₩▲▼]+$")
17
19
  HANGUL = re.compile(r"[가-힣]")
18
20
  EMOJI = re.compile(r"[\U0001F000-\U0001FAFF\u2600-\u27BF]")
21
+ # Families the tonality compiler draws as display pages; every other `family@` name is a content slide.
22
+ DISPLAY_FAMILY = re.compile(r"(?:cover|section)(?:-|$)|(?:statement|quote|closing-statement|image-full)$")
19
23
 
20
24
 
21
25
  def _box(shape):
@@ -62,6 +66,9 @@ def _text_size(shape):
62
66
 
63
67
 
64
68
  def _display(slide):
69
+ family = next((shape.name.split("@", 1)[1] for shape in slide.shapes if shape.name.startswith("family@")), None)
70
+ if family is not None:
71
+ return bool(DISPLAY_FAMILY.match(family))
65
72
  text = [shape for shape in slide.shapes if getattr(shape, "has_text_frame", False) and shape.text.strip()]
66
73
  visual = [shape for shape in slide.shapes if getattr(shape, "has_table", False) or getattr(shape, "has_chart", False)]
67
74
  return bool(text) and max(map(_text_size, text)) >= T["displayPt"] and len(text) <= 2 and not visual
@@ -79,6 +86,12 @@ def _radius(shape):
79
86
  return None
80
87
 
81
88
 
89
+ def _page_furniture(box, width, height, tolerance=0.03):
90
+ """A fill on two or more canvas edges (a band, a rail, a colour field) frames the page, not content."""
91
+ x, y, w, h = box
92
+ return sum([x <= tolerance, y <= tolerance, x + w >= width - tolerance, y + h >= height - tolerance]) >= 2
93
+
94
+
82
95
  def _vertical_gaps(top, bottom, boxes):
83
96
  cursor = top
84
97
  gaps = []
@@ -92,14 +105,16 @@ def _vertical_gaps(top, bottom, boxes):
92
105
  def analyse(path):
93
106
  deck = Presentation(str(path))
94
107
  canvas = deck.slide_width * deck.slide_height
95
- findings = []
108
+ findings = check_output(deck) + check_labels(deck) + check_figures(deck) + check_run_ins(deck)
96
109
 
97
110
  def add(rule, severity, slide, value, threshold, tier="measured", shape=None):
98
111
  findings.append({"rule": rule, "severity": severity, "tier": tier, "slide": slide,
99
112
  "shape": shape, "value": value, "threshold": threshold})
100
113
 
101
114
  for slide_no, slide in enumerate(deck.slides, 1):
102
- shapes = [shape for shape in slide.shapes if isinstance(shape.width, int) and isinstance(shape.height, int)]
115
+ # The sample-data tag is a deliberate label on every slide, not part of the design.
116
+ shapes = [shape for shape in slide.shapes if isinstance(shape.width, int) and isinstance(shape.height, int)
117
+ and not shape.name.startswith("lit-notice")]
103
118
  display = _display(slide)
104
119
  hues = []
105
120
  for index, shape in enumerate(shapes, 1):
@@ -185,10 +200,24 @@ def analyse(path):
185
200
  add("OF-105", "MEDIUM", slide_no, round(inter / intra, 2), T["groupRatioMin"])
186
201
 
187
202
  if not display:
188
- title = max((shape for shape in shapes if getattr(shape, "has_text_frame", False) and shape.text.strip() and _text_size(shape) >= T["displayPt"]), key=_text_size, default=None)
189
- top = (title.top + title.height) / EMU + .1 if title else .8
190
- bottom = deck.slide_height / EMU - .55
191
- content = [_box(shape) for shape in shapes if shape is not title and (getattr(shape, "has_table", False) or getattr(shape, "has_chart", False) or shape.shape_type == MSO_SHAPE_TYPE.PICTURE or (getattr(shape, "has_text_frame", False) and shape.text.strip() and _text_size(shape) > T["captionPt"])) and top <= shape.top / EMU < bottom]
203
+ # The compiler names its title frame; a title in the lower half closes the body from below.
204
+ title = next((shape for shape in shapes if shape.name.startswith("title@")), None) or max(
205
+ (shape for shape in shapes if getattr(shape, "has_text_frame", False) and shape.text.strip() and _text_size(shape) >= T["displayPt"]), key=_text_size, default=None)
206
+ if title is not None and title.top > deck.slide_height / 2:
207
+ top, bottom = .8, title.top / EMU - .1
208
+ else:
209
+ top = (title.top + title.height) / EMU + .1 if title else .8
210
+ bottom = deck.slide_height / EMU - .55
211
+ held = [_box(shape) for shape in shapes if shape is not title and (getattr(shape, "has_table", False) or getattr(shape, "has_chart", False) or shape.shape_type == MSO_SHAPE_TYPE.PICTURE or (getattr(shape, "has_text_frame", False) and shape.text.strip() and _text_size(shape) > T["captionPt"]))]
212
+ # A title in a side rail: the body stands beside the rail from the title's top, and what stands
213
+ # in the rail under the title (criteria labels, takeaways, the source) belongs to the rail.
214
+ if title is not None and title.left < .1 * deck.slide_width and title.width < .35 * deck.slide_width and title.top < deck.slide_height / 4:
215
+ rx, ry, rw, rh = _box(title)
216
+ beside = [box for box in held if box[0] >= rx + rw - .05 and box[1] < bottom]
217
+ under = [box for box in held if box[0] + box[2] <= rx + rw + .05 and box[1] >= ry + rh - .05]
218
+ if beside and len(beside) + len(under) == len([box for box in held if box[1] < bottom]):
219
+ top, held = ry, beside
220
+ content = [box for box in held if box[1] < bottom and box[1] + box[3] > top]
192
221
  if content and bottom > top:
193
222
  gaps = _vertical_gaps(top, bottom, content)
194
223
  fraction = max(gaps) / (bottom - top)
@@ -196,11 +225,11 @@ def analyse(path):
196
225
  add("OF-109", "HIGH" if fraction > T["emptyBandHigh"] else "MEDIUM", slide_no, round(fraction, 3), {"medium": T["emptyBandMedium"], "high": T["emptyBandHigh"]})
197
226
 
198
227
  card_gaps = []
199
- for index, card in enumerate(shapes, 1):
228
+ for index, card in enumerate([] if display else shapes, 1):
200
229
  if _fill(card) is None or card.width * card.height / canvas >= T["fullBleedFraction"]:
201
230
  continue
202
231
  x, y, width, height = _box(card)
203
- if width <= 0 or height <= 0:
232
+ if width <= 0 or height <= 0 or _page_furniture((x, y, width, height), deck.slide_width / EMU, deck.slide_height / EMU):
204
233
  continue
205
234
  frame = getattr(card, "text_frame", None) if getattr(card, "has_text_frame", False) else None
206
235
  top = y + (frame.margin_top / EMU if frame else 0)