slidict 0.3.1 → 0.4.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: a58f7f50da42dd170f4f479e90d30f750bfa7e09edc7175b567c34f0637e9d37
4
- data.tar.gz: f61880e7f032400e945e5174abae17e15077b0439b352b90af8df2937cf6df9e
3
+ metadata.gz: a9f6b8851999b92437addc0186d6a047c1c8317145940987514df479e02fabc2
4
+ data.tar.gz: fb42f714400e77cbc97931d105cef01a8b54776c0ce67db152ce6ce475136caf
5
5
  SHA512:
6
- metadata.gz: 30f98a358a929f7cd6048f3862183bf8a20c901ea6b6c86a3492865b4abbbdf64de140939c6c754168d4645b66e074788fdbf7a669c1176c7d5e9445b2e4edcd
7
- data.tar.gz: d89395639ce333ec905175788aa44da292951560d230529d34f0c1be01b2091a9fc827e4986dff8f02cb07535c0d36562bdc9b05704f13cf6da623f16e532b5c
6
+ metadata.gz: 88ad47ca34c0e2d7bcf99f118905ba3b7b7f8df571aa4e118080552659564bc2bb7d79d007c3c67a9dc4ebd291e415e6cf423f93299f013485856737e54f7e77
7
+ data.tar.gz: 781cd459d6c82e62a29bbc8702bea3c7f9d4125d78341e2b13723608ded41e9131af456b87dbfc61525dbc7ebab91da9729fc4465e3b852c0298a96f1e277223
@@ -5,36 +5,19 @@ on:
5
5
  branches:
6
6
  - main
7
7
 
8
+ permissions:
9
+ contents: write
10
+ pull-requests: read
11
+
8
12
  jobs:
9
13
  changelog:
10
14
  runs-on: ubuntu-latest
11
15
 
12
16
  steps:
13
- - name: Checkout Code
14
- uses: actions/checkout@v3
15
- with:
16
- fetch-depth: 0
17
-
18
- - name: Extract version
19
- id: version
20
- run: |
21
- version=$(grep -Eo "[0-9]+\.[0-9]+\.[0-9]+" lib/slidict/version.rb | head -n 1)
22
- echo "version=v$version" >> $GITHUB_ENV
23
- echo "VERSION_TAG=v$version" >> $GITHUB_ENV
24
-
25
- - name: Check if version file changed
26
- id: version_file
27
- run: |
28
- if git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" | grep -qx "lib/slidict/version.rb"; then
29
- echo "changed=true" >> "$GITHUB_OUTPUT"
30
- else
31
- echo "changed=false" >> "$GITHUB_OUTPUT"
32
- fi
33
-
34
- - name: Release with release-drafter
17
+ - name: Update draft release notes
35
18
  uses: release-drafter/release-drafter@v5
36
19
  with:
37
20
  config-name: release-drafter.yml
38
- publish: ${{ steps.version_file.outputs.changed == 'true' }}
21
+ publish: false
39
22
  env:
40
23
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,31 +1,51 @@
1
1
  name: Ruby Gem
2
2
 
3
3
  on:
4
- push:
5
- branches:
6
- - main
7
- paths:
8
- - lib/slidict/version.rb
4
+ workflow_run:
5
+ workflows:
6
+ - Changelog
7
+ types:
8
+ - completed
9
9
 
10
10
  jobs:
11
11
  build:
12
12
  name: Build + Publish
13
13
  runs-on: ubuntu-latest
14
+ if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main'
14
15
  permissions:
15
- contents: read
16
+ contents: write
16
17
  packages: write
17
18
 
18
19
  steps:
19
20
  - uses: actions/checkout@v3
20
21
  with:
22
+ ref: ${{ github.event.workflow_run.head_sha }}
21
23
  fetch-depth: 0
22
24
 
25
+ - name: Check if version file changed
26
+ id: version_file
27
+ run: |
28
+ if git diff --name-only HEAD~1 HEAD | grep -qx "lib/slidict/version.rb"; then
29
+ echo "changed=true" >> "$GITHUB_OUTPUT"
30
+ else
31
+ echo "changed=false" >> "$GITHUB_OUTPUT"
32
+ fi
33
+
34
+ - name: Extract version
35
+ if: steps.version_file.outputs.changed == 'true'
36
+ id: version
37
+ run: |
38
+ version=$(grep -Eo "[0-9]+\.[0-9]+\.[0-9]+" lib/slidict/version.rb | head -n 1)
39
+ echo "tag=v$version" >> "$GITHUB_OUTPUT"
40
+
23
41
  - name: Set up Ruby 3.4
42
+ if: steps.version_file.outputs.changed == 'true'
24
43
  uses: ruby/setup-ruby@v1
25
44
  with:
26
45
  ruby-version: 3.4.3
27
46
 
28
47
  - name: Publish to GPR
48
+ if: steps.version_file.outputs.changed == 'true'
29
49
  run: |
30
50
  mkdir -p $HOME/.gem
31
51
  touch $HOME/.gem/credentials
@@ -38,6 +58,7 @@ jobs:
38
58
  OWNER: ${{ github.repository_owner }}
39
59
 
40
60
  - name: Publish to RubyGems
61
+ if: steps.version_file.outputs.changed == 'true'
41
62
  run: |
42
63
  mkdir -p $HOME/.gem
43
64
  touch $HOME/.gem/credentials
@@ -47,3 +68,14 @@ jobs:
47
68
  gem push *.gem
48
69
  env:
49
70
  GEM_HOST_API_KEY: "${{ secrets.RUBYGEMS_AUTH_TOKEN }}"
71
+
72
+ - name: Publish release notes
73
+ if: steps.version_file.outputs.changed == 'true'
74
+ uses: release-drafter/release-drafter@v5
75
+ with:
76
+ config-name: release-drafter.yml
77
+ publish: true
78
+ tag: ${{ steps.version.outputs.tag }}
79
+ name: ${{ steps.version.outputs.tag }}
80
+ env:
81
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
data/AGENTS.md CHANGED
@@ -21,8 +21,8 @@ OpenAI-compatible chat API.
21
21
 
22
22
  Run both tests and lint before considering a change complete.
23
23
 
24
- When you add or change a CLI command (or its options), update the "Commands" section
25
- of `README.md` to document it.
24
+ When you add or change a feature (a CLI command, its options, or any other
25
+ user-facing behavior), update `README.md` to document it.
26
26
 
27
27
  ## Commit conventions
28
28
 
data/README.md CHANGED
@@ -13,6 +13,7 @@ Unlike traditional slide generators, Slidict focuses on communication before sli
13
13
  - Local-first MVP implemented in Ruby
14
14
  - Built-in Sinatra server for browsing generated slides from `public/`
15
15
  - OpenAI Compatible API support, so you can point Slidict at OpenAI, Ollama, LM Studio, vLLM, or any other server implementing the same `/chat/completions` endpoint
16
+ - Presentation structure linter: checks whether a Markdown/Asciidoc deck's *structure* (not its looks) will land with an audience
16
17
 
17
18
  ## Requirements
18
19
 
@@ -127,6 +128,34 @@ bin/slidict serve -p 4567 -o 0.0.0.0
127
128
  `bin/slidict --publish` and `--slide-id` (see [Usage](#usage)) wrap this same `create`/`edit`
128
129
  behavior so you can save the slides you just generated straight to slidict.io.
129
130
 
131
+ ### `slidict lint`
132
+
133
+ Diagnoses whether a Markdown or Asciidoc slide deck has a structure an audience can
134
+ actually follow -- not whether it looks nice. Diagnosis only; it does not rewrite the
135
+ file. Requires an LLM endpoint (see [Configuration](#configuration)).
136
+
137
+ ```bash
138
+ bin/slidict lint talk.md --llm-base-url http://localhost:11434/v1 --llm-api-key ollama --llm-model llama3
139
+ ```
140
+
141
+ ```text
142
+ [warning] Slide 3: this slide's main point is unclear
143
+ [warning] Slide 5: "MCP" is used without first explaining it
144
+ [info] Slide 8: the closing slide is weak; consider adding a one-sentence takeaway for the audience
145
+ ```
146
+
147
+ It checks six things across the whole deck:
148
+
149
+ 1. Is the audience clear?
150
+ 2. Can the overall point be stated in one sentence?
151
+ 3. Does the deck flow background → problem → solution → result/learning?
152
+ 4. Is any single slide overloaded with information?
153
+ 5. Is jargon used without first explaining it?
154
+ 6. Does the closing slide give the audience a concrete takeaway?
155
+
156
+ The deck format (`markdown` or `asciidoc`) is auto-detected from the file extension;
157
+ override it with `--format`. Run `bin/slidict lint -h` for the full list of options.
158
+
130
159
  ## Configuration
131
160
 
132
161
  Slidict generates slides with an LLM through any OpenAI Compatible API. Configure the
@@ -191,6 +220,7 @@ We optimize for communication, not decoration.
191
220
  - [x] Interactive CLI
192
221
  - [x] Slide generation
193
222
  - [x] OpenAI Compatible API support (configurable base URL, so Ollama, LM Studio, and other compatible servers work out of the box)
223
+ - [x] Presentation structure linter (`slidict lint`)
194
224
 
195
225
  ## License
196
226
 
@@ -7,7 +7,7 @@ module Slidict
7
7
  module Cli
8
8
  class App
9
9
  def initialize(input: $stdin, output: $stdout, renderer: Output::Renderer.new, auth_client: nil,
10
- credentials: nil, sleeper: Kernel, slides_command: nil, server: nil)
10
+ credentials: nil, sleeper: Kernel, slides_command: nil, server: nil, lint_command: nil)
11
11
  @input = input
12
12
  @output = output
13
13
  @renderer = renderer
@@ -16,6 +16,7 @@ module Slidict
16
16
  @sleeper = sleeper
17
17
  @slides_command = slides_command
18
18
  @server = server
19
+ @lint_command = lint_command
19
20
  end
20
21
 
21
22
  def run(argv = [])
@@ -24,6 +25,7 @@ module Slidict
24
25
  return auth if options[:command] == "auth"
25
26
  return slides(options[:args]) if options[:command] == "slides"
26
27
  return serve(options[:args]) if options[:command] == "serve"
28
+ return lint(options[:args]) if options[:command] == "lint"
27
29
 
28
30
  config = build_config(options)
29
31
  client = llm_client_for(config)
@@ -94,6 +96,13 @@ module Slidict
94
96
  return options
95
97
  end
96
98
 
99
+ if args.first == "lint"
100
+ args.shift
101
+ options[:command] = "lint"
102
+ options[:args] = args
103
+ return options
104
+ end
105
+
97
106
  until args.empty?
98
107
  case (arg = args.shift)
99
108
  when "-h", "--help"
@@ -198,6 +207,10 @@ module Slidict
198
207
  server.run(args)
199
208
  end
200
209
 
210
+ def lint(args)
211
+ lint_command.run(args)
212
+ end
213
+
201
214
  def publish_to_slidict(deck, content, options)
202
215
  slides_command.publish(
203
216
  id: options[:slide_id],
@@ -216,6 +229,10 @@ module Slidict
216
229
  @server ||= Serve.new(output: @output)
217
230
  end
218
231
 
232
+ def lint_command
233
+ @lint_command ||= Lint.new(output: @output)
234
+ end
235
+
219
236
  def body_format_for(framework)
220
237
  Output::Format.fetch(framework).body_format
221
238
  end
@@ -246,6 +263,7 @@ module Slidict
246
263
  Usage: slidict auth
247
264
  Usage: slidict slides <list|show|create|edit> [options]
248
265
  Usage: slidict serve [sinatra options]
266
+ Usage: slidict lint <file> [options]
249
267
 
250
268
  Generate presentation source files from a short conversation.
251
269
 
@@ -253,6 +271,8 @@ module Slidict
253
271
  auth Authenticate the CLI with GitHub and save a CLI access token
254
272
  slides Manage your slides on slidict.io (run `slidict slides -h` for details)
255
273
  serve Serve slide files from ./public with Sinatra
274
+ lint Check whether a slide deck's structure will land with its audience
275
+ (run `slidict lint -h` for details)
256
276
 
257
277
  Options:
258
278
  --topic TEXT Presentation topic
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slidict
4
+ module Cli
5
+ # Implements `slidict lint <file>`: diagnoses whether a Markdown/Asciidoc
6
+ # slide deck has a structure that an audience can actually follow (not
7
+ # whether it looks nice). Diagnosis only -- it does not rewrite the file.
8
+ class Lint
9
+ ASCIIDOC_EXTENSIONS = %w[.adoc .asciidoc].freeze
10
+
11
+ def initialize(output:, linter_factory: nil, renderer: Slidict::Lint::Renderer.new)
12
+ @output = output
13
+ @linter_factory = linter_factory || method(:default_linter)
14
+ @renderer = renderer
15
+ end
16
+
17
+ def run(argv)
18
+ options = parse(argv)
19
+ return print_help if options[:help] || options[:path].nil?
20
+ return file_not_found(options[:path]) unless File.exist?(options[:path])
21
+
22
+ run_lint(options)
23
+ rescue ArgumentError => e
24
+ print_usage_error(e)
25
+ rescue Slidict::Lint::Linter::Error, Llm::Client::Error => e
26
+ print_error(e)
27
+ end
28
+
29
+ private
30
+
31
+ def run_lint(options)
32
+ config = build_config(options)
33
+ return llm_required unless config.llm_enabled?
34
+
35
+ print_findings(lint(options, config))
36
+ end
37
+
38
+ def lint(options, config)
39
+ @linter_factory.call(config).lint(File.read(options[:path]), format: format_for(options))
40
+ end
41
+
42
+ def print_usage_error(error)
43
+ print_error(error)
44
+ @output.puts
45
+ print_help
46
+ 1
47
+ end
48
+
49
+ def print_error(error)
50
+ @output.puts "Error: #{error.message}"
51
+ 1
52
+ end
53
+
54
+ def parse(argv)
55
+ args = argv.dup
56
+ options = { path: extract_path!(args) }
57
+ parse_options!(args, options)
58
+ options
59
+ end
60
+
61
+ def extract_path!(args)
62
+ args.shift unless args.first.to_s.start_with?("-")
63
+ end
64
+
65
+ def parse_options!(args, options)
66
+ until args.empty?
67
+ case (arg = args.shift)
68
+ when "-h", "--help" then options[:help] = true
69
+ when "--format" then options[:format] = fetch_value!(args, arg)
70
+ when "--llm-base-url" then options[:llm_base_url] = fetch_value!(args, arg)
71
+ when "--llm-api-key" then options[:llm_api_key] = fetch_value!(args, arg)
72
+ when "--llm-model" then options[:llm_model] = fetch_value!(args, arg)
73
+ else raise ArgumentError, "unknown option #{arg}"
74
+ end
75
+ end
76
+ end
77
+
78
+ def fetch_value!(args, option)
79
+ value = args.shift
80
+ raise ArgumentError, "#{option} requires a value" if value.nil? || value.start_with?("-")
81
+
82
+ value
83
+ end
84
+
85
+ def build_config(options)
86
+ Config.from_env.merge(
87
+ base_url: options[:llm_base_url],
88
+ api_key: options[:llm_api_key],
89
+ model: options[:llm_model]
90
+ )
91
+ end
92
+
93
+ def default_linter(config)
94
+ client = Llm::Client.new(base_url: config.base_url, api_key: config.api_key, model: config.model)
95
+ Slidict::Lint::Linter.new(client: client)
96
+ end
97
+
98
+ def format_for(options)
99
+ return options[:format] if options[:format]
100
+
101
+ ASCIIDOC_EXTENSIONS.include?(File.extname(options[:path]).downcase) ? "asciidoc" : "markdown"
102
+ end
103
+
104
+ def print_findings(findings)
105
+ @output.puts(findings.empty? ? "No issues found." : @renderer.render(findings))
106
+ 0
107
+ end
108
+
109
+ def file_not_found(path)
110
+ @output.puts "Error: file not found: #{path}"
111
+ 1
112
+ end
113
+
114
+ def llm_required
115
+ @output.puts "Error: lint requires an LLM endpoint (--llm-base-url or SLIDICT_LLM_BASE_URL)"
116
+ 1
117
+ end
118
+
119
+ def print_help
120
+ @output.puts <<~HELP
121
+ Usage: slidict lint <file> [options]
122
+ Diagnoses whether a slide deck's structure will land with its audience.
123
+ --format FORMAT markdown or asciidoc (default: auto-detected from extension)
124
+ --llm-base-url URL OpenAI Compatible API base URL (env: SLIDICT_LLM_BASE_URL)
125
+ --llm-api-key KEY API key for the LLM endpoint (env: SLIDICT_LLM_API_KEY)
126
+ --llm-model NAME Model name to request (env: SLIDICT_LLM_MODEL, default: gpt-4o-mini)
127
+ -h, --help Show this help
128
+ HELP
129
+ 0
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slidict
4
+ module Lint
5
+ # A single diagnostic produced by the linter: which slide it concerns,
6
+ # how severe it is ("warning" blocks understanding, "info" is a
7
+ # suggestion), and the human-readable message to show the user.
8
+ Finding = Struct.new(:slide, :severity, :message, keyword_init: true)
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slidict
4
+ module Lint
5
+ # Orchestrates a lint run: splits the raw deck source into slides and
6
+ # asks the LLM client to diagnose the presentation's structure.
7
+ class Linter
8
+ class Error < StandardError; end
9
+
10
+ def initialize(client:)
11
+ @client = client
12
+ end
13
+
14
+ def lint(content, format: "markdown")
15
+ slides = SlideParser.parse(content, format: format)
16
+ raise Error, "no slides found in the given file" if slides.empty?
17
+
18
+ @client.lint_slides(slides)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slidict
4
+ module Lint
5
+ # Formats findings as one line per finding:
6
+ # [warning] Slide 3: this slide's main point is unclear
7
+ class Renderer
8
+ def render(findings)
9
+ findings.sort_by(&:slide).map { |finding| line_for(finding) }.join("\n")
10
+ end
11
+
12
+ private
13
+
14
+ def line_for(finding)
15
+ "[#{finding.severity}] Slide #{finding.slide}: #{finding.message}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slidict
4
+ module Lint
5
+ # Splits raw Markdown/Asciidoc slide source into one text block per
6
+ # slide, so the linter can refer to slides by number. Frontmatter
7
+ # (Markdown) and the document title/preamble (Asciidoc) are dropped
8
+ # since they are not slides a reader sees.
9
+ class SlideParser
10
+ MARKDOWN_SEPARATOR = /\A-{3,}\s*\z/
11
+ ASCIIDOC_SLIDE_HEADING = /\A==\s+/
12
+ MARKDOWN_HEADING = /\A#\s+/
13
+
14
+ def self.parse(content, format: "markdown")
15
+ format == "asciidoc" ? parse_asciidoc(content) : parse_markdown(content)
16
+ end
17
+
18
+ def self.parse_markdown(content)
19
+ blocks = split_lines(content, MARKDOWN_SEPARATOR)
20
+ blocks = blocks.drop(2) if content.lines.first&.match?(MARKDOWN_SEPARATOR)
21
+
22
+ if blocks.size <= 1
23
+ by_heading = split_lines(content, MARKDOWN_HEADING, keep_separator: true, drop_preamble: true)
24
+ blocks = by_heading unless by_heading.empty?
25
+ end
26
+
27
+ clean(blocks)
28
+ end
29
+ private_class_method :parse_markdown
30
+
31
+ def self.parse_asciidoc(content)
32
+ clean(split_lines(content, ASCIIDOC_SLIDE_HEADING, keep_separator: true, drop_preamble: true))
33
+ end
34
+ private_class_method :parse_asciidoc
35
+
36
+ # Splits `content` into blocks every time a line matches `separator`.
37
+ # With keep_separator: true the matching line starts a new block
38
+ # instead of being discarded (used for heading-based splitting).
39
+ # With drop_preamble: true, any lines before the first match are
40
+ # discarded rather than kept as a leading block.
41
+ def self.split_lines(content, separator, keep_separator: false, drop_preamble: false)
42
+ blocks = []
43
+ current = drop_preamble ? nil : []
44
+ content.each_line do |line|
45
+ current = append_line(blocks, current, line, separator, keep_separator)
46
+ end
47
+ blocks << current.join if current
48
+ blocks
49
+ end
50
+ private_class_method :split_lines
51
+
52
+ def self.append_line(blocks, current, line, separator, keep_separator)
53
+ return current << line if current && !line.match?(separator)
54
+
55
+ blocks << current.join if current
56
+ return current unless line.match?(separator)
57
+
58
+ keep_separator ? [line] : []
59
+ end
60
+ private_class_method :append_line
61
+
62
+ def self.clean(blocks)
63
+ blocks.map(&:strip).reject(&:empty?)
64
+ end
65
+ private_class_method :clean
66
+ end
67
+ end
68
+ end
@@ -34,6 +34,14 @@ module Slidict
34
34
  slides_from(content)
35
35
  end
36
36
 
37
+ # slide_texts is an array of slide bodies (1-indexed by position) as
38
+ # produced by Slidict::Lint::SlideParser. Returns an array of
39
+ # Slidict::Lint::Finding.
40
+ def lint_slides(slide_texts)
41
+ content = chat_completion(lint_prompt_for(slide_texts))
42
+ findings_from(content)
43
+ end
44
+
37
45
  private
38
46
 
39
47
  def prompt_for(deck)
@@ -51,6 +59,66 @@ module Slidict
51
59
  PROMPT
52
60
  end
53
61
 
62
+ LINT_PROMPT_TEMPLATE = <<~PROMPT
63
+ You are a presentation structure linter for tech talks and lightning talks.
64
+ Your goal is not to judge how the slides look, but to diagnose whether the
65
+ structure will actually land with an audience.
66
+
67
+ Evaluate the deck as a whole against these six checks:
68
+
69
+ 1. Is the audience clear (who is this talk for)?
70
+ 2. Can the overall point be stated in one sentence?
71
+ 3. Does the deck flow background -> problem -> solution -> result/learning
72
+ (call out where the flow breaks down)?
73
+ 4. Is any single slide overloaded with information?
74
+ 5. Is jargon used without first explaining it?
75
+ 6. Does the closing slide give the audience one concrete takeaway?
76
+
77
+ For each finding, name the single slide it relates to most. For deck-wide
78
+ findings (unclear audience, unclear thesis, etc.), point to the slide where
79
+ the problem is most visible, or slide 1 if you cannot tell.
80
+
81
+ Decide severity using these rules:
82
+ - warning: likely to block audience understanding (checks 1-5)
83
+ - info: a suggestion that would make things better (check 6, or minor polish)
84
+
85
+ Here is the slide content ("--- Slide N ---" marks each slide boundary):
86
+
87
+ %<numbered>s
88
+
89
+ Respond with a JSON array of findings only. Each item must be an object of
90
+ the form {"slide": <integer>, "severity": "warning" or "info", "message": "<one sentence>"}.
91
+
92
+ Do not include any commentary, markdown code fences, or text other than the
93
+ JSON array. Return an empty array [] if you find no issues.
94
+ PROMPT
95
+
96
+ def lint_prompt_for(slide_texts)
97
+ numbered = slide_texts.each_with_index.map { |text, i| "--- Slide #{i + 1} ---\n#{text}" }.join("\n\n")
98
+ format(LINT_PROMPT_TEMPLATE, numbered: numbered)
99
+ end
100
+
101
+ def findings_from(content)
102
+ parsed = JSON.parse(extract_json_array(content))
103
+ raise Error, "expected a JSON array of findings" unless parsed.is_a?(Array)
104
+
105
+ parsed.map { |item| finding_from(item) }
106
+ rescue JSON::ParserError, KeyError, ArgumentError, TypeError => e
107
+ raise Error, "could not parse model response: #{e.message}"
108
+ end
109
+
110
+ def finding_from(item)
111
+ Lint::Finding.new(
112
+ slide: Integer(item.fetch("slide")),
113
+ severity: normalize_severity(item["severity"]),
114
+ message: item.fetch("message")
115
+ )
116
+ end
117
+
118
+ def normalize_severity(value)
119
+ %w[warning info].include?(value) ? value : "info"
120
+ end
121
+
54
122
  def chat_completion(prompt)
55
123
  response = JSON.parse(post_chat_completion(prompt))
56
124
  content = response.dig("choices", 0, "message", "content")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Slidict
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/slidict.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "time"
4
4
 
5
5
  require_relative "slidict/cli/app"
6
+ require_relative "slidict/cli/lint"
6
7
  require_relative "slidict/cli/serve"
7
8
  require_relative "slidict/cli/slides"
8
9
  require_relative "slidict/config"
@@ -10,6 +11,10 @@ require_relative "slidict/deck"
10
11
  require_relative "slidict/external/slidict_io/auth"
11
12
  require_relative "slidict/external/slidict_io/client"
12
13
  require_relative "slidict/external/slidict_io/credentials"
14
+ require_relative "slidict/lint/finding"
15
+ require_relative "slidict/lint/linter"
16
+ require_relative "slidict/lint/renderer"
17
+ require_relative "slidict/lint/slide_parser"
13
18
  require_relative "slidict/llm/client"
14
19
  require_relative "slidict/output/format"
15
20
  require_relative "slidict/output/renderer"
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.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yusuke Abe
@@ -74,6 +74,7 @@ files:
74
74
  - Rakefile
75
75
  - lib/slidict.rb
76
76
  - lib/slidict/cli/app.rb
77
+ - lib/slidict/cli/lint.rb
77
78
  - lib/slidict/cli/serve.rb
78
79
  - lib/slidict/cli/slides.rb
79
80
  - lib/slidict/config.rb
@@ -81,6 +82,10 @@ files:
81
82
  - lib/slidict/external/slidict_io/auth.rb
82
83
  - lib/slidict/external/slidict_io/client.rb
83
84
  - lib/slidict/external/slidict_io/credentials.rb
85
+ - lib/slidict/lint/finding.rb
86
+ - lib/slidict/lint/linter.rb
87
+ - lib/slidict/lint/renderer.rb
88
+ - lib/slidict/lint/slide_parser.rb
84
89
  - lib/slidict/llm/client.rb
85
90
  - lib/slidict/output/format.rb
86
91
  - lib/slidict/output/renderer.rb