slidict 0.4.2 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 829a5806efaa7c29d773818f0683fa3c0525bebfe883bd32261ba29f5a649a5e
4
- data.tar.gz: e45e42541ba0940a883f470f7d03941df71b5825c0c9b93471fe3b1c991b0de5
3
+ metadata.gz: bcb8c90bc5cd08ea0215119e5b04eb6fbe7e9ed44c8ee5f6d5ef2a7aaaa884de
4
+ data.tar.gz: cef002ffa756fa6a30af65df47cd1c157ceb1187cc021b31ced01de41e73f2c1
5
5
  SHA512:
6
- metadata.gz: ff04a4475b51083d1c8ed884ad10f924000bb6c47adb0947d6c365fa3dd81d83eb09b94b280794bed9d2ef1001b559589c9241d92b0ccc005a7969065a814eb1
7
- data.tar.gz: a0385b86027802eb018c4c9bda5fc26ee8030df9eee449879bd5dc299faa32382df90e5b8c707b528c846018d1fa8f28b3861ce299ed8d58f7d5ad125bc29b15
6
+ metadata.gz: fa541a9126446303bf5e4b8ef0e1cc852311e6d1d9d022db1446594e49e76a7a56fd379100c727296c70cdf09c2379b954cb057cdea63902716d1d153573d58f
7
+ data.tar.gz: 9119b310faaa5442a01c716b79e804f9f5f9e90c2d9cd8cb06977905e9d976d45b915e2d1138775ddc36c8306c1528b96a8402e0e3045058d9c66d0c849da15d
data/AGENTS.md CHANGED
@@ -39,3 +39,15 @@ Common types used in this repo:
39
39
  - `test:` adding or correcting tests
40
40
 
41
41
  Keep the summary short and in the imperative mood (e.g. `fix: handle empty topic input`).
42
+
43
+ ## Versioning (SemVer)
44
+
45
+ This project follows [Semantic Versioning](https://semver.org/). For a CLI like slidict, apply these rules:
46
+
47
+ | Change type | Version bump |
48
+ |---|---|
49
+ | Breaking change — existing commands, options, config format, or output parsing breaks | `MAJOR` (`1.x → 2.0.0`) |
50
+ | Backward-compatible addition — new command, new option, new output field | `MINOR` (`1.2.x → 1.3.0`) |
51
+ | Backward-compatible fix — bug fix, internal refactor, perf improvement, dependency update | `PATCH` (`1.2.3 → 1.2.4`) |
52
+
53
+ While the project is at `0.x`, breaking changes may be released as `MINOR` bumps (e.g. `0.3.x → 0.4.0`) following common OSS convention.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2026-07-01
4
+
5
+ - Add data-driven presentation methods with SCQA, PREP, and Pyramid Principle templates.
6
+ - Add `--method`, `list-methods`, and `show-method` CLI support.
7
+
8
+
3
9
  ## [0.1.0] - 2026-06-21
4
10
 
5
11
  - Initial release
data/README.md CHANGED
@@ -225,3 +225,127 @@ We optimize for communication, not decoration.
225
225
  ## License
226
226
 
227
227
  MIT
228
+
229
+ ## Presentation methods
230
+
231
+ Slidict supports data-driven presentation methods. A method defines the narrative
232
+ technique, slide roles, AI instructions, and review checklist in YAML, while the Ruby
233
+ gem only loads, validates, and applies that data.
234
+
235
+ ```bash
236
+ bin/slidict new --method prep --topic "New onboarding flow"
237
+ bin/slidict --method scqa --topic "Database migration plan"
238
+ bin/slidict --method pyramid --topic "FY roadmap recommendation"
239
+ bin/slidict list-methods
240
+ bin/slidict show-method scqa
241
+ ```
242
+
243
+ Built-in methods currently include SCQA, PREP, and Pyramid Principle.
244
+
245
+ ### Method architecture
246
+
247
+ The method system is intentionally data-first:
248
+
249
+ 1. YAML files live in `data/slidict/methods/`.
250
+ 2. `Slidict::PresentationMethodRegistry` discovers built-in files and plugin-provided files.
251
+ 3. `Slidict::PresentationMethod` validates each YAML document and maps it to Ruby objects.
252
+ 4. `Slidict::Deck` uses the method's slide roles as the built-in non-LLM outline.
253
+ 5. `Slidict::Llm::Client` injects the method description, slide roles, and AI instructions into the generation prompt.
254
+
255
+ This keeps contribution review small: a new built-in method is usually one YAML file and no Ruby code.
256
+
257
+ ### Directory structure
258
+
259
+ ```text
260
+ data/slidict/methods/
261
+ prep.yml
262
+ pyramid.yml
263
+ scqa.yml
264
+ lib/slidict/presentation_method.rb
265
+ ```
266
+
267
+ For 100+ methods, keep one method per file and use stable lowercase IDs such as
268
+ `scqa`, `prep`, `pyramid`, or `problem-solution`. Categories are free-form labels
269
+ that can later drive filtering without changing the file layout.
270
+
271
+ ### YAML schema
272
+
273
+ Each method file is a single YAML mapping:
274
+
275
+ ```yaml
276
+ id: scqa
277
+ name: SCQA
278
+ category: narrative
279
+ description: Situation, Complication, Question, Answer structure for building a clear business narrative.
280
+ locale: en
281
+ suitable_for:
282
+ - Strategy updates
283
+ - Problem-solving proposals
284
+ slides:
285
+ - title: Situation
286
+ role: Establish the shared context the audience already recognizes.
287
+ instructions: Describe the current state with concrete facts.
288
+ ai_instructions:
289
+ - Use the SCQA sequence exactly.
290
+ review_checklist:
291
+ - Does the situation establish common ground?
292
+ references:
293
+ - title: The Pyramid Principle
294
+ author: Barbara Minto
295
+ ```
296
+
297
+ Required fields are `id`, `name`, `category`, `description`, `suitable_for`,
298
+ `slides`, `ai_instructions`, and `review_checklist`. `references` and `locale` are
299
+ optional; `locale` defaults to `en`. Every slide entry requires `title`, `role`, and
300
+ `instructions`.
301
+
302
+ ### Ruby class design
303
+
304
+ - `Slidict::PresentationMethod` represents one method definition.
305
+ - `Slidict::MethodSlide` represents each slide role.
306
+ - `Slidict::PresentationMethodRegistry` loads built-in YAML files and plugin YAML files.
307
+ - `Slidict::Deck` accepts an optional `presentation_method` and turns method slide roles into a local template when `--no-llm` is used or no LLM is configured.
308
+
309
+ Validation happens at load time with `YAML.safe_load_file`. Slidict checks required
310
+ fields, requires lowercase method IDs (`a-z`, `0-9`, and `-`), and verifies that each
311
+ slide role has the required fields.
312
+
313
+ ### CLI design
314
+
315
+ - `--method ID` applies a method to generated slides.
316
+ - `list-methods` prints every discovered method.
317
+ - `show-method ID` prints a method's description, use cases, slide roles, and review checklist.
318
+
319
+ ### Plugin design
320
+
321
+ External gems can distribute methods without changing Slidict itself. A plugin gem only
322
+ needs to place YAML files under `lib/slidict/methods/*.yml` so RubyGems can expose them
323
+ through `Gem.find_files`.
324
+
325
+ Example plugin layout:
326
+
327
+ ```text
328
+ slidict-methods-product/
329
+ lib/slidict/methods/product-demo.yml
330
+ slidict-methods-product.gemspec
331
+ ```
332
+
333
+ After the plugin gem is installed, Slidict automatically includes those YAML files in
334
+ `list-methods`, `show-method`, and `--method` lookup.
335
+
336
+ ### Future localization
337
+
338
+ Method files include a `locale` field. The recommended future shape is one file per
339
+ localized method, for example `scqa.en.yml` and `scqa.ja.yml`, with the same method ID
340
+ plus locale-aware lookup. Keeping all user-facing text in YAML means translations can be
341
+ reviewed independently from Ruby changes.
342
+
343
+ ### OSS contribution workflow
344
+
345
+ To propose a new presentation method:
346
+
347
+ 1. Copy an existing YAML file in `data/slidict/methods/`.
348
+ 2. Change `id`, `name`, `category`, `description`, `suitable_for`, `slides`, `ai_instructions`, and `review_checklist`.
349
+ 3. Add `references` when useful.
350
+ 4. Run `bundle exec rspec` and `bundle exec rubocop`.
351
+ 5. Open a PR with the method file and a short explanation of the communication technique.
@@ -0,0 +1,35 @@
1
+ id: prep
2
+ name: PREP
3
+ category: persuasive
4
+ description: Point, Reason, Example, Point structure for concise persuasive talks.
5
+ locale: en
6
+ suitable_for:
7
+ - Short recommendations
8
+ - Lightning talks
9
+ - Sales or product pitches
10
+ slides:
11
+ - title: Point
12
+ role: State the main claim or recommendation immediately.
13
+ instructions: Open with the conclusion the audience should remember.
14
+ - title: Reason
15
+ role: Explain why the point is valid or important.
16
+ instructions: Give two or three audience-relevant reasons without overloading the slide.
17
+ - title: Example
18
+ role: Make the reason concrete with evidence, a case, or a scenario.
19
+ instructions: Use a specific example that proves the point in the audience's context.
20
+ - title: Point Revisited
21
+ role: Reinforce the original claim after the supporting material.
22
+ instructions: Restate the point with sharper wording and connect it to the desired action.
23
+ - title: Action
24
+ role: Ask the audience to decide, try, approve, or remember something.
25
+ instructions: End with a clear call to action and success criterion.
26
+ ai_instructions:
27
+ - Put the conclusion before the explanation.
28
+ - Keep the example concrete enough that the audience can visualize it.
29
+ - Make the final point stronger, not merely repetitive.
30
+ review_checklist:
31
+ - Is the opening point unmistakable?
32
+ - Do the reasons support the point rather than introduce new claims?
33
+ - Is the example specific and relevant?
34
+ - Does the closing restatement lead to action?
35
+ references: []
@@ -0,0 +1,41 @@
1
+ id: pyramid
2
+ name: Pyramid Principle
3
+ category: executive-communication
4
+ description: Top-down communication structure that starts with the governing thought and supports it with grouped arguments.
5
+ locale: en
6
+ suitable_for:
7
+ - Executive presentations
8
+ - Consulting-style recommendations
9
+ - Complex decisions with multiple supporting arguments
10
+ slides:
11
+ - title: Governing Thought
12
+ role: Present the answer or recommendation first.
13
+ instructions: State the central message as a complete sentence, not a topic label.
14
+ - title: Key Line
15
+ role: Preview the small set of main arguments that support the governing thought.
16
+ instructions: Group supporting ideas into mutually distinct and collectively useful points.
17
+ - title: Argument 1
18
+ role: Develop the first supporting argument.
19
+ instructions: Explain the argument with evidence and show how it supports the governing thought.
20
+ - title: Argument 2
21
+ role: Develop the second supporting argument.
22
+ instructions: Explain the argument with evidence and avoid repeating Argument 1.
23
+ - title: Argument 3
24
+ role: Develop the third supporting argument or risk response.
25
+ instructions: Use this slide for a third pillar, trade-off, or objection handling.
26
+ - title: Synthesis
27
+ role: Reconnect the support to the recommendation and action.
28
+ instructions: Summarize the logic and ask for the specific decision or next step.
29
+ ai_instructions:
30
+ - Start with the answer before details.
31
+ - Organize support into a clear hierarchy with parallel argument labels.
32
+ - Keep each argument distinct and tied back to the governing thought.
33
+ review_checklist:
34
+ - Is the governing thought a complete answer?
35
+ - Are supporting arguments grouped logically?
36
+ - Are argument labels parallel and non-overlapping?
37
+ - Does every detail support a higher-level claim?
38
+ - Does the synthesis request a concrete decision or action?
39
+ references:
40
+ - title: The Pyramid Principle
41
+ author: Barbara Minto
@@ -0,0 +1,38 @@
1
+ id: scqa
2
+ name: SCQA
3
+ category: narrative
4
+ description: Situation, Complication, Question, Answer structure for building a clear business narrative.
5
+ locale: en
6
+ suitable_for:
7
+ - Strategy updates
8
+ - Problem-solving proposals
9
+ - Executive briefings
10
+ slides:
11
+ - title: Situation
12
+ role: Establish the shared context the audience already recognizes.
13
+ instructions: Describe the current state with concrete facts and avoid introducing the problem too early.
14
+ - title: Complication
15
+ role: Show what changed, broke, or created tension.
16
+ instructions: Explain why the current state is no longer sufficient and make the stakes explicit.
17
+ - title: Question
18
+ role: State the key decision or question the talk must answer.
19
+ instructions: Phrase the question from the audience's point of view.
20
+ - title: Answer
21
+ role: Present the core recommendation or conclusion.
22
+ instructions: Give the answer first, then support it with the strongest evidence.
23
+ - title: Next Steps
24
+ role: Convert the answer into action.
25
+ instructions: Close with the decision, owner, and immediate next step.
26
+ ai_instructions:
27
+ - Use the SCQA sequence exactly and keep each slide focused on one narrative job.
28
+ - Make the answer concise enough to be repeated as a one-sentence takeaway.
29
+ - Prefer audience-relevant evidence over generic background.
30
+ review_checklist:
31
+ - Does the situation establish common ground?
32
+ - Is the complication specific and consequential?
33
+ - Is the question explicit and audience-centered?
34
+ - Does the answer directly resolve the question?
35
+ - Are the next steps concrete?
36
+ references:
37
+ - title: The Pyramid Principle
38
+ author: Barbara Minto
@@ -26,6 +26,8 @@ module Slidict
26
26
  return slides(options[:args]) if options[:command] == "slides"
27
27
  return serve(options[:args]) if options[:command] == "serve"
28
28
  return lint(options[:args]) if options[:command] == "lint"
29
+ return list_methods if options[:command] == "list-methods"
30
+ return show_method(options[:args]) if options[:command] == "show-method"
29
31
 
30
32
  config = build_config(options)
31
33
  return print_available_models(config) if config.llm_enabled? && config.model.nil?
@@ -38,7 +40,8 @@ module Slidict
38
40
  duration: ask("How long is the presentation?", options[:duration]),
39
41
  audience: ask("Who is the audience?", options[:audience]),
40
42
  goal: ask("What should the audience remember or do?", options[:goal]),
41
- framework: options[:framework]
43
+ framework: options[:framework],
44
+ presentation_method: options[:presentation_method]
42
45
  )
43
46
 
44
47
  if client
@@ -50,7 +53,7 @@ module Slidict
50
53
  end
51
54
  deck = Deck.new(
52
55
  topic: deck.topic, duration: deck.duration, audience: deck.audience, goal: deck.goal,
53
- framework: deck.framework, slides: slides
56
+ framework: deck.framework, slides: slides, presentation_method: deck.presentation_method
54
57
  )
55
58
  end
56
59
 
@@ -76,6 +79,8 @@ module Slidict
76
79
  options = { framework: "slidev" }
77
80
  args = argv.dup
78
81
 
82
+ args.shift if args.first == "new"
83
+
79
84
  if args.first == "auth"
80
85
  args.shift
81
86
  raise ArgumentError, "auth does not accept options" unless args.empty?
@@ -105,6 +110,21 @@ module Slidict
105
110
  return options
106
111
  end
107
112
 
113
+ if args.first == "list-methods"
114
+ args.shift
115
+ raise ArgumentError, "list-methods does not accept options" unless args.empty?
116
+
117
+ options[:command] = "list-methods"
118
+ return options
119
+ end
120
+
121
+ if args.first == "show-method"
122
+ args.shift
123
+ options[:command] = "show-method"
124
+ options[:args] = args
125
+ return options
126
+ end
127
+
108
128
  until args.empty?
109
129
  case (arg = args.shift)
110
130
  when "-h", "--help"
@@ -123,6 +143,8 @@ module Slidict
123
143
  options[:goal] = fetch_value!(args, arg)
124
144
  when "--framework"
125
145
  options[:framework] = fetch_value!(args, arg)
146
+ when "--method"
147
+ options[:method] = fetch_value!(args, arg)
126
148
  when "--llm-base-url"
127
149
  options[:llm_base_url] = fetch_value!(args, arg)
128
150
  when "--llm-api-key"
@@ -145,6 +167,7 @@ module Slidict
145
167
  end
146
168
 
147
169
  options[:output] ||= output_path_for(options[:framework], options[:filename])
170
+ options[:presentation_method] = method_for(options[:method])
148
171
  options
149
172
  end
150
173
 
@@ -228,6 +251,41 @@ module Slidict
228
251
  lint_command.run(args)
229
252
  end
230
253
 
254
+ def list_methods
255
+ method_registry.all.each do |method|
256
+ @output.puts format(
257
+ "%-12<id>s %-28<name>s %<category>s",
258
+ id: method.id, name: method.name, category: method.category
259
+ )
260
+ end
261
+ 0
262
+ end
263
+
264
+ def show_method(args)
265
+ raise ArgumentError, "show-method requires a method id" if args.empty?
266
+ raise ArgumentError, "show-method accepts exactly one method id" unless args.size == 1
267
+
268
+ method = method_registry.fetch(args.first)
269
+ @output.puts "#{method.name} (#{method.id})"
270
+ @output.puts "Category: #{method.category}"
271
+ @output.puts "Description: #{method.description}"
272
+ @output.puts "Suitable for:"
273
+ method.suitable_for.each { |item| @output.puts " - #{item}" }
274
+ @output.puts "Slides:"
275
+ method.slides.each_with_index { |slide, i| @output.puts " #{i + 1}. #{slide.title}: #{slide.role}" }
276
+ @output.puts "Review checklist:"
277
+ method.review_checklist.each { |item| @output.puts " - #{item}" }
278
+ 0
279
+ end
280
+
281
+ def method_for(id)
282
+ id ? method_registry.fetch(id) : nil
283
+ end
284
+
285
+ def method_registry
286
+ @method_registry ||= PresentationMethodRegistry.new
287
+ end
288
+
231
289
  def publish_to_slidict(deck, content, options)
232
290
  slides_command.publish(
233
291
  id: options[:slide_id],
@@ -276,11 +334,13 @@ module Slidict
276
334
 
277
335
  def print_help
278
336
  @output.puts <<~HELP
279
- Usage: slidict [options]
337
+ Usage: slidict [new] [options]
280
338
  Usage: slidict auth
281
339
  Usage: slidict slides <list|show|create|edit> [options]
282
340
  Usage: slidict serve [sinatra options]
283
341
  Usage: slidict lint <file> [options]
342
+ Usage: slidict list-methods
343
+ Usage: slidict show-method <id>
284
344
 
285
345
  Generate presentation source files from a short conversation.
286
346
 
@@ -290,6 +350,8 @@ module Slidict
290
350
  serve Serve slide files from ./public with Sinatra
291
351
  lint Check whether a slide deck's structure will land with its audience
292
352
  (run `slidict lint -h` for details)
353
+ list-methods List available presentation methods
354
+ show-method Show details for one presentation method
293
355
 
294
356
  Options:
295
357
  --topic TEXT Presentation topic
@@ -297,6 +359,7 @@ module Slidict
297
359
  --audience TEXT Target audience
298
360
  --goal TEXT Desired audience takeaway or action
299
361
  --framework NAME #{Output::Format.names.join(", ")} (default: slidev)
362
+ --method ID Presentation method, for example scqa, prep, or pyramid
300
363
  --filename NAME File name under public/ (default: next sequential file)
301
364
  --llm-base-url URL OpenAI Compatible API base URL (env: SLIDICT_LLM_BASE_URL).
302
365
  When omitted, the built-in slide template is used instead.
data/lib/slidict/deck.rb CHANGED
@@ -4,14 +4,15 @@ module Slidict
4
4
  Slide = Struct.new(:title, :bullets, keyword_init: true)
5
5
 
6
6
  class Deck
7
- attr_reader :topic, :duration, :audience, :goal, :framework
7
+ attr_reader :topic, :duration, :audience, :goal, :framework, :presentation_method
8
8
 
9
- def initialize(topic:, duration:, audience:, goal:, framework: "slidev", slides: nil)
9
+ def initialize(topic:, duration:, audience:, goal:, framework: "slidev", slides: nil, presentation_method: nil)
10
10
  @topic = normalize(topic, fallback: "Untitled presentation")
11
11
  @duration = normalize(duration, fallback: "5 minutes")
12
12
  @audience = normalize(audience, fallback: "general audience")
13
13
  @goal = normalize(goal, fallback: "understand the key message")
14
14
  @framework = normalize(framework, fallback: "slidev").downcase
15
+ @presentation_method = presentation_method
15
16
  @slides = slides
16
17
  end
17
18
 
@@ -22,6 +23,8 @@ module Slidict
22
23
  private
23
24
 
24
25
  def default_slides
26
+ return method_slides if presentation_method
27
+
25
28
  [
26
29
  Slide.new(title: topic, bullets: ["For #{audience}", "Goal: #{goal}", "Length: #{duration}"]),
27
30
  Slide.new(title: "Why this matters", bullets: ["Clarifies the problem before discussing solutions", "Keeps the story focused on audience value", "Sets up a memorable takeaway"]),
@@ -31,6 +34,15 @@ module Slidict
31
34
  ]
32
35
  end
33
36
 
37
+ def method_slides
38
+ presentation_method.slides.map do |slide|
39
+ Slide.new(
40
+ title: slide.title,
41
+ bullets: [slide.role, slide.instructions, "Tie this slide to: #{goal}"]
42
+ )
43
+ end
44
+ end
45
+
34
46
  def normalize(value, fallback:)
35
47
  text = value.to_s.strip
36
48
  text.empty? ? fallback : text
@@ -64,14 +64,34 @@ module Slidict
64
64
  Duration: #{deck.duration}
65
65
  Audience: #{deck.audience}
66
66
  Goal: #{deck.goal}
67
+ #{method_prompt_for(deck)}
67
68
 
68
- Return exactly 5 slides as a JSON array. Each item must be an object with
69
+ Return one slide for each required slide role when a presentation method is
70
+ provided; otherwise return exactly 5 slides. Each item must be an object with
69
71
  a "title" string and a "bullets" array of 2-4 short strings.
70
72
  Respond with the JSON array only: no commentary, no markdown code fences,
71
73
  and no reasoning or thinking content before or after it.
72
74
  PROMPT
73
75
  end
74
76
 
77
+ def method_prompt_for(deck)
78
+ method = deck.presentation_method
79
+ return "" unless method
80
+
81
+ slides = method.slides.each_with_index.map do |slide, index|
82
+ "#{index + 1}. #{slide.title} — role: #{slide.role}; instructions: #{slide.instructions}"
83
+ end.join("\n")
84
+ instructions = method.ai_instructions.map { |item| "- #{item}" }.join("\n")
85
+ <<~METHOD
86
+ Presentation method: #{method.name} (#{method.id})
87
+ Method description: #{method.description}
88
+ Required slide roles:
89
+ #{slides}
90
+ Method-specific generation instructions:
91
+ #{instructions}
92
+ METHOD
93
+ end
94
+
75
95
  LINT_PROMPT_TEMPLATE = <<~PROMPT
76
96
  You are a presentation structure linter for tech talks and lightning talks.
77
97
  Your goal is not to judge how the slides look, but to diagnose whether the
@@ -109,7 +129,7 @@ module Slidict
109
129
  def lint_prompt_for(slide_texts, translate: nil)
110
130
  numbered = slide_texts.each_with_index.map { |text, i| "--- Slide #{i + 1} ---\n#{text}" }.join("\n\n")
111
131
  prompt = format(LINT_PROMPT_TEMPLATE, numbered: numbered)
112
- translate ? "#{prompt}\nWrite each message field in #{translate}." : prompt
132
+ translate ? "#{prompt}\nTranslate only the \"message\" field of each finding into #{translate}. Keep \"slide\" as an integer and \"severity\" as exactly \"warning\" or \"info\" — do not translate those values." : prompt
113
133
  end
114
134
 
115
135
  def findings_from(content)
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Slidict
6
+ MethodSlide = Struct.new(:title, :role, :instructions, keyword_init: true)
7
+
8
+ class PresentationMethod
9
+ REQUIRED_FIELDS = %w[id name category description suitable_for slides ai_instructions review_checklist].freeze
10
+ REQUIRED_SLIDE_FIELDS = %w[title role instructions].freeze
11
+
12
+ attr_reader :id, :name, :category, :description, :suitable_for, :slides,
13
+ :ai_instructions, :review_checklist, :references, :locale, :source_path
14
+
15
+ def initialize(attributes, source_path: nil)
16
+ @source_path = source_path
17
+ @id = attributes.fetch("id")
18
+ @name = attributes.fetch("name")
19
+ @category = attributes.fetch("category")
20
+ @description = attributes.fetch("description")
21
+ @suitable_for = Array(attributes.fetch("suitable_for"))
22
+ @slides = Array(attributes.fetch("slides")).map do |slide|
23
+ MethodSlide.new(
24
+ title: slide.fetch("title"),
25
+ role: slide.fetch("role"),
26
+ instructions: slide.fetch("instructions")
27
+ )
28
+ end
29
+ @ai_instructions = Array(attributes.fetch("ai_instructions"))
30
+ @review_checklist = Array(attributes.fetch("review_checklist"))
31
+ @references = Array(attributes["references"])
32
+ @locale = attributes.fetch("locale", "en")
33
+ end
34
+
35
+ def self.load_file(path)
36
+ attributes = YAML.safe_load_file(path, permitted_classes: [], aliases: false)
37
+ validate!(attributes, path)
38
+ new(attributes, source_path: path)
39
+ rescue Psych::Exception, KeyError, TypeError => e
40
+ raise ArgumentError, "invalid presentation method #{path}: #{e.message}"
41
+ end
42
+
43
+ def self.validate!(attributes, path)
44
+ raise ArgumentError, "#{path} must contain a YAML mapping" unless attributes.is_a?(Hash)
45
+
46
+ missing = REQUIRED_FIELDS.reject { |field| present?(attributes[field]) }
47
+ raise ArgumentError, "#{path} is missing required fields: #{missing.join(', ')}" unless missing.empty?
48
+
49
+ unless attributes["id"].is_a?(String) && attributes["id"].match?(/\A[a-z0-9-]+\z/)
50
+ raise ArgumentError, "#{path} id must be a string using lowercase letters, numbers, and hyphens"
51
+ end
52
+ raise ArgumentError, "#{path} slides must be a non-empty array" unless attributes["slides"].is_a?(Array) && !attributes["slides"].empty?
53
+
54
+ attributes["slides"].each_with_index do |slide, index|
55
+ unless slide.is_a?(Hash)
56
+ raise ArgumentError, "#{path} slide #{index + 1} must be a mapping"
57
+ end
58
+
59
+ missing_slide = REQUIRED_SLIDE_FIELDS.reject { |field| present?(slide[field]) }
60
+ next if missing_slide.empty?
61
+
62
+ raise ArgumentError, "#{path} slide #{index + 1} is missing required fields: #{missing_slide.join(', ')}"
63
+ end
64
+ end
65
+
66
+ def self.present?(value)
67
+ case value
68
+ when String then !value.strip.empty?
69
+ when Array then !value.empty?
70
+ else !value.nil?
71
+ end
72
+ end
73
+ end
74
+
75
+ class PresentationMethodRegistry
76
+ BUILTIN_GLOB = File.expand_path("../../data/slidict/methods/*.yml", __dir__)
77
+ PLUGIN_GLOB = "slidict/methods/*.yml"
78
+
79
+ def initialize(paths: nil, include_plugins: true)
80
+ @paths = paths || default_paths(include_plugins: include_plugins)
81
+ end
82
+
83
+ def all
84
+ @all ||= @paths.sort.map { |path| PresentationMethod.load_file(path) }.sort_by(&:id)
85
+ end
86
+
87
+ def find(id)
88
+ all.find { |method| method.id == id.to_s }
89
+ end
90
+
91
+ def fetch(id)
92
+ find(id) || raise(ArgumentError, "unknown presentation method #{id}")
93
+ end
94
+
95
+ private
96
+
97
+ def default_paths(include_plugins:)
98
+ paths = Dir[BUILTIN_GLOB]
99
+ paths += Gem.find_files(PLUGIN_GLOB) if include_plugins
100
+ paths.uniq
101
+ end
102
+ end
103
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Slidict
4
- VERSION = "0.4.2"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/slidict.rb CHANGED
@@ -18,6 +18,7 @@ require_relative "slidict/lint/slide_parser"
18
18
  require_relative "slidict/llm/client"
19
19
  require_relative "slidict/output/format"
20
20
  require_relative "slidict/output/renderer"
21
+ require_relative "slidict/presentation_method"
21
22
  require_relative "slidict/version"
22
23
 
23
24
  module Slidict
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slidict
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yusuke Abe
@@ -72,6 +72,9 @@ files:
72
72
  - LICENSE.txt
73
73
  - README.md
74
74
  - Rakefile
75
+ - data/slidict/methods/prep.yml
76
+ - data/slidict/methods/pyramid.yml
77
+ - data/slidict/methods/scqa.yml
75
78
  - lib/slidict.rb
76
79
  - lib/slidict/cli/app.rb
77
80
  - lib/slidict/cli/lint.rb
@@ -89,6 +92,7 @@ files:
89
92
  - lib/slidict/llm/client.rb
90
93
  - lib/slidict/output/format.rb
91
94
  - lib/slidict/output/renderer.rb
95
+ - lib/slidict/presentation_method.rb
92
96
  - lib/slidict/version.rb
93
97
  homepage: https://labs.slidict.io/slidict/
94
98
  licenses: