sevgi-function 0.95.0 → 1.0.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: 5d64555b43d632235e9d1f1419294447fc5e63c0c2af0ba3b8c89fc1f82e7474
4
- data.tar.gz: 6a15d7e54278e3d0cee74369616f60b6e43d70f8e2a7784c33a6737099fd9e4b
3
+ metadata.gz: 7fd50bb171b09d5221092de41845690e2b1e8e05e62811b972306b0522f6cbea
4
+ data.tar.gz: d8cff5f7b9af0492f0d0824271b4a2dc95069d2d4e82bbab012b56e0b15dc631
5
5
  SHA512:
6
- metadata.gz: 60083cb4298cad1644ec70bd490652b07bec34fe9498d8359653a82800fae41ac2d6128c527095af04f992a351bd260fb870761289537c3a5778dc71c214506a
7
- data.tar.gz: bfc1aeaa49fb6a08cb7b78f14dc5e8b077e3d09de7bc52f92bc460692f5ec5831c2a30cae5ddde96b49b9ed0c494546f884b12af026557c0d8e676171d272130
6
+ metadata.gz: ca877b117f6e9e7d7a3da866b3ac73d6120c820038e0192e0e55bb927c9b28c527f68b16ecf02fddbd69f47435fdb940214e70fe78f2f3d96e4f3a006a9732cd
7
+ data.tar.gz: 5cced2a22f74bb0836f96d29de58d2d2dd1052d871bcc24bce6ffd1e59ee2760ab71e5bb2f83d36e7cf6bcdb30e3d3f0f6a4c9f7ca755bde02f2cb405948b16c
data/CHANGELOG.md CHANGED
@@ -3,7 +3,226 @@
3
3
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
- ## Unreleased
6
+ ## 1.0.0 - 2026-09-16
7
+
8
+ ### Added
9
+
10
+ - Added Hatch and Orb Showcase examples and displayed both arc lengths in the Arc example.
11
+
12
+ ### Changed
13
+
14
+ - Matched documentation page widths to the homepage Showcase.
15
+ - Aligned Derender user guidance with UTF-8 normalization and safe subtree selection rules.
16
+ - Clarified the packaged skill's script invocation, subtree copying, and Derender selection and encoding rules.
17
+ - Allowed Canvas and Paper values as the first `SVG` argument when using the default document profile.
18
+ - Made element `dup` and `clone` own independent subtrees while retaining IDs and Ruby copy semantics.
19
+ - Made Segment equality compare length and direction; ordering still compares length only.
20
+ - Limited Tile cell, row, and column queries to the requested geometry and accelerated XML character validation.
21
+ - Simplified the first-run guide and aligned library constructor examples.
22
+ - Updated the packaged skill for canvas origins, alignment, Paper lookup, curved geometry, and export constraints.
23
+ - Revised DSL catalog examples and packaged Sevgi skill guidance to use statement-style calls and show inspection results.
24
+ - Refined Showcase example tabs, made Ruby and XML views fill their panels, and hid executable preambles from Ruby views.
25
+ - Documented non-rendering element data and clarified the related DSL catalog examples.
26
+ - Refined Showcase heading spacing, page-title colors, and DSL catalog cards across light and dark themes.
27
+ - Removed an obsolete ruler example and unused site code.
28
+ - Kept the Squared and Copperplate guidesheet examples together at the end of the Showcase.
29
+ - Updated the packaged skill to avoid duplicate semantic shapes and unnecessary paint passes.
30
+
31
+ ### Fixed
32
+
33
+ - Kept horizontal scrollbars visible in Showcase example code panels.
34
+ - Read Derender input in its declared XML encoding and normalized generated source, XML declarations, and markup to UTF-8.
35
+ - Preserved open-path endpoints and derived edges independently of display precision.
36
+ - Kept near-axis and near-parallel line equations and intersections independent of angle and slope rounding.
37
+ - Corrected the Showcase social image dimensions.
38
+ - Added SVG namespaces to standalone drawing examples and regenerated their outputs.
39
+ - Made failed Sevgi Rake tasks stop dependent tasks and captured required-library failures in executor results.
40
+ - Removed executor signal-handler replacement so the host and shell retain SIGINT ownership.
41
+ - Escaped late export CSS as XML text and inserted it only at a supported final root closing tag.
42
+ - Preserved Derender processing instructions and rejected custom entity references and unsupported trailing document nodes before inclusion.
43
+ - Included both box origins in `Align`, unified Tile index validation, and isolated custom numeric attribute snapshots.
44
+ - Prevented avoidable overflow and underflow in point distances.
45
+ - Corrected browser source-panel expectations and added executable documentation, YARD, and packaged-skill checks.
46
+ - Rendered inline code in DSL catalog summaries and kept linked task themes visible below the sticky header.
47
+ - Restored Showcase tab icons with embedded Font Awesome SVGs.
48
+
49
+ ## 0.100.0 - 2026-09-09
50
+
51
+ ### Added
52
+
53
+ - Added `ArcTo` and `ArcBy` for SVG-native elliptical arc paths.
54
+ - Added Geometry `Arc`, `Ellipse`, and `Circle` with bounds, length, predicates, affine transformations, and
55
+ linear/quadratic intersections. Closed ellipses and circles support sweeps and hatching.
56
+ - Added Geometry `Point.midpoint`, `Element#closed?`, lined `vertices`, open-path traversal, and `Rect#center`.
57
+ - Added Geometry aggregate bounding boxes through `Operation.box`, point collinearity through `Point.collinear?`, and
58
+ polygon `simple?`, `convex?`, and `concave?` predicates.
59
+ - Added Protractor and Arc examples and documented the drawing and calculation APIs.
60
+
61
+ ### Changed
62
+
63
+ - Removed redundant parent scans while duplicating element subtrees.
64
+ - Reused source bounds across Tile cells to reduce repeated calculations and object allocations.
65
+
66
+ ### Fixed
67
+
68
+ - Preserved Showcase card HTML through Markdown rendering so all examples and their tabs remain visible.
69
+ - Preserved XML declarations, pre-root comments, CDATA sections, inline comments, and namespace-only leaves in
70
+ Derender output and evaluation.
71
+ - Limited PDF stamp recoloring to replaced text and carried graphics state across a page's content streams.
72
+ - Corrected decimal interval fitting and made Grid point queries honor the active numeric precision.
73
+ - Prevented implicit shell interpretation of single executable arguments in `F.sh` and `F.sh!`, including paths with spaces or shell metacharacters.
74
+ - Preserved inherited namespaces on detached Derender nodes in generated source and direct evaluation, including imports through `EvaluateChildren` and `IncludeChildren`.
75
+ - Prevented sweep and Hatch lines from crossing the exterior between two tangent vertices of a concave polygon.
76
+ - Aligned Tile cells with their declared bounds for circles, rotated shapes, and polygons whose first vertex differs from their bounding-box origin.
77
+ - Limited `Load` chains to 128 active sources with `Executor::LoadDepthError`, preserving cycle detection and nested load diagnostics.
78
+ - Rejected nil and false aggregate-box inputs with the documented geometry error.
79
+ - Restored precision-aware rejection of zero-length Triangle sides.
80
+ - Made point-set collinearity independent of input order and removed eager polygon edge-pair storage.
81
+ - Applied the collinearity tolerance to every three-point subset, removing coordinate-axis-dependent baseline selection.
82
+
83
+ ## 0.99.0 - 2026-08-09
84
+
85
+ ### Added
86
+
87
+ - Added `SVG.Module` for building anonymous callable drawing modules with their contract installed before definition.
88
+ - Added responsive Mermaid diagrams for the Derender round trip and SVG validation lifecycle.
89
+
90
+ ### Changed
91
+
92
+ - Reorganized the documentation around getting started, usage, composition, documents, layout, output, and examples.
93
+ - Documented how the `igves` and `igsev` command names reflect their conversion directions.
94
+ - Normalized DSL catalog provider labels to lowercase and simplified polyfill-backed editor entries to `inkscape`.
95
+
96
+ ### Fixed
97
+
98
+ - Allowed A5-on-A4 pdfcpu conversion to replace an existing output file.
99
+ - Pre-rendered Mermaid diagrams as inline SVG instead of parsing them in site visitors' browsers.
100
+ - Removed duplicate provider and context labels from DSL catalog entries.
101
+
102
+ ## 0.98.2 - 2026-07-25
103
+
104
+ ### Fixed
105
+
106
+ - Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
107
+
108
+ ## 0.98.1 - 2026-07-20
109
+
110
+ ### Changed
111
+
112
+ - Changed the `sevgi` CLI to use the executor's isolated scope by default.
113
+
114
+ ### Removed
115
+
116
+ - Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
117
+ `Sevgi.execute` and `Sevgi.execute_file` when needed.
118
+
119
+ ## 0.98.0 - 2026-07-20
120
+
121
+ ### Added
122
+
123
+ - Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
124
+ `Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
125
+ destinations without changing physical load identity.
126
+
127
+ ## 0.97.0 - 2026-07-19
128
+
129
+ ### Added
130
+
131
+ - Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
132
+ source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
133
+ - Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
134
+ - Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
135
+
136
+ ### Changed
137
+
138
+ - Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
139
+ application dependencies.
140
+
141
+ ## 0.96.0 - 2026-07-18
142
+
143
+ ### Added
144
+
145
+ - Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
146
+ `igves --omit` exposes the same behavior from the command line.
147
+ - Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
148
+ - Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
149
+ - Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
150
+ renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
151
+ - Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
152
+ `Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
153
+
154
+ ### Changed
155
+
156
+ - Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
157
+ capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
158
+ `Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
159
+ - Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
160
+ `Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
161
+ - Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
162
+ `Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
163
+ - Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
164
+ `constraint` replace axis-implying constructor names.
165
+ - Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
166
+ - Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
167
+ - Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
168
+ - Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
169
+ - Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
170
+ file inputs now use the corresponding `File`-suffixed entrypoints.
171
+ - Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
172
+ inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
173
+ - Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
174
+ is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
175
+ `Stay` for traversal stop tokens.
176
+ - Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
177
+ return nil from `<=>`.
178
+ - Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
179
+ `execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
180
+ - Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
181
+ unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
182
+ elements, and Grid query values.
183
+ - Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
184
+ `.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
185
+ - Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
186
+ String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
187
+ - Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
188
+ public state while parser, strategy, and construction plumbing remain private.
189
+ - Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
190
+ forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
191
+
192
+ ### Fixed
193
+
194
+ - Rejected directories from default file discovery while preserving custom locator matchers.
195
+ - Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
196
+ - Preserved source Canvas units and names when deriving fitted Grid canvases.
197
+ - Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
198
+ tablet menu at 1024px and below.
199
+ - Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
200
+ arbitrary user-supplied attributes remain untouched.
201
+ - Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
202
+ rendered id value, including false and numeric values, through its serialized string.
203
+ - Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
204
+ and validated raw output paths before expansion or rendering.
205
+ - Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
206
+ false executor boot receivers remain explicit rather than defaulting to an internal scope.
207
+ - Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
208
+ executor results retain owned immutable snapshots where their public contracts promise value semantics.
209
+ - Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
210
+ snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
211
+
212
+ ### Removed
213
+
214
+ - Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
215
+ immutable.
216
+ - Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
217
+ executables remain unchanged.
218
+ - Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
219
+ now keep the harness under the Showcase namespace.
220
+ - Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
221
+ - Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
222
+ - Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
223
+ direct construction of abstract or internally wired Content, Element, and Grid query types.
224
+ - Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
225
+ bracket constructors, and direct Stop construction.
7
226
 
8
227
  ## 0.95.0 - 2026-07-11
9
228
 
@@ -87,7 +306,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
87
306
 
88
307
  ### Changed
89
308
 
90
- - Added the checker board example to the documentation showcase flow.
309
+ - Added the checkerboard example to the documentation showcase flow.
91
310
  - Consolidated Victor Book attribution for adapted showcase examples into a single documentation note.
92
311
 
93
312
  ### Fixed
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Sevgi Function
2
2
 
3
- Shared helper functions used by Sevgi components.
3
+ Sevgi Function contains the supported `Sevgi::F` toolbox shared by Sevgi components and advanced extensions. It is not
4
+ a general-purpose utility library. Nested helper modules organize the facade implementation and are not consumer
5
+ mixins.
4
6
 
5
7
  ## Install
6
8
 
@@ -17,20 +19,25 @@ require "sevgi/function"
17
19
  ## Example
18
20
 
19
21
  ```ruby
20
- Sevgi::F.eq?(0.1 + 0.2, 0.3, precision: 12)
22
+ Sevgi::F.with_precision(12) do
23
+ Sevgi::F.eq?(0.1 + 0.2, 0.3) # => true
24
+ end
21
25
  ```
22
26
 
27
+ The facade also provides `.sevgi` file discovery, generated-file output, argv-safe child processes, naming helpers,
28
+ and terminal status output. Use the linked API documentation for exact results and failure contracts.
29
+
23
30
  ## Ruby compatibility
24
31
 
25
- Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4.0 and the current development Ruby from `.ruby-version`.
32
+ Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
26
33
 
27
34
  ## Native prerequisites
28
35
 
29
- None beyond Ruby and this gem's Ruby dependencies.
36
+ This gem needs only Ruby and its Ruby dependencies.
30
37
 
31
38
  ## Links
32
39
 
33
- - Documentation: https://sevgi.roktas.dev
34
- - API documentation: https://www.rubydoc.info/gems/sevgi-function
35
- - Source: https://github.com/roktas/sevgi/tree/main/function
36
- - Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
40
+ - Documentation: <https://sevgi.roktas.dev>
41
+ - API documentation: <https://www.rubydoc.info/gems/sevgi-function>
42
+ - Source: <https://github.com/roktas/sevgi/tree/main/function>
43
+ - Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
data/lib/sevgi/core.rb CHANGED
@@ -39,7 +39,7 @@ module Sevgi
39
39
  end
40
40
  end
41
41
 
42
- # Error raised for internal invariants and implementation paths that should be unreachable.
42
+ # Error raised for broken internal invariants and unreachable implementation paths.
43
43
  PanicError = Class.new(Error) unless defined?(self::PanicError)
44
44
 
45
45
  # Error raised for invalid public API usage.
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Sevgi
4
4
  module Function
5
- # ANSI color and style helpers for terminal output.
5
+ # ANSI color and style methods promoted to {Sevgi::F}. This module organizes the facade implementation. It is not a
6
+ # consumer mixin contract.
6
7
  module Color
7
8
  # Wraps a string in the blue terminal style.
8
9
  # @param string [Object] content to style
@@ -1,27 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
3
  require "fileutils"
5
4
 
6
5
  module Sevgi
7
6
  module Function
8
- # File-system helpers used by build scripts and DSL support code.
7
+ # File-system methods promoted to {Sevgi::F}. This module organizes the facade implementation. It is not a consumer
8
+ # mixin contract.
9
9
  module File
10
- # Checks whether a file would change if written with content.
10
+ # Checks whether a file differs from the supplied content.
11
11
  # @param file [String] file path to compare
12
12
  # @param content [String] proposed file content
13
13
  # @yield optional normalization filter applied to both old and new content
14
14
  # @yieldparam content [String] content to normalize
15
15
  # @yieldreturn [String]
16
16
  # @return [Boolean] true when the file is missing or content differs
17
- # @raise [Errno::EACCES] when the file cannot be read
17
+ # @raise [SystemCallError] when the file cannot be inspected or read
18
18
  def changed?(file, content, &filter)
19
19
  return true unless ::File.exist?(file)
20
20
 
21
21
  old_content = ::File.read(file)
22
22
  old_content, content = [old_content, content].map(&filter) if filter
23
23
 
24
- Digest::SHA1.digest(old_content) != Digest::SHA1.digest(content)
24
+ old_content != content
25
25
  end
26
26
 
27
27
  # Finds an existing file by exact path or by trying default extensions.
@@ -29,11 +29,11 @@ module Sevgi
29
29
  # @param extensions [Array<String>] extensions to try when file has no extension
30
30
  # @return [String, nil] matching file path, or nil when no file is found
31
31
  def existing(file, extensions)
32
- return file if ::File.exist?(file)
32
+ return file if ::File.file?(file)
33
33
  return nil unless ::File.extname(file).empty?
34
34
  return nil if extensions.empty?
35
35
 
36
- extensions.map { |ext| "#{file}.#{ext}" }.detect { |file| ::File.exist?(file) }
36
+ extensions.map { |ext| "#{file}.#{ext}" }.detect { |file| ::File.file?(file) }
37
37
  end
38
38
 
39
39
  # Finds an existing file or raises.
@@ -79,8 +79,7 @@ module Sevgi
79
79
  # @yieldparam content [String] old or new content
80
80
  # @yieldreturn [String]
81
81
  # @return [String, nil] expanded file path when written, otherwise nil
82
- # @raise [Errno::EACCES] when the file cannot be read or written
83
- # @raise [Errno::ENOENT] when the parent directory does not exist
82
+ # @raise [SystemCallError] when the destination cannot be inspected, read, or written
84
83
  def out(content, *paths, &filter)
85
84
  if paths.empty?
86
85
  ::Kernel.puts(content)
@@ -122,7 +121,7 @@ module Sevgi
122
121
  # Creates a file and any missing parent directories.
123
122
  # @param paths [Array<String>] path components for the file
124
123
  # @return [String] touched file path
125
- # @raise [Errno::EACCES] when the file or parent directory cannot be created
124
+ # @raise [SystemCallError] when the file or parent directory cannot be created
126
125
  def touch(*paths)
127
126
  ::File.join(*paths).tap do |path|
128
127
  ::FileUtils.mkdir_p(::File.dirname(path))
@@ -10,31 +10,45 @@ module Sevgi
10
10
  # @return [String] candidate path that matched
11
11
  # @!attribute [r] dir
12
12
  # @return [String] directory where the match was found
13
- Location = Data.define(:file, :slug, :dir)
13
+ Location = Data.define(:file, :slug, :dir) do
14
+ # Creates an owned immutable location snapshot.
15
+ # @param file [String] absolute matching file path
16
+ # @param slug [String] candidate path that matched
17
+ # @param dir [String] directory where the match was found
18
+ # @return [void]
19
+ def initialize(file:, slug:, dir:)
20
+ super(file: file.dup.freeze, slug: slug.dup.freeze, dir: dir.dup.freeze)
21
+ end
22
+
23
+ private_class_method :[]
24
+ end
14
25
 
15
- # Locates one of several candidate files by walking upward from a start directory.
26
+ # Locates one of several candidate files by walking upward from an immutable configuration snapshot. Each call
27
+ # observes the filesystem again and returns an owned immutable {Location} snapshot.
16
28
  class Locate
17
29
  # @overload call(paths, start = Dir.pwd, exclude: nil, &block)
18
30
  # Builds a locator and runs it.
19
31
  # @param paths [Array<String>, String] candidate file paths
20
32
  # @param start [String] directory where lookup starts
21
33
  # @param exclude [Array<String>, String, nil] paths ignored during lookup
22
- # @yield optional matcher used instead of file existence checks
34
+ # @yield optional matcher used instead of built-in file checks
23
35
  # @yieldparam path [String] candidate path
24
36
  # @yieldreturn [Boolean]
25
37
  # @return [Sevgi::Function::Location, nil] found location, or nil
38
+ # @raise [Errno::ENOENT] when the start directory does not exist
39
+ # @raise [Errno::ENOTDIR] when the start path is not a directory
26
40
  def self.call(*, **, &block) = new(*, **).call(&block)
27
41
 
28
- # Returns candidate paths.
29
- # @return [Array<String>]
42
+ # Returns the frozen owned candidate paths.
43
+ # @return [Array<String>] frozen candidate path strings
30
44
  attr_reader :paths
31
45
 
32
- # Returns the absolute start directory.
33
- # @return [String]
46
+ # Returns the frozen absolute start directory.
47
+ # @return [String] frozen absolute path
34
48
  attr_reader :start
35
49
 
36
- # Returns expanded paths ignored during lookup.
37
- # @return [Array<String>, nil]
50
+ # Returns the frozen owned paths ignored during lookup.
51
+ # @return [Array<String>, nil] frozen absolute path strings, or nil
38
52
  attr_reader :exclude
39
53
 
40
54
  # Builds an upward file locator.
@@ -43,13 +57,14 @@ module Sevgi
43
57
  # @param exclude [Array<String>, String, nil] paths ignored during lookup after absolute expansion
44
58
  # @return [void]
45
59
  def initialize(paths, start = ::Dir.pwd, exclude: nil)
46
- @paths = Array(paths)
47
- @start = ::File.expand_path(start)
48
- @exclude = [*exclude].map { ::File.expand_path(it) } unless exclude.nil?
60
+ @paths = Array(paths).map { it.dup.freeze }.freeze
61
+ @start = ::File.expand_path(start).freeze
62
+ @exclude = [*exclude].map { ::File.expand_path(it).freeze }.freeze unless exclude.nil?
63
+ freeze
49
64
  end
50
65
 
51
66
  # Runs the upward lookup.
52
- # @yield optional matcher used instead of file existence checks
67
+ # @yield optional matcher used instead of built-in file checks
53
68
  # @yieldparam path [String] absolute candidate path
54
69
  # @yieldreturn [Boolean]
55
70
  # @note Absolute exclusions are applied before the default or custom matcher.
@@ -64,7 +79,7 @@ module Sevgi
64
79
 
65
80
  slug, file = found
66
81
 
67
- return Location[file, slug, here]
82
+ return Location.new(file:, slug:, dir: here)
68
83
  end
69
84
  end
70
85
 
@@ -90,7 +105,8 @@ module Sevgi
90
105
  candidate = ::File.expand_path(path, here)
91
106
  next if excluded?(candidate)
92
107
 
93
- return [path, candidate] if (block || proc { ::File.exist?(it) }).call(candidate)
108
+ matched = block ? block.call(candidate) : ::File.file?(candidate)
109
+ return [path, candidate] if matched
94
110
  end
95
111
 
96
112
  nil
@@ -109,6 +125,8 @@ module Sevgi
109
125
  # @param extension [String] default extension added before lookup
110
126
  # @return [Sevgi::Function::Location] found location
111
127
  # @raise [Sevgi::Error] when no matching file exists
128
+ # @raise [Errno::ENOENT] when the start directory does not exist
129
+ # @raise [Errno::ENOTDIR] when the start path is not a directory
112
130
  def self.locate(filename, start, exclude: nil, extension: EXTENSION)
113
131
  Locate.(F.qualify(filename, extension), start, exclude:).tap do |path|
114
132
  Error.("Cannot load a file matching: #{filename}") unless path
@@ -2,14 +2,17 @@
2
2
 
3
3
  module Sevgi
4
4
  module Function
5
- # Numeric and trigonometric helpers used by geometry and DSL code.
5
+ # Numeric and trigonometric methods promoted to {Sevgi::F}. This module owns the public thread-local
6
+ # {Math.precision} configuration but is not otherwise a consumer mixin contract.
6
7
  module Math
7
8
  # Default decimal precision used by approximate comparisons.
8
9
  PRECISION = 6
9
10
 
10
11
  PRECISION_KEY = :sevgi_function_math_precision
12
+ QUADRANT_COSINES = [1.0, 0.0, -1.0, 0.0].freeze
13
+ QUADRANT_SINES = [0.0, 1.0, 0.0, -1.0].freeze
11
14
 
12
- private_constant :PRECISION_KEY
15
+ private_constant :PRECISION_KEY, :QUADRANT_COSINES, :QUADRANT_SINES
13
16
 
14
17
  # Returns the current thread's default numeric precision.
15
18
  # @return [Integer] current thread precision, or {PRECISION} when no override is set
@@ -22,60 +25,78 @@ module Sevgi
22
25
  # Sets or clears the current thread's default numeric precision.
23
26
  # @param precision [Integer, nil] precision override, or nil to return to {PRECISION}
24
27
  # @return [Integer, nil] assigned precision
28
+ # @raise [Sevgi::ArgumentError] when precision is not an Integer or nil
25
29
  def self.precision=(precision)
30
+ unless precision.nil? || precision.is_a?(::Integer)
31
+ ArgumentError.("Precision must be an Integer or nil: #{precision.inspect}")
32
+ end
33
+
26
34
  Thread.current.thread_variable_set(PRECISION_KEY, precision)
27
35
  end
28
36
 
29
37
  # Returns the inverse cosine in degrees.
30
38
  # @param value [Numeric] cosine value
31
39
  # @return [Float]
32
- # @raise [Math::DomainError] when value is outside -1..1
33
- def acos(value) = to_degrees(::Math.acos(value))
40
+ # @raise [Sevgi::ArgumentError] when value is not a finite real number
41
+ # @raise [::Math::DomainError] when value is outside -1..1
42
+ def acos(value) = to_degrees(::Math.acos(finite_real(:value, value)))
34
43
 
35
44
  # Returns the inverse cotangent in degrees.
36
45
  # @param value [Numeric] cotangent value
37
46
  # @return [Float]
38
- def acot(value) = 90.0 - to_degrees(::Math.atan(value))
47
+ # @raise [Sevgi::ArgumentError] when value is not a finite real number
48
+ def acot(value) = 90.0 - to_degrees(::Math.atan(finite_real(:value, value)))
39
49
 
40
50
  # Rounds a float with an explicit or thread-local precision.
41
51
  # @param float [Numeric] value to round
42
52
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
43
53
  # @return [Numeric] rounded value
44
- # @raise [TypeError] when float cannot be rounded
54
+ # @raise [Sevgi::ArgumentError] when value is not finite real or precision is not an Integer or nil
45
55
  def approx(float, precision = nil)
46
- float.round(precision.nil? ? Function::Math.precision : precision)
56
+ precision = precision.nil? ? Function::Math.precision : valid_precision(precision)
57
+ valid_real(:value, float).round(precision)
47
58
  end
48
59
 
49
60
  # Returns the inverse sine in degrees.
50
61
  # @param value [Numeric] sine value
51
62
  # @return [Float]
52
- # @raise [Math::DomainError] when value is outside -1..1
53
- def asin(value) = to_degrees(::Math.asin(value))
63
+ # @raise [Sevgi::ArgumentError] when value is not a finite real number
64
+ # @raise [::Math::DomainError] when value is outside -1..1
65
+ def asin(value) = to_degrees(::Math.asin(finite_real(:value, value)))
54
66
 
55
67
  # Returns the inverse tangent in degrees.
56
68
  # @param value [Numeric] tangent value
57
69
  # @return [Float]
58
- def atan(value) = to_degrees(::Math.atan(value))
70
+ # @raise [Sevgi::ArgumentError] when value is not a finite real number
71
+ def atan(value) = to_degrees(::Math.atan(finite_real(:value, value)))
59
72
 
60
73
  # Returns the quadrant-aware inverse tangent in degrees.
61
74
  # @param y [Numeric] y component
62
75
  # @param x [Numeric] x component
63
76
  # @return [Float]
64
- def atan2(y, x) = to_degrees(::Math.atan2(y, x))
77
+ # @raise [Sevgi::ArgumentError] when an operand is not a finite real number
78
+ def atan2(y, x) = to_degrees(::Math.atan2(finite_real(:y, y), finite_real(:x, x)))
65
79
 
66
- # Returns the cosine of an angle expressed in degrees.
80
+ # Returns the cosine of an angle expressed in degrees. Integer quarter turns return exact `-1.0`, `0.0`, or
81
+ # `1.0`. Other angles use Ruby's floating-point Math implementation.
67
82
  # @param degrees [Numeric] angle in degrees
68
83
  # @return [Float]
69
- def cos(degrees) = ::Math.cos(to_radians(degrees))
84
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
85
+ def cos(degrees)
86
+ degrees = finite_real(:degrees, degrees)
87
+ quadrant_value(degrees, QUADRANT_COSINES) { ::Math.cos(radians(degrees)) }
88
+ end
70
89
 
71
90
  # Returns the cotangent of an angle expressed in degrees.
72
91
  # @param degrees [Numeric] angle in degrees
73
92
  # @return [Float]
93
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
74
94
  def cot(degrees) = 1.0 / ::Math.tan(to_radians(degrees))
75
95
 
76
96
  # Counts complete divisions in a length.
77
- # @param length [Numeric] finite total length
78
- # @param division [Numeric] finite, non-zero division size
97
+ # A positive quotient one floating-point step below the next integer counts as that integer.
98
+ # @param length [Numeric] finite real total length
99
+ # @param division [Numeric] finite real, non-zero division size
79
100
  # @return [Integer]
80
101
  # @raise [Sevgi::ArgumentError] when an operand is not a finite Numeric or division is zero
81
102
  def count(length, division)
@@ -83,7 +104,11 @@ module Sevgi
83
104
  divisor = finite_real(:division, division)
84
105
  ArgumentError.("Division must not be zero") if divisor.zero?
85
106
 
86
- (length / divisor).to_i
107
+ quotient = length / divisor
108
+ whole = quotient.to_i
109
+ candidate = whole + 1
110
+
111
+ quotient > whole && quotient.next_float == candidate ? candidate : whole
87
112
  end
88
113
 
89
114
  # Compares two numeric values after approximate rounding.
@@ -91,6 +116,7 @@ module Sevgi
91
116
  # @param right [Numeric] right operand
92
117
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
93
118
  # @return [Boolean]
119
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
94
120
  def eq?(left, right, precision: nil) = approx(left, precision) == approx(right, precision)
95
121
 
96
122
  # Checks whether the rounded left operand is greater than or equal to the rounded right operand.
@@ -98,6 +124,7 @@ module Sevgi
98
124
  # @param right [Numeric] right operand
99
125
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
100
126
  # @return [Boolean]
127
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
101
128
  def ge?(left, right, precision: nil) = approx(left, precision) >= approx(right, precision)
102
129
 
103
130
  # Checks whether the rounded left operand is greater than the rounded right operand.
@@ -105,6 +132,7 @@ module Sevgi
105
132
  # @param right [Numeric] right operand
106
133
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
107
134
  # @return [Boolean]
135
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
108
136
  def gt?(left, right, precision: nil) = approx(left, precision) > approx(right, precision)
109
137
 
110
138
  # Checks whether the rounded left operand is less than or equal to the rounded right operand.
@@ -112,6 +140,7 @@ module Sevgi
112
140
  # @param right [Numeric] right operand
113
141
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
114
142
  # @return [Boolean]
143
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
115
144
  def le?(left, right, precision: nil) = approx(left, precision) <= approx(right, precision)
116
145
 
117
146
  # Checks whether the rounded left operand is less than the rounded right operand.
@@ -119,33 +148,46 @@ module Sevgi
119
148
  # @param right [Numeric] right operand
120
149
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
121
150
  # @return [Boolean]
151
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
122
152
  def lt?(left, right, precision: nil) = approx(left, precision) < approx(right, precision)
123
153
 
124
154
  # Rounds a value only when precision is present.
125
155
  # @param float [Numeric] value to round
126
156
  # @param precision [Integer, nil] explicit precision, or nil to return float unchanged
127
157
  # @return [Numeric]
128
- def round(float, precision) = precision ? float.round(precision) : float
158
+ # @raise [Sevgi::ArgumentError] when value is not finite real or precision is not an Integer or nil
159
+ def round(float, precision)
160
+ number = valid_real(:value, float)
161
+ precision.nil? ? number : number.round(valid_precision(precision))
162
+ end
129
163
 
130
- # Returns the sine of an angle expressed in degrees.
164
+ # Returns the sine of an angle expressed in degrees. Integer quarter turns return exact `-1.0`, `0.0`, or `1.0`.
165
+ # other angles use Ruby's floating-point Math implementation.
131
166
  # @param degrees [Numeric] angle in degrees
132
167
  # @return [Float]
133
- def sin(degrees) = ::Math.sin(to_radians(degrees))
168
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
169
+ def sin(degrees)
170
+ degrees = finite_real(:degrees, degrees)
171
+ quadrant_value(degrees, QUADRANT_SINES) { ::Math.sin(radians(degrees)) }
172
+ end
134
173
 
135
174
  # Returns the tangent of an angle expressed in degrees.
136
175
  # @param degrees [Numeric] angle in degrees
137
176
  # @return [Float]
177
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
138
178
  def tan(degrees) = ::Math.tan(to_radians(degrees))
139
179
 
140
180
  # Converts radians to degrees.
141
181
  # @param radians [Numeric] angle in radians
142
182
  # @return [Float]
143
- def to_degrees(radians) = radians.to_f * 180 / ::Math::PI
183
+ # @raise [Sevgi::ArgumentError] when radians is not a finite real number
184
+ def to_degrees(radians) = finite_real(:radians, radians) * 180 / ::Math::PI
144
185
 
145
186
  # Converts degrees to radians.
146
187
  # @param degrees [Numeric] angle in degrees
147
188
  # @return [Float]
148
- def to_radians(degrees) = degrees.to_f / 180 * ::Math::PI
189
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
190
+ def to_radians(degrees) = radians(finite_real(:degrees, degrees))
149
191
 
150
192
  # Runs a block with a current-thread precision override.
151
193
  # @param precision [Integer, nil] scoped precision, or nil to use {PRECISION}
@@ -153,6 +195,10 @@ module Sevgi
153
195
  # @yieldreturn [Object]
154
196
  # @return [Object] block return value
155
197
  # @raise [Sevgi::ArgumentError] when no block is given
198
+ # @raise [Sevgi::ArgumentError] when precision is not an Integer or nil
199
+ # @example Compare values under a temporary precision
200
+ # Sevgi::F.with_precision(2) { Sevgi::F.eq?(1.001, 1.0) } # => true
201
+ # Sevgi::Function::Math.precision # => 6
156
202
  def with_precision(precision, &block)
157
203
  ArgumentError.("Block required") unless block
158
204
 
@@ -167,19 +213,20 @@ module Sevgi
167
213
  # @param value [Numeric] value to check
168
214
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
169
215
  # @return [Boolean]
216
+ # @raise [Sevgi::ArgumentError] when value is not finite real or precision is invalid
170
217
  def zero?(value, precision: nil) = eq?(value, 0.0, precision:)
171
218
 
172
219
  private
173
220
 
174
221
  def finite_real(field, value)
175
- unless value.is_a?(::Numeric)
176
- ArgumentError.("#{field} must be a finite Numeric: #{value.inspect}")
222
+ unless value.is_a?(::Numeric) && !value.is_a?(::Complex)
223
+ ArgumentError.("#{field} must be a finite real Numeric: #{value.inspect}")
177
224
  end
178
225
 
179
226
  number = begin
180
227
  value.to_f
181
228
  rescue ::StandardError => e
182
- ArgumentError.("#{field} must be a finite Numeric: #{value.inspect} (#{e.message})")
229
+ ArgumentError.("#{field} must be a finite real Numeric: #{value.inspect} (#{e.message})")
183
230
  end
184
231
 
185
232
  ArgumentError.("#{field} must be finite: #{value.inspect}") unless number.is_a?(::Float) && number.finite?
@@ -187,6 +234,26 @@ module Sevgi
187
234
  number
188
235
  end
189
236
 
237
+ def quadrant_value(degrees, values)
238
+ turn = degrees % 360
239
+ return yield unless (turn % 90).zero?
240
+
241
+ values[(turn / 90).to_i]
242
+ end
243
+
244
+ def radians(degrees) = degrees / 180 * ::Math::PI
245
+
246
+ def valid_precision(precision)
247
+ return precision if precision.is_a?(::Integer)
248
+
249
+ ArgumentError.("Precision must be an Integer or nil: #{precision.inspect}")
250
+ end
251
+
252
+ def valid_real(field, value)
253
+ finite_real(field, value)
254
+ value
255
+ end
256
+
190
257
  end
191
258
 
192
259
  extend Math
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require "shellwords"
4
5
 
5
6
  module Sevgi
6
7
  module Function
7
- # Shell execution helpers and executable lookup utilities.
8
+ # Command methods promoted to {Sevgi::F}. This module owns the public immutable {Shell::Result} value but is not
9
+ # otherwise a consumer mixin contract.
8
10
  module Shell
9
11
  # Checks whether a program exists and is executable.
10
12
  # @param program [Object] program name, absolute path, or relative slash-containing path
11
13
  # @return [Boolean] true when an executable regular file is found
12
- # @note PATH is evaluated on every call; empty PATH segments mean the current directory.
14
+ # @note PATH is evaluated on every call. Empty PATH segments mean the current directory.
13
15
  def executable?(program)
14
16
  program = program.to_s
15
17
  return false if program.empty?
@@ -24,30 +26,68 @@ module Sevgi
24
26
  # @param args [Array<Object>] command arguments
25
27
  # @return [nil]
26
28
  # @raise [Sevgi::Error] when the program cannot be found in PATH
27
- # @note The first argument is checked as one exact argv entry; it is never shell-split.
29
+ # @note The first argument is checked as one exact argv entry. It is never shell-split.
28
30
  def executable!(*args)
29
31
  program = args.first.to_s
30
32
  Error.("Missing executable: #{program}") unless executable?(program)
31
33
  end
32
34
 
33
- # Result object returned by shell commands.
34
- Result = Struct.new(:args, :outs, :errs, :exit_code) do
35
+ # Immutable result returned by shell commands.
36
+ #
37
+ # @example Inspect a command result
38
+ # require "rbconfig"
39
+ # result = Sevgi::F.sh(RbConfig.ruby, "-e", "puts 42")
40
+ # result.ok? # => true
41
+ # result.outline # => "42"
42
+ Result = Data.define(:args, :outs, :errs, :exit_code, :signal) do
35
43
  # @!attribute [r] args
36
- # @return [Array<String>] command arguments
44
+ # @return [Array<String>] frozen command arguments
37
45
  # @!attribute [r] outs
38
- # @return [Array<String>] captured stdout lines
46
+ # @return [Array<String>] frozen captured stdout lines
39
47
  # @!attribute [r] errs
40
- # @return [Array<String>] captured stderr lines
48
+ # @return [Array<String>] frozen captured stderr lines
41
49
  # @!attribute [r] exit_code
42
50
  # @return [Integer, nil] process exit code
51
+ # @!attribute [r] signal
52
+ # @return [Integer, nil] terminating signal number
53
+
54
+ # Creates an owned result snapshot.
55
+ # @param args [Array<Object>] command arguments
56
+ # @param outs [Array<String>] captured stdout lines
57
+ # @param errs [Array<String>] captured stderr lines
58
+ # @param exit_code [Integer, nil] process exit code
59
+ # @param signal [Integer, nil] terminating signal number
60
+ # @return [void]
61
+ def initialize(args:, outs:, errs:, exit_code:, signal:)
62
+ super(
63
+ args: args.map { it.to_s.dup.freeze }.freeze,
64
+ outs: outs.map { it.dup.freeze }.freeze,
65
+ errs: errs.map { it.dup.freeze }.freeze,
66
+ exit_code:,
67
+ signal:
68
+ )
69
+ end
43
70
 
44
- # Returns stdout, a separator, and stderr as one string.
45
- # @return [String]
46
- def all = [*outs, "\n\n", *errs].join("\n").strip
71
+ private_class_method :[]
72
+
73
+ # Returns captured output with one blank line between non-empty streams.
74
+ # Captured lines are joined with one newline and are not otherwise trimmed.
75
+ # @example Combine standard output and standard error
76
+ # result = Sevgi::Function::Shell::Result.new(
77
+ # args: ["tool"], outs: ["output"], errs: ["warning"], exit_code: 0, signal: nil
78
+ # )
79
+ # result.all # => "output\n\nwarning"
80
+ # @return [String] combined output, or an empty string when neither stream contains lines
81
+ def all
82
+ return err if outs.empty?
83
+ return out if errs.empty?
84
+
85
+ "#{out}\n\n#{err}"
86
+ end
47
87
 
48
88
  # Returns the command as a shell-like display string.
49
89
  # @return [String]
50
- def cmd = args.join(" ")
90
+ def cmd = ::Shellwords.join(args)
51
91
 
52
92
  # Returns captured stderr as a string.
53
93
  # @return [String]
@@ -59,7 +99,7 @@ module Sevgi
59
99
 
60
100
  # Reports whether the command exited successfully.
61
101
  # @return [Boolean]
62
- def ok? = exit_code&.zero?
102
+ def ok? = !exit_code.nil? && exit_code.zero?
63
103
 
64
104
  # Returns captured stdout as a string.
65
105
  # @return [String]
@@ -69,15 +109,18 @@ module Sevgi
69
109
  # @return [String, nil]
70
110
  def outline = outs.first
71
111
 
72
- # Builds a successful empty result.
73
- # @return [Sevgi::Function::Shell::Result]
74
- def self.dummy = new([], [], [], 0)
112
+ # Reports whether the command was terminated by a signal.
113
+ # @return [Boolean]
114
+ def signaled? = !signal.nil?
75
115
  end
76
116
 
77
117
  # Shared process-global SIGINT state for overlapping shell runners.
78
118
  # @api private
79
119
  module Signals
80
- Entry = Data.define(:runner, :pid)
120
+ Entry = Data.define(:runner, :pid) do
121
+ private_class_method :[]
122
+ end
123
+
81
124
  WAKE = "."
82
125
 
83
126
  private_constant :WAKE
@@ -183,21 +226,29 @@ module Sevgi
183
226
  # @yield optional content writer for stdin
184
227
  # @yieldreturn [String, nil]
185
228
  # @return [Sevgi::Function::Shell::Result]
229
+ # @raise [Sevgi::ArgumentError] when no command is given
186
230
  # @raise [Errno::ENOENT] when the executable cannot be spawned
187
231
  def call(*args, &input)
188
- return Result.dummy if args.empty?
232
+ ArgumentError.("Shell command required") if args.empty?
189
233
 
190
234
  @coathooks = 0
191
- outs, errs, status = Open3.popen3(*args) do |stdin, stdout, stderr, wait_thread|
235
+ outs, errs, status = Open3.popen3(*command(args)) do |stdin, stdout, stderr, wait_thread|
192
236
  capture(stdin, stdout, stderr, wait_thread, &input)
193
237
  end
194
238
 
195
- Result.new(args, outs, errs, status.exitstatus)
239
+ Result.new(args:, outs:, errs:, exit_code: status.exitstatus, signal: status.termsig)
196
240
  end
197
241
 
198
242
  private
199
243
 
200
- # rubocop:disable Lint/RescueException
244
+ def command(args)
245
+ args = args.dup
246
+ index = args.first.is_a?(::Hash) ? 1 : 0
247
+ args[index] = [args[index], args[index]] unless args[index].is_a?(::Array)
248
+ args
249
+ end
250
+
251
+ # rubocop:disable-next Lint/RescueException
201
252
  def capture(stdin, stdout, stderr, wait_thread, &input)
202
253
  registered = false
203
254
  Signals.register(self, wait_thread.pid)
@@ -212,7 +263,6 @@ module Sevgi
212
263
  close_input(stdin)
213
264
  Signals.unregister(self) if registered
214
265
  end
215
- # rubocop:enable Lint/RescueException
216
266
 
217
267
  def start_readers(stdout, stderr)
218
268
  [
@@ -286,14 +336,17 @@ module Sevgi
286
336
  end
287
337
  end
288
338
 
339
+ private_constant :Runner
340
+
289
341
  # @overload sh(*args, &block)
290
342
  # Runs a command and captures stdout, stderr, and exit status.
291
343
  # @param args [Array<String>] command and arguments
292
344
  # @yield optional stdin producer, evaluated once after output readers start
293
- # @yieldreturn [String, nil] content to write to stdin; nil writes nothing
345
+ # @yieldreturn [String, nil] content to write to stdin. Nil writes nothing
294
346
  # @return [Sevgi::Function::Shell::Result]
347
+ # @raise [Sevgi::ArgumentError] when no command is given
295
348
  # @raise [SystemCallError] when the executable cannot be spawned or process pipes cannot be opened
296
- # @raise [StandardError] when the input block raises; the child is terminated and reaped before propagation
349
+ # @raise [StandardError] when the input block raises. The child is terminated and reaped before propagation
297
350
  # @note The child's stdin is closed after the input block. During execution, the first SIGINT sends TERM to the
298
351
  # child process and the second SIGINT as KILL to each active child outside trap context, then restores the
299
352
  # previous handler.
@@ -302,11 +355,12 @@ module Sevgi
302
355
  # Runs a command, requiring both executable lookup and successful exit status.
303
356
  # @param args [Array<String>] command and arguments
304
357
  # @yield optional stdin producer, evaluated once after output readers start
305
- # @yieldreturn [String, nil] content to write to stdin; nil writes nothing
358
+ # @yieldreturn [String, nil] content to write to stdin. Nil writes nothing
306
359
  # @return [Sevgi::Function::Shell::Result]
360
+ # @raise [Sevgi::ArgumentError] when no command is given
307
361
  # @raise [Sevgi::Error] when the executable is missing or the command fails
308
362
  # @raise [SystemCallError] when the executable cannot be spawned or process pipes cannot be opened
309
- # @raise [StandardError] when the input block raises; the child is terminated and reaped before propagation
363
+ # @raise [StandardError] when the input block raises. The child is terminated and reaped before propagation
310
364
  # @note The child's stdin is closed after the input block. During execution, the first SIGINT sends TERM to the
311
365
  # child process and the second SIGINT as KILL to each active child outside trap context, then restores the previous
312
366
  # handler.
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Sevgi
4
4
  module Function
5
- # String helpers used by generated names and user-facing text.
5
+ # String methods promoted to {Sevgi::F}. This module organizes the facade implementation. It is not a consumer mixin
6
+ # contract.
6
7
  module String
7
8
  # Returns the final constant name segment from a module path.
8
9
  # @param path [Object] module, class, or string-like path
@@ -19,9 +20,11 @@ module Sevgi
19
20
 
20
21
  extend String
21
22
 
22
- # Lightweight English pluralization helper.
23
+ # English pluralization promoted to {Sevgi::F}. This module organizes the facade implementation. It is not a
24
+ # consumer mixin contract.
23
25
  module Pluralize
24
- # Words that should not be pluralized.
26
+ # Words excluded from pluralization.
27
+ # @api private
25
28
  UNCOUNTABLES = %w[
26
29
  equipment
27
30
  fish
@@ -36,6 +39,7 @@ module Sevgi
36
39
  .freeze
37
40
 
38
41
  # Singular-to-plural forms that do not follow suffix rules.
42
+ # @api private
39
43
  IRREGULARS = Hash[
40
44
  *%w[
41
45
  child
@@ -59,9 +63,11 @@ module Sevgi
59
63
  .freeze
60
64
 
61
65
  # Plural forms already accepted as plural.
66
+ # @api private
62
67
  PLURALS = IRREGULARS.invert.freeze
63
68
 
64
69
  # Ordered suffix replacement rules.
70
+ # @api private
65
71
  RULES = [
66
72
  [/(quiz)$/i, "\\1zes"],
67
73
  [/^(oxen)$/i, "\\1"],
@@ -84,16 +90,20 @@ module Sevgi
84
90
  [/^(ax|test)is$/i, "\\1es"],
85
91
  [/s$/i, "s"],
86
92
  [/$/, "s"]
87
- ].freeze
93
+ ]
94
+ .each(&:freeze)
95
+ .freeze
96
+
97
+ private_constant :IRREGULARS, :PLURALS, :RULES, :UNCOUNTABLES
88
98
 
89
99
  # Pluralizes an English word using a small built-in rule set.
90
100
  # @param word [Object] word to pluralize
91
101
  # @return [String]
92
- # @example
93
- # F.pluralize("post") # => "posts"
94
- # F.pluralize("octopus") # => "octopi"
95
- # F.pluralize("sheep") # => "sheep"
96
- # F.pluralize("CamelOctopus") # => "CamelOctopi"
102
+ # @example Pluralize through the library facade
103
+ # Sevgi::F.pluralize("post") # => "posts"
104
+ # Sevgi::F.pluralize("octopus") # => "octopi"
105
+ # Sevgi::F.pluralize("sheep") # => "sheep"
106
+ # Sevgi::F.pluralize("CamelOctopus") # => "CamelOctopi"
97
107
  def pluralize(word)
98
108
  result = word.to_s.dup
99
109
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Sevgi
4
4
  module Function
5
- # Small terminal status helpers used by build and release tasks.
5
+ # Terminal status methods promoted to {Sevgi::F}. This module organizes the facade implementation. It is not a
6
+ # consumer mixin contract.
6
7
  module UI
7
8
  # Reports an in-progress status message.
8
9
  # @param message [Object] status message
@@ -3,6 +3,6 @@
3
3
  module Sevgi
4
4
  module Function
5
5
  # Current version of the Sevgi function gem.
6
- VERSION = "0.95.0"
6
+ VERSION = "1.0.0"
7
7
  end
8
8
  end
@@ -13,13 +13,23 @@ require_relative "function/ui"
13
13
  require_relative "function/version"
14
14
 
15
15
  module Sevgi
16
- # Shared helper namespace used directly as `Sevgi::Function` and through {Sevgi::F}.
16
+ # Support toolbox for Sevgi components and advanced extensions. The supported helper facade is {Sevgi::F}. It
17
+ # provides degree-based trigonometry and precision, file discovery and output, argv-safe commands, naming helpers,
18
+ # and small terminal status tools. It is not intended as a general-purpose utility library.
17
19
  #
18
- # @example Use helper methods through the public alias
19
- # F.pluralize("axis")
20
+ # {Function::Location}, {Function::Locate}, and {Function::Shell::Result} are public supporting values. The
21
+ # thread-local precision accessors remain on {Function::Math.precision}. Other nested helper modules organize the
22
+ # facade implementation and its method documentation. Consumers do not include or extend them.
23
+ #
24
+ # @example Use the supported facade in library code
25
+ # Sevgi::F.with_precision(3) do
26
+ # Sevgi::F.cos(60)
27
+ # Sevgi::F.approx(1.0 / 3)
28
+ # end
29
+ # @see https://sevgi.roktas.dev/functions/ Function toolbox guide
20
30
  module Function
21
31
  end
22
32
 
23
- # Public alias for the shared function helper namespace.
33
+ # Supported helper facade for Sevgi extensions and advanced consumers.
24
34
  F = Function unless defined?(F)
25
35
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sevgi-function
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.95.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Recai Oktaş
@@ -9,7 +9,7 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: Enhances the Sevgi toolkit with utility methods.
12
+ description: Collects numeric, string, file, shell, and terminal helpers.
13
13
  email: roktas@gmail.com
14
14
  executables: []
15
15
  extensions: []
@@ -50,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
50
  - !ruby/object:Gem::Version
51
51
  version: '0'
52
52
  requirements: []
53
- rubygems_version: 4.0.11
53
+ rubygems_version: 4.0.20
54
54
  specification_version: 4
55
- summary: Various utility functions for the Sevgi toolkit.
55
+ summary: Shared utility functions for Sevgi components.
56
56
  test_files: []