markdown_exec 3.5.1 → 3.5.2

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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/.ai-agent-instructions +54 -0
  3. data/.cursorrules +198 -0
  4. data/.rubocop.wide.yml +5 -0
  5. data/.rubocop.yml +7 -2
  6. data/CHANGELOG.md +12 -1
  7. data/Gemfile.lock +1 -1
  8. data/Rakefile +2 -0
  9. data/ai-principles.md +516 -0
  10. data/architecture-decisions.md +190 -0
  11. data/bats/block-hide.bats +1 -1
  12. data/bats/block-type-bash.bats +5 -5
  13. data/bats/block-type-link.bats +1 -1
  14. data/bats/block-type-opts.bats +3 -3
  15. data/bats/block-type-port.bats +2 -2
  16. data/bats/block-type-shell-require-ux.bats +2 -2
  17. data/bats/block-type-ux-allowed.bats +4 -4
  18. data/bats/block-type-ux-auto.bats +1 -1
  19. data/bats/block-type-ux-chained.bats +1 -1
  20. data/bats/block-type-ux-default.bats +1 -1
  21. data/bats/block-type-ux-echo-hash-transform.bats +1 -1
  22. data/bats/block-type-ux-echo-hash.bats +2 -2
  23. data/bats/block-type-ux-echo.bats +3 -3
  24. data/bats/block-type-ux-exec-hash-transform.bats +1 -1
  25. data/bats/block-type-ux-exec-hash.bats +2 -2
  26. data/bats/block-type-ux-exec.bats +1 -1
  27. data/bats/block-type-ux-force.bats +1 -1
  28. data/bats/block-type-ux-formats.bats +1 -1
  29. data/bats/block-type-ux-hidden.bats +1 -1
  30. data/bats/block-type-ux-invalid.bats +1 -1
  31. data/bats/block-type-ux-readonly.bats +1 -1
  32. data/bats/block-type-ux-require-chained.bats +2 -2
  33. data/bats/block-type-ux-require-context.bats +2 -2
  34. data/bats/block-type-ux-require.bats +2 -2
  35. data/bats/block-type-ux-required-variables.bats +1 -1
  36. data/bats/block-type-ux-row-format.bats +1 -1
  37. data/bats/block-type-ux-sources.bats +4 -4
  38. data/bats/block-type-ux-transform.bats +1 -1
  39. data/bats/block-type-vars.bats +3 -3
  40. data/bats/border.bats +1 -1
  41. data/bats/cli.bats +11 -11
  42. data/bats/command-substitution-options.bats +2 -2
  43. data/bats/command-substitution.bats +1 -1
  44. data/bats/document-shell.bats +1 -1
  45. data/bats/history.bats +5 -5
  46. data/bats/import-conflict.bats +1 -1
  47. data/bats/import-directive-line-continuation.bats +1 -1
  48. data/bats/import-directive-parameter-symbols.bats +1 -1
  49. data/bats/import-duplicates.bats +6 -6
  50. data/bats/import-parameter-symbols.bats +1 -1
  51. data/bats/import-with-text-substitution.bats +1 -1
  52. data/bats/import.bats +3 -3
  53. data/bats/indented-block-type-vars.bats +1 -1
  54. data/bats/indented-multi-line-output.bats +1 -1
  55. data/bats/line-decor-dynamic.bats +1 -1
  56. data/bats/line-wrapping.bats +1 -1
  57. data/bats/load-vars-state-demo.bats +4 -4
  58. data/bats/markup.bats +4 -4
  59. data/bats/mde.bats +4 -4
  60. data/bats/option-expansion.bats +1 -1
  61. data/bats/options-collapse.bats +4 -4
  62. data/bats/options.bats +47 -17
  63. data/bats/plain.bats +1 -1
  64. data/bats/publish.bats +2 -2
  65. data/bats/table-column-truncate.bats +1 -1
  66. data/bats/table.bats +2 -2
  67. data/bats/variable-expansion-multiline.bats +1 -1
  68. data/bats/variable-expansion.bats +6 -6
  69. data/conversation-template.md +611 -0
  70. data/docs/block-execution-modes.md +177 -0
  71. data/docs/block-filtering.md +252 -0
  72. data/docs/block-naming-patterns.md +210 -0
  73. data/docs/block-scanning-patterns.md +248 -0
  74. data/docs/cli-reference.md +370 -0
  75. data/docs/dev/block-hide.md +1 -1
  76. data/docs/dev/block-type-ux-transform.md +5 -4
  77. data/docs/dev/print_bytes.md +3 -0
  78. data/docs/dev/shebang.md +6 -0
  79. data/docs/docker-testing.md +5 -0
  80. data/docs/execution-control.md +384 -0
  81. data/docs/getting-started.md +209 -0
  82. data/docs/import-options.md +391 -0
  83. data/docs/tab-completion.md +7 -0
  84. data/docs/ux-blocks.md +376 -0
  85. data/examples/linked1.md +8 -1
  86. data/implementation-decisions.md +212 -0
  87. data/lib/cached_nested_file_reader.rb +138 -1
  88. data/lib/command_result.rb +27 -6
  89. data/lib/executed_shell_command.rb +512 -0
  90. data/lib/filter.rb +7 -7
  91. data/lib/hash_delegator.rb +403 -350
  92. data/lib/link_history.rb +22 -11
  93. data/lib/markdown_exec/version.rb +1 -1
  94. data/lib/mdoc.rb +103 -44
  95. data/lib/menu.src.yml +110 -83
  96. data/lib/menu.yml +149 -83
  97. data/lib/transformed_shell_command.rb +449 -0
  98. data/lib/wl.rb +15 -0
  99. data/lib/ww.rb +16 -5
  100. data/requirements.md +111 -0
  101. data/semantic-tokens.md +132 -0
  102. data/tasks.md +69 -0
  103. metadata +26 -4
  104. data/docs/ux-blocks-examples.md +0 -120
  105. data/docs/ux-blocks-init-act.md +0 -100
@@ -0,0 +1,449 @@
1
+ #!/usr/bin/env -S bundle exec ruby
2
+ # frozen_string_literal: true
3
+
4
+ # encoding=utf-8
5
+
6
+ require_relative 'executed_shell_command'
7
+
8
+ ##
9
+ # TransformedShellCommand executes a shell command and provides access to
10
+ # a transformed version of the output.
11
+ #
12
+ # The class accepts:
13
+ # * The same arguments as ExecutedShellCommand (command, chdir, env)
14
+ # * A regex pattern with named capture groups (supports multi-line output)
15
+ # * A format that can be either:
16
+ # - A Symbol: calls that method on the output (e.g., :strip, :upcase)
17
+ # - A String: format string with named placeholders like '%{group_name}'
18
+ #
19
+ # The command is executed automatically during initialization, and the
20
+ # entire output (including multi-line output) is transformed in a single
21
+ # transformation operation using the format.
22
+ #
23
+ # The regex pattern can match across multiple lines. Use the multiline
24
+ # flag (m) or construct patterns that handle newlines appropriately.
25
+ #
26
+ # Basic usage with format string:
27
+ #
28
+ # regex = /(?<name>\w+):(?<value>\d+)/
29
+ # format_str = "Name: %{name}, Value: %{value}"
30
+ # cmd = TransformedShellCommand.new("echo 'user:123'", regex: regex, format: format_str)
31
+ # cmd.transformed_output # => "Name: user, Value: 123"
32
+ # cmd.result # => original ExecutedShellCommand::Result
33
+ #
34
+ # Basic usage with Symbol:
35
+ #
36
+ # cmd = TransformedShellCommand.new("echo ' hello '", regex: /.*/, format: :strip)
37
+ # cmd.transformed_output # => "hello"
38
+ #
39
+ # Multi-line output example:
40
+ #
41
+ # regex = /(?<first>\w+)\n(?<second>\w+)\n(?<third>\w+)/m
42
+ # format_str = "%{first}-%{second}-%{third}"
43
+ # cmd = TransformedShellCommand.new("echo -e 'one\ntwo\nthree'", regex: regex, format: format_str)
44
+ # cmd.transformed_output # => "one-two-three"
45
+ #
46
+ class TransformedShellCommand
47
+ attr_reader :command, :env, :chdir, :regex, :format
48
+
49
+ def initialize(command, regex:, format:, chdir: nil, env: {})
50
+ @command = command
51
+ @chdir = chdir
52
+ @env = env
53
+ @regex = regex && (regex.is_a?(Regexp) ? regex : Regexp.new(regex))
54
+ @format = format
55
+ @result = nil
56
+ @transformed_output = nil
57
+ execute_and_transform
58
+ end
59
+
60
+ ##
61
+ # Returns the transformed output string.
62
+ # The transformation is performed once during initialization and memoized.
63
+ #
64
+ attr_reader :transformed_output
65
+
66
+ ##
67
+ # Returns the original ExecutedShellCommand result.
68
+ #
69
+ def result
70
+ @executed_command.result
71
+ end
72
+
73
+ # Convenience delegators to the original result:
74
+
75
+ def stdout
76
+ result.stdout
77
+ end
78
+
79
+ def stderr
80
+ result.stderr
81
+ end
82
+
83
+ def exit_code
84
+ result.exit_code
85
+ end
86
+
87
+ def success?
88
+ result.success?
89
+ end
90
+
91
+ def failure?
92
+ !result.success?
93
+ end
94
+
95
+ def duration
96
+ result.duration
97
+ end
98
+
99
+ def started_at
100
+ result.started_at
101
+ end
102
+
103
+ def finished_at
104
+ result.finished_at
105
+ end
106
+
107
+ def pid
108
+ result.pid
109
+ end
110
+
111
+ private
112
+
113
+ ##
114
+ # Execute the command and transform the output.
115
+ #
116
+ def execute_and_transform
117
+ @executed_command = ExecutedShellCommand.new(@command, chdir: @chdir,
118
+ env: @env)
119
+ @result = @executed_command.result
120
+ @transformed_output = transform_output(@result.stdout)
121
+ end
122
+
123
+ ##
124
+ # Transform the output using the regex and format.
125
+ #
126
+ # If format is a Symbol, calls that method on the value.
127
+ # If format is a String, extracts named groups and applies the format string.
128
+ # If the regex doesn't match or format is nil, returns the original value.
129
+ #
130
+ def transform_output(value)
131
+ return value if value.nil? || value.empty?
132
+ return value unless @format
133
+
134
+ # If format is a Symbol, call that method on the value
135
+ if @format.is_a?(Symbol)
136
+ return value.send(@format)
137
+ end
138
+
139
+ # Extract named groups from the value using the regex
140
+ named_groups = @regex && extract_named_groups(value, @regex)
141
+ return value unless named_groups
142
+
143
+ # Apply format string with named placeholders
144
+ apply_format_string(@format, named_groups)
145
+ rescue StandardError
146
+ # On error, return original value
147
+ value
148
+ end
149
+
150
+ ##
151
+ # Extract named groups from a string using a regex pattern.
152
+ #
153
+ # Supports multi-line strings. The regex pattern should be constructed
154
+ # to handle newlines (e.g., using the multiline flag 'm' or patterns
155
+ # that explicitly match newlines).
156
+ #
157
+ # @param str [String] the string to match (can be multi-line)
158
+ # @param pattern [Regexp] the regex pattern with named groups
159
+ # @return [Hash<Symbol, String>, nil] hash of named groups, or nil if no match
160
+ #
161
+ def extract_named_groups(str, pattern)
162
+ # Match against the entire string (including newlines)
163
+ match = str.match(pattern)
164
+ return nil unless match
165
+
166
+ match.named_captures&.transform_keys(&:to_sym)
167
+ end
168
+
169
+ ##
170
+ # Apply format string with named placeholders like '%{name}'.
171
+ #
172
+ # Replaces '%{group_name}' with the corresponding value from the named_groups hash.
173
+ #
174
+ # @param format_str [String] format string with '%{name}' placeholders
175
+ # @param named_groups [Hash<Symbol, String>] hash of named groups
176
+ # @return [String] formatted string
177
+ #
178
+ def apply_format_string(format_str, named_groups)
179
+ result = format_str.dup
180
+
181
+ # Replace each '%{name}' placeholder with the corresponding value
182
+ named_groups.each do |key, value|
183
+ placeholder = "%{#{key}}"
184
+ result.gsub!(placeholder, value.to_s)
185
+ end
186
+
187
+ result
188
+ end
189
+ end
190
+
191
+ # Test suite when running as a script
192
+ return if $PROGRAM_NAME != __FILE__
193
+
194
+ require 'bundler/setup'
195
+ Bundler.require(:default)
196
+
197
+ require 'minitest/autorun'
198
+
199
+ class TransformedShellCommandTest < Minitest::Test
200
+ def test_basic_transformation
201
+ regex = /(?<name>\w+):(?<value>\d+)/
202
+ format_str = 'Name: %{name}, Value: %{value}'
203
+ cmd = TransformedShellCommand.new(
204
+ "echo 'user:123'",
205
+ regex: regex,
206
+ format: format_str
207
+ )
208
+
209
+ assert cmd.success?
210
+ assert_equal "user:123\n", cmd.stdout
211
+ assert_equal 'Name: user, Value: 123', cmd.transformed_output
212
+ end
213
+
214
+ def test_symbol_transform
215
+ regex = /.*/
216
+ cmd = TransformedShellCommand.new(
217
+ "echo ' HELLO WORLD '",
218
+ regex: regex,
219
+ format: :strip
220
+ )
221
+
222
+ assert cmd.success?
223
+ assert_equal " HELLO WORLD \n", cmd.stdout
224
+ assert_equal 'HELLO WORLD', cmd.transformed_output
225
+ end
226
+
227
+ def test_symbol_transform_upcase
228
+ regex = /.*/
229
+ cmd = TransformedShellCommand.new(
230
+ "echo 'hello'",
231
+ regex: regex,
232
+ format: :upcase
233
+ )
234
+
235
+ assert cmd.success?
236
+ assert_equal "hello\n", cmd.stdout
237
+ assert_equal "HELLO\n", cmd.transformed_output
238
+ end
239
+
240
+ def test_no_match_returns_original
241
+ regex = /(?<name>\w+):(?<value>\d+)/
242
+ format_str = 'Name: %{name}, Value: %{value}'
243
+ cmd = TransformedShellCommand.new(
244
+ "echo 'no match here'",
245
+ regex: regex,
246
+ format: format_str
247
+ )
248
+
249
+ assert cmd.success?
250
+ assert_equal "no match here\n", cmd.stdout
251
+ assert_equal "no match here\n", cmd.transformed_output
252
+ end
253
+
254
+ def test_multiple_named_groups
255
+ regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
256
+ format_str = 'Date: %{month}/%{day}/%{year}'
257
+ cmd = TransformedShellCommand.new(
258
+ "echo '2024-12-25'",
259
+ regex: regex,
260
+ format: format_str
261
+ )
262
+
263
+ assert cmd.success?
264
+ assert_equal "2024-12-25\n", cmd.stdout
265
+ assert_equal 'Date: 12/25/2024', cmd.transformed_output
266
+ end
267
+
268
+ def test_delegates_to_result
269
+ regex = /(?<name>\w+)/
270
+ format_str = '%{name}'
271
+ cmd = TransformedShellCommand.new(
272
+ "echo 'test'",
273
+ regex: regex,
274
+ format: format_str
275
+ )
276
+
277
+ assert_kind_of ExecutedShellCommand::Result, cmd.result
278
+ assert_equal "test\n", cmd.stdout
279
+ assert_equal 0, cmd.exit_code
280
+ assert cmd.success?
281
+ assert_kind_of Numeric, cmd.duration
282
+ assert_kind_of Time, cmd.started_at
283
+ assert_kind_of Time, cmd.finished_at
284
+ assert_kind_of Integer, cmd.pid
285
+ end
286
+
287
+ def test_with_chdir
288
+ regex = /(?<content>.*)/
289
+ format_str = 'Content: %{content}'
290
+ Dir.mktmpdir do |tmpdir|
291
+ test_file = File.join(tmpdir, 'test.txt')
292
+ File.write(test_file, 'hello world')
293
+
294
+ cmd = TransformedShellCommand.new(
295
+ 'cat test.txt',
296
+ regex: regex,
297
+ format: format_str,
298
+ chdir: tmpdir
299
+ )
300
+
301
+ assert cmd.success?
302
+ assert_equal 'hello world', cmd.stdout
303
+ assert_equal 'Content: hello world', cmd.transformed_output
304
+ end
305
+ end
306
+
307
+ def test_with_env
308
+ regex = /(?<var>\w+)=(?<val>\w+)/
309
+ format_str = '%{var} is %{val}'
310
+ cmd = TransformedShellCommand.new(
311
+ "echo 'TEST_VAR=test_value'",
312
+ regex: regex,
313
+ format: format_str,
314
+ env: { 'CUSTOM_VAR' => 'custom_value' }
315
+ )
316
+
317
+ assert cmd.success?
318
+ assert_equal "TEST_VAR=test_value\n", cmd.stdout
319
+ assert_equal 'TEST_VAR is test_value', cmd.transformed_output
320
+ end
321
+
322
+ def test_regex_as_string
323
+ regex_str = '(?<name>\\w+):(?<value>\\d+)'
324
+ format_str = 'Name: %{name}, Value: %{value}'
325
+ cmd = TransformedShellCommand.new(
326
+ "echo 'user:123'",
327
+ regex: regex_str,
328
+ format: format_str
329
+ )
330
+
331
+ assert cmd.success?
332
+ assert_equal 'Name: user, Value: 123', cmd.transformed_output
333
+ end
334
+
335
+ def test_format_string_with_literal_text
336
+ regex = /(?<num>\d+)/
337
+ format_str = 'The number is %{num}!'
338
+ cmd = TransformedShellCommand.new(
339
+ "echo '42'",
340
+ regex: regex,
341
+ format: format_str
342
+ )
343
+
344
+ assert cmd.success?
345
+ assert_equal "42\n", cmd.stdout
346
+ assert_equal 'The number is 42!', cmd.transformed_output
347
+ end
348
+
349
+ def test_empty_output
350
+ regex = /(?<name>\w+)/
351
+ format_str = '%{name}'
352
+ cmd = TransformedShellCommand.new(
353
+ 'true',
354
+ regex: regex,
355
+ format: format_str
356
+ )
357
+
358
+ assert cmd.success?
359
+ assert_equal '', cmd.stdout
360
+ assert_equal '', cmd.transformed_output
361
+ end
362
+
363
+ def test_multiple_placeholders_same_group
364
+ regex = /(?<word>\w+)/
365
+ format_str = '%{word} %{word} %{word}'
366
+ cmd = TransformedShellCommand.new(
367
+ "echo 'hello'",
368
+ regex: regex,
369
+ format: format_str
370
+ )
371
+
372
+ assert cmd.success?
373
+ assert_equal "hello\n", cmd.stdout
374
+ assert_equal 'hello hello hello', cmd.transformed_output
375
+ end
376
+
377
+ def test_multiline_output_single_transformation
378
+ regex = /(?<first>\w+)\n(?<second>\w+)\n(?<third>\w+)/m
379
+ format_str = '%{first}-%{second}-%{third}'
380
+ cmd = TransformedShellCommand.new(
381
+ "printf 'one\ntwo\nthree\n'",
382
+ regex: regex,
383
+ format: format_str
384
+ )
385
+
386
+ assert cmd.success?
387
+ assert_equal "one\ntwo\nthree\n", cmd.stdout
388
+ assert_equal 'one-two-three', cmd.transformed_output
389
+ end
390
+
391
+ def test_multiline_output_with_multiline_regex
392
+ regex = /Name: (?<name>[\w\s]+)\nAge: (?<age>\d+)\nCity: (?<city>[\w\s]+)/m
393
+ format_str = '%{name} (%{age}) from %{city}'
394
+ cmd = TransformedShellCommand.new(
395
+ "printf 'Name: John Doe\nAge: 30\nCity: New York\n'",
396
+ regex: regex,
397
+ format: format_str
398
+ )
399
+
400
+ assert cmd.success?
401
+ assert_includes cmd.stdout, 'John Doe'
402
+ assert_includes cmd.stdout, '30'
403
+ assert_includes cmd.stdout, 'New York'
404
+ # Remove trailing newline for comparison
405
+ assert_equal 'John Doe (30) from New York', cmd.transformed_output.chomp
406
+ end
407
+
408
+ def test_multiline_output_symbol_transform
409
+ regex = /.*/m
410
+ cmd = TransformedShellCommand.new(
411
+ "printf ' line1\n line2\n line3 \n'",
412
+ regex: regex,
413
+ format: :strip
414
+ )
415
+
416
+ assert cmd.success?
417
+ assert_equal " line1\n line2\n line3 \n", cmd.stdout
418
+ # strip removes leading/trailing whitespace from entire string
419
+ # Leading spaces from first line and trailing spaces/newline from last line are removed
420
+ assert_equal "line1\n line2\n line3", cmd.transformed_output
421
+ end
422
+
423
+ def test_multiline_output_captures_spanning_lines
424
+ regex = /Start: (?<start>.*?)End: (?<end>.*?)$/m
425
+ format_str = 'From %{start} to %{end}'
426
+ cmd = TransformedShellCommand.new(
427
+ "printf 'Start: alpha\nbeta\ngamma\nEnd: delta\n'",
428
+ regex: regex,
429
+ format: format_str
430
+ )
431
+
432
+ assert cmd.success?
433
+ assert_equal "From alpha\nbeta\ngamma\n to delta", cmd.transformed_output
434
+ end
435
+
436
+ def test_multiline_output_no_match_returns_original
437
+ regex = /(?<name>\w+):(?<value>\d+)/
438
+ format_str = 'Name: %{name}, Value: %{value}'
439
+ cmd = TransformedShellCommand.new(
440
+ "printf 'line1\nline2\nline3\n'",
441
+ regex: regex,
442
+ format: format_str
443
+ )
444
+
445
+ assert cmd.success?
446
+ assert_equal "line1\nline2\nline3\n", cmd.stdout
447
+ assert_equal "line1\nline2\nline3\n", cmd.transformed_output
448
+ end
449
+ end
data/lib/wl.rb ADDED
@@ -0,0 +1,15 @@
1
+ require "binding_of_caller"
2
+
3
+ def show(name)
4
+ # look one frame up and fetch the caller's local variable
5
+ value = binding.of_caller(1).local_variable_get(name)
6
+ puts name.to_s + ': ' + value.to_s
7
+ end
8
+
9
+ def main
10
+ x = 1
11
+ show(:x)
12
+ end
13
+
14
+ main()
15
+
data/lib/ww.rb CHANGED
@@ -392,7 +392,10 @@ class Array
392
392
  raise ArgumentError,
393
393
  'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
394
394
 
395
- partition { |item| item.respond_to?(method) && item.send(method) == value }
395
+ partition do |item|
396
+ item.respond_to?(method) &&
397
+ item.send(method) == value
398
+ end
396
399
  rescue NoMethodError => err
397
400
  warn "Method #{method} not available on some items: #{err.message}"
398
401
  [[], self]
@@ -412,7 +415,10 @@ class Array
412
415
  raise ArgumentError,
413
416
  'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
414
417
 
415
- reject { |item| item.respond_to?(method) && item.send(method) == value }
418
+ reject do |item|
419
+ item.respond_to?(method) &&
420
+ item.send(method) == value
421
+ end
416
422
  end
417
423
  rescue NoMethodError => err
418
424
  warn "Method #{method} not available on some items: #{err.message}"
@@ -433,7 +439,10 @@ class Array
433
439
  raise ArgumentError,
434
440
  'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
435
441
 
436
- select { |item| item.respond_to?(method) && item.send(method) == value }
442
+ select do |item|
443
+ item.respond_to?(method) &&
444
+ item.send(method) == value
445
+ end
437
446
  end
438
447
  rescue NoMethodError => err
439
448
  warn "Method #{method} not available on some items: #{err.message}"
@@ -704,8 +713,10 @@ class TestWwFunction < Minitest::Test
704
713
  $debug = true
705
714
 
706
715
  # Test that wwa exits (we can't easily test exit behavior in minitest)
707
- # So we'll just verify it would call ww0 properly by testing the structure
708
- # Note: wwa calls exit, so we can't test it directly without special handling
716
+ # So we'll just verify it would call ww0 properly by testing the
717
+ # structure
718
+ # Note: wwa calls exit, so we can't test it directly without special
719
+ # handling
709
720
  skip 'wwa exits the program, cannot test directly in minitest'
710
721
  end
711
722
 
data/requirements.md ADDED
@@ -0,0 +1,111 @@
1
+ # Requirements
2
+
3
+ **STDD Methodology Version**: 1.0.0
4
+
5
+ ## Overview
6
+ This document defines the functional and non-functional requirements for your project. Each requirement should have a unique semantic token `[REQ:IDENTIFIER]` for traceability.
7
+
8
+ ### Requirement Structure
9
+
10
+ Each requirement includes:
11
+ - **Description**: What the requirement specifies
12
+ - **Rationale**: Why the requirement exists
13
+ - **Satisfaction Criteria**: How we know the requirement is satisfied (acceptance criteria, success conditions)
14
+ - **Validation Criteria**: How we verify/validate the requirement is met (testing approach, verification methods, success metrics)
15
+
16
+ **Note**: Validation criteria defined here inform the testing strategy documented in `architecture-decisions.md` and the specific test implementations in `implementation-decisions.md`.
17
+
18
+ ## Core Functionality
19
+
20
+ ### 1. [REQ:SHEBANG_HIDING] CLI Option to Hide Shebang Lines in Document Output
21
+
22
+ **Priority: P1 (Important)**
23
+
24
+ - **Description**: MDE shall provide a CLI option that, when enabled (default), causes the shebang line (lines starting with `#!`) to be extracted from input file(s) and not displayed as part of the document output. The initial implementation will extract shebang lines during the cached nested read process.
25
+ - **Rationale**: Shebang lines are execution directives for scripts and are not typically part of the document content when displayed. Users may want to include shebang lines in their markdown source files for direct execution (e.g., `#!/usr/bin/env mde`), but these should not appear in the rendered document output by default. This improves document readability and follows common markdown processing conventions.
26
+ - **Satisfaction Criteria** (How we know the requirement is satisfied):
27
+ - A CLI option exists (e.g., `--hide-shebang` or `--no-show-shebang`) that controls shebang line visibility
28
+ - The option defaults to enabled (shebang lines are hidden by default)
29
+ - When enabled, shebang lines are extracted from input files during cached nested read
30
+ - Shebang lines are not included in the processed document output when the option is enabled
31
+ - When disabled, shebang lines are included in the document output as normal lines
32
+ - The option works with nested imports (imported files also have their shebang lines handled according to the option)
33
+ - **Validation Criteria** (How we verify/validate the requirement is met):
34
+ - Unit tests verify shebang line detection and extraction logic
35
+ - Integration tests verify shebang lines are excluded from document output when option is enabled
36
+ - Integration tests verify shebang lines are included when option is disabled
37
+ - Tests verify behavior with nested imports
38
+ - Manual verification with sample markdown files containing shebang lines
39
+ - CLI help/documentation shows the option and its default value
40
+
41
+ **Status**: ✅ Implemented
42
+
43
+ ### 2. [REQ:EXAMPLE_FEATURE] Example Feature Name
44
+
45
+ **Priority: P0 (Critical) | P1 (Important) | P2 (Nice-to-Have) | P3 (Future)**
46
+
47
+ - **Description**: Brief description of what this feature does
48
+ - **Rationale**: Why this feature is needed
49
+ - **Satisfaction Criteria** (How we know the requirement is satisfied):
50
+ - Criterion 1
51
+ - Criterion 2
52
+ - Criterion 3
53
+ - **Validation Criteria** (How we verify/validate the requirement is met):
54
+ - Validation method 1 (e.g., unit tests, integration tests, manual verification)
55
+ - Validation method 2
56
+ - Success metrics or thresholds
57
+
58
+ **Status**: ⏳ Planned | ✅ Implemented
59
+
60
+ ### 2. [REQ:ANOTHER_FEATURE] Another Feature Name
61
+
62
+ **Priority: P0 (Critical)**
63
+
64
+ - **Description**: Description of the feature
65
+ - **Rationale**: Why it's needed
66
+ - **Satisfaction Criteria** (How we know the requirement is satisfied):
67
+ - Criterion 1
68
+ - Criterion 2
69
+ - **Validation Criteria** (How we verify/validate the requirement is met):
70
+ - Validation method 1
71
+ - Validation method 2
72
+
73
+ **Status**: ⏳ Planned
74
+
75
+ ## Non-Functional Requirements
76
+
77
+ ### 1. Performance [REQ:PERFORMANCE]
78
+ - Requirement description
79
+ - Metrics or targets
80
+
81
+ ### 2. Reliability [REQ:RELIABILITY]
82
+ - Requirement description
83
+ - Availability targets
84
+
85
+ ### 3. Maintainability [REQ:MAINTAINABILITY]
86
+ - Requirement description
87
+ - Code quality standards
88
+
89
+ ### 4. Usability [REQ:USABILITY]
90
+ - Requirement description
91
+ - User experience goals
92
+
93
+ ## Edge Cases to Handle
94
+
95
+ 1. **Edge Case 1**
96
+ - Description
97
+ - Expected behavior
98
+
99
+ 2. **Edge Case 2**
100
+ - Description
101
+ - Expected behavior
102
+
103
+ ## Future Enhancements (Out of Scope)
104
+
105
+ The following features are documented but marked as future enhancements:
106
+ - Feature 1
107
+ - Feature 2
108
+ - Feature 3
109
+
110
+ These may be considered for future iterations but are not required for the initial implementation.
111
+