doc_guard 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7e1710f91fe17ebf7bc8d1f123fc093d6c7e0ad4fc2c42fec0866445f707849e
4
+ data.tar.gz: 9553cff66d4bc4c958f7a2849c9d317e36e611b662b5816fb8b8a7d476b722b6
5
+ SHA512:
6
+ metadata.gz: 0a595ad237f74d9757914eabc28766a8b649a84e25d42e7dc7a909dcc99726f718bedd102e3a3463e8820d1ffcaf75a8cda941154f9a3bc5f2285d489acab3ae
7
+ data.tar.gz: d38e09d03d05e5deb09ce66b9d2148cb2e5ae1989748a565a4489f523ada659d25986a234e300b9acaf9aece4a8d691facf31fd59ee9e9d056deb4d0df2c551e
data/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [1.0.0] - 2025-07-02
11
+
12
+ ### Added
13
+ - Initial release of DocGuard gem, a tool to help keep documentation up-to-date with your source code.
14
+ - Support for annotating markdown documentation files with tracked source files via special HTML comments.
15
+ - Available CLI commands (built on Thor):
16
+ - `doc_guard record` - records current digests of tracked files.
17
+ - `doc_guard assess` - checks if documentation is up to date based on tracked file changes.
18
+ - `doc_guard help` - displays available commands and usage instructions.
19
+ - Configuration support:
20
+ - Load settings via CLI options, environment variables, and YAML config file (`.doc_guard.yml`).
21
+ - Defaults provided for documentation path and digests store file.
22
+ - Integration-ready for CI/CD pipelines to automate documentation relevance checks.
23
+ - Autoloading using Zeitwerk for cleaner project structure.
24
+ - Comprehensive error handling in CLI with proper exit codes.
25
+ - Basic RSpec tests covering configuration and core functionality.
26
+
27
+ ### Changed
28
+
29
+ - N/A (initial release)
30
+
31
+ ### Fixed
32
+
33
+ - N/A (initial release)
34
+
35
+ ---
36
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Patryk Gramatowski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,166 @@
1
+ <img src="doc_guard-logo.png" alt="doc_guard logo" width="250" style="display: block; margin: 0 auto"/>
2
+
3
+ ---
4
+
5
+ ### 🛡️ Keep your documentation up-to-date with your code
6
+
7
+ **DocGuard** is a lightweight, extensible Ruby gem that helps **ensure your documentation remains accurate** as your code evolves. It **tracks** source files referenced in documentation and **warns** when changes occur.
8
+
9
+ ---
10
+
11
+ ### 🚀 Features
12
+
13
+ - **Automatic File Tracking:**
14
+ Easily annotate your documentation files with the source code files they reference. DocGuard automatically detects which files your docs depend on, eliminating manual tracking.
15
+
16
+ - **Digest Calculation & Change Detection:**
17
+ DocGuard computes SHA256 digests (hashes) of tracked files and compares them with previously stored digests to detect any changes. This helps identify when code changes may impact your documentation.
18
+
19
+ - **Real-time Relevance Assessment:**
20
+ Quickly determine if your documentation is still relevant based on the current state of your code. Get instant feedback when your docs might be outdated.
21
+
22
+ - **Clear and Actionable Reporting:**
23
+ Receive concise CLI reports highlighting which files caused potential documentation mismatches, helping you prioritize documentation updates.
24
+
25
+ - **Fail-safe Exit Codes:**
26
+ Integrate DocGuard seamlessly into CI/CD pipelines, it exits with meaningful statuses:
27
+ - `0` when docs are up to date,
28
+ - `1` when documentation may be outdated,
29
+ - `2` on unexpected errors.
30
+
31
+ - **Flexible CLI (built on [Thor](https://github.com/rails/thor)):**
32
+ Easy-to-use commands to assess relevance and extend with future commands for recording or other workflows.
33
+
34
+ - **Lightweight & Framework Agnostic:**
35
+ Works well in Rails projects but designed to be framework-independent and lightweight enough to fit any Ruby codebase.
36
+
37
+ - **JSON-based Digest Storage:**
38
+ Stores digests in a human-readable JSON file, making it easy to audit or manage outside the tool if needed.
39
+
40
+
41
+ ### 💿 Installation
42
+
43
+ Install the gem and add to the application's Gemfile by executing:
44
+
45
+ ```bash
46
+ bundle add doc_guard
47
+ ```
48
+
49
+ If bundler is not being used to manage dependencies, install the gem by executing:
50
+
51
+ ```bash
52
+ gem install doc_guard
53
+ ```
54
+
55
+ or add this line to your application's Gemfile:
56
+
57
+ ```ruby
58
+ gem "doc_guard"
59
+ ```
60
+
61
+ ### ⚙️ Usage
62
+
63
+ #### 1. Annotate Documentation with Tracked Files 📝
64
+
65
+ To enable DocGuard to track which source files your documentation depends on, add a special HTML comment in your markdown files (e.g., `README.md` or any `.md` files inside a `docs/` folder).
66
+
67
+ ##### How to annotate:
68
+ Insert the following comment at the top (or anywhere relevant) in your markdown file:
69
+
70
+ ```md
71
+ <!-- doc_guard files: [app/models/user.rb, app/services/authenticator.rb] -->
72
+ Your amazing and very important documentation related to the user model and authentication service is here.
73
+ ```
74
+
75
+ #### 2. Record the current state of the relationship between your documentation and your code 💾
76
+
77
+ ```bash
78
+ $ doc_guard record
79
+ ```
80
+
81
+ This stores the current digests of referenced files in `.doc_guard_digests.json.`
82
+
83
+ #### 3. Assess documentation relevance 🚦
84
+
85
+ ```bash
86
+ $ doc_guard assess
87
+ ```
88
+
89
+ Example output:
90
+ ```bash
91
+ ‼️ Documentation may be outdated!
92
+
93
+ 📄 docs/user.md
94
+ - app/models/user.rb
95
+ - app/services/authenticator.rb
96
+
97
+ 📄 docs/admin.md
98
+ - app/models/admin.rb
99
+ ```
100
+
101
+ ### 🔧 Configuration
102
+
103
+ DocGuard can be configured in multiple flexible ways to suit different environments (e.g., local development, CI/CD pipelines, GitHub Actions).
104
+
105
+ Configuration values are resolved using the following priority:
106
+ 1. CLI flags
107
+ 2. Environment variables
108
+ 3. YAML config file (`.doc_guard.yml`)
109
+ 4. Internal defaults
110
+
111
+ #### 🗂️ Available Configuration Options
112
+
113
+ | Setting | CLI Flag | ENV Variable | YAML Config Key | Default Value |
114
+ |-----------------------------|------------------------------------|--------------------------------------|----------------------------------------|-------------------------------|
115
+ | Documentation path | `--documentation-path` | `DOC_GUARD_DOCUMENTATION_PATH` | `documentation_path` | `"docs"` |
116
+ | Digests store file path | `--digests-store-file-path` | `DOC_GUARD_DIGESTS_STORE_FILE_PATH` | `digests_store_file_path` | `".doc_guard_digests.json"` |
117
+ | Config file path (for YAML) | `--config-file-path` | `DOC_GUARD_CONFIG_FILE_PATH` | *(Not loaded from YAML)* | `".doc_guard.yml"` |
118
+
119
+ *Note: The `config_file_path` is used only to load other settings from a YAML file. It cannot itself be configured via YAML.*
120
+
121
+ #### 📁 Config File Example
122
+ ```yaml
123
+ # .doc_guard.yml
124
+
125
+ documentation_path: custom_docs
126
+ digests_store_file_path: tmp/doc_guard_state.json
127
+ ```
128
+ *Note: Place this file in your project root.*
129
+
130
+ This configuration approach supports usage in CI/CD pipelines, local development, or scripts, **without any dependency** on Rails or other frameworks.
131
+
132
+ ### 🛣️ Roadmap
133
+
134
+ The following features are being considered or are actively planned for future releases:
135
+
136
+ - **External documentation integrations**
137
+ Support for syncing and validating documentation that lives outside the main codebase, e.g., Confluence, GitHub Wikis, or other remote documentation tools.
138
+
139
+ - **Custom digest strategies**
140
+ Allow users to define custom strategies for calculating file "change relevance", e.g., ignore comments or whitespace, or weigh different parts of the code differently.
141
+
142
+ - **AI-assisted relevance analysis**
143
+ In the future, integrate with AI systems to help assess whether documentation should be updated based on the semantic nature of recent code changes (e.g., if a developer introduces a new feature but forgets to update corresponding docs).
144
+
145
+ - **Advanced code–documentation annotation system**
146
+ Improve how source code is linked to documentation, potentially with fine-grained annotations (e.g., per method, class, or even logic block), offering a more accurate mapping between code changes and relevant documentation sections.
147
+
148
+ Have an idea or need a feature? Feel free to [open an issue](https://github.com/patrickgramatowski/doc_guard/issues) or contribute!
149
+
150
+ ### 🧑‍💻 Development
151
+
152
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
153
+
154
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
155
+
156
+ ### 🤝 Contributing
157
+
158
+ Bug reports and pull requests are welcome on GitHub at https://github.com/patrickgramatowski/doc_guard. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/patrickgramatowski/doc_guard/blob/master/CODE_OF_CONDUCT.md).
159
+
160
+ ### 📄 License
161
+
162
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
163
+
164
+ ### 📜 Code of Conduct
165
+
166
+ Everyone interacting in the DocGuard project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/patrickgramatowski/doc_guard/blob/master/CODE_OF_CONDUCT.md).
data/bin/doc_guard ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "doc_guard"
6
+
7
+ DocGuard::Cli.start(ARGV)
data/doc_guard.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/doc_guard/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "doc_guard"
7
+ spec.version = DocGuard::VERSION
8
+ spec.authors = ["Patryk Gramatowski"]
9
+ spec.email = ["patrick.gramatowski@gmail.com"]
10
+
11
+ spec.summary = "DocGuard helps maintain up-to-date project documentation by tracking files referenced in your docs."
12
+ spec.description = <<~DESCRIPTION
13
+ DocGuard helps maintain up-to-date project documentation by tracking files referenced in your docs,
14
+ calculating file digests, and assessing whether code changes impact your documentation relevance.
15
+ It provides CLI commands to assess documentation relevance and record the current documentation state,
16
+ enabling automated enforcement and better documentation quality in Rails and Ruby projects.
17
+ DESCRIPTION
18
+
19
+ spec.homepage = "https://github.com/patrickgramatowski/doc_guard"
20
+ spec.license = "MIT"
21
+ spec.required_ruby_version = ">= 3.2"
22
+
23
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
24
+ spec.metadata["rubygems_mfa_required"] = "true"
25
+
26
+ spec.metadata["homepage_uri"] = spec.homepage
27
+ spec.metadata["source_code_uri"] = spec.homepage
28
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
29
+
30
+ spec.bindir = "bin"
31
+ spec.executables = [spec.name]
32
+ spec.require_paths = ["lib"]
33
+ spec.files = [
34
+ Dir.glob("{#{spec.require_paths.join(",")}}/**/*.rb"),
35
+ %W[#{spec.name}.gemspec],
36
+ %w[CHANGELOG.md README.md LICENSE.txt]
37
+ ].flatten
38
+
39
+ spec.add_dependency "digest", "~> 3.0"
40
+ spec.add_dependency "thor", "~> 1.3"
41
+ spec.add_dependency "zeitwerk", "~> 2.6"
42
+
43
+ spec.post_install_message = "=> ✅ Setup complete! You can now use `#{spec.name}` on the command line."
44
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ module AssessDocumentationRelevance
5
+ # Main orchestration class that assesses whether documentation is still relevant
6
+ # based on digests of referenced files.
7
+ class Process
8
+ # Entry point to run the relevance assessment process.
9
+ #
10
+ # @param config [DocGuard::Config] Optional configuration.
11
+ # @return [void]
12
+ def self.run(config: ::DocGuard::Config.new)
13
+ new(config: config).call
14
+ end
15
+
16
+ # Initializes the process with the given configuration.
17
+ #
18
+ # @param config [DocGuard::Config]
19
+ def initialize(config: ::DocGuard::Config.new)
20
+ @config = config
21
+ end
22
+
23
+ # Executes the full relevance assessment pipeline.
24
+ #
25
+ # @return [void]
26
+ def call
27
+ tracked_files = load_tracked_files
28
+ stored_digests = load_stored_digests
29
+ current_digests = calculate_current_digests(tracked_files)
30
+ mismatches = compare_digests(stored_digests, current_digests)
31
+ assessment = assess_relevance(tracked_files, mismatches)
32
+
33
+ report_assessment(assessment)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :config
39
+
40
+ # Loads files tracked from documentation annotations.
41
+ #
42
+ # @return [Hash{String => Array<String>}] Mapping of documentation files to arrays of tracked source files.
43
+ def load_tracked_files
44
+ ::DocGuard::Shared::Subprocesses::LoadTrackedFilesFromDocumentation.run(config: config)
45
+ end
46
+
47
+ # Loads stored digests for previously tracked files.
48
+ #
49
+ # @return [Hash<String, Hash<String, String>>] Mapping of documentation files to
50
+ # tracked file digests (e.g., { "docs/file.md" => { "app/file.rb" => "digest" } }),
51
+ # or an empty hash if the file does not exist
52
+ def load_stored_digests
53
+ Subprocesses::LoadStoredDigests.run(config: config)
54
+ end
55
+
56
+ # Calculates current digests of tracked files.
57
+ #
58
+ # @param tracked_files [Hash<String, Array<String>>] Mapping of documentation files to lists of tracked source file paths.
59
+ # @return [Hash<String, Hash<String, String?>>] Mapping of documentation files to tracked source files
60
+ # and their SHA256 digests (or nil if missing).
61
+ def calculate_current_digests(tracked_files)
62
+ ::DocGuard::Shared::Subprocesses::CalculateCurrentDigests.run(tracked_files: tracked_files)
63
+ end
64
+
65
+ # Compares stored and current digests to find mismatches.
66
+ #
67
+ # @param stored_digests [Hash<String, Hash<String, String>>] Previously stored digests.
68
+ # @param current_digests [Hash<String, Hash<String, String?>>] Freshly calculated digests.
69
+ # @return [Array<String>] List of file paths with mismatched or missing digests.
70
+ def compare_digests(stored_digests, current_digests)
71
+ Subprocesses::CompareDigests.run(stored_digests: stored_digests, current_digests: current_digests)
72
+ end
73
+
74
+ # Assesses documentation relevance based on mismatched source files.
75
+ #
76
+ # @param tracked_files [Hash{String => Array<String>}] Doc-to-tracked-files mapping.
77
+ # @param mismatches [Array<String>] list of changed file paths.
78
+ # @return [Hash{Symbol => Object}]
79
+ # - :relevant [Boolean] true if any documentation is outdated.
80
+ # - :mismatches [Hash{String => Array<String>}] docs with the changed files they reference.
81
+ def assess_relevance(tracked_files, mismatches)
82
+ Subprocesses::AssessRelevance.run(tracked_files: tracked_files, mismatches: mismatches)
83
+ end
84
+
85
+ # Reports the result of the assessment and exits if outdated.
86
+ #
87
+ # @param assessment [Hash{Symbol => Object}] Result of the relevance check.
88
+ # @return [void]
89
+ def report_assessment(assessment)
90
+ Subprocesses::ReportAssessment.run(assessment: assessment)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ module AssessDocumentationRelevance
5
+ module Subprocesses
6
+ # Determines if documentation is outdated based on digest mismatches.
7
+ #
8
+ # This subprocess receives a mapping of documentation files to the source files
9
+ # they describe, and compares that with a list of source files that have changed.
10
+ # If any documentation file references at least one changed file, it's considered outdated.
11
+ class AssessRelevance
12
+ # Assesses which documentation files are outdated based on file digest mismatches.
13
+ #
14
+ # @param tracked_files [Hash{String => Array<String>}] Mapping of documentation file paths to the source files they reference.
15
+ # @param mismatches [Array<String>] List of source files whose digests have changed.
16
+ #
17
+ # @return [Hash{Symbol => Object}]
18
+ # - :relevant [Boolean] `true` if any documentation files are affected.
19
+ # - :mismatches [Hash{String => Array<String>}] Mapping of documentation files to the list of changed source files they reference.
20
+ def self.run(tracked_files: {}, mismatches: [])
21
+ documentation_mismatches = tracked_files.each_with_object({}) do |(documentation_file, source_files), acc|
22
+ changed = source_files.select { |file| mismatches.include?(file) }
23
+ acc[documentation_file] = changed unless changed.empty?
24
+ end
25
+
26
+ {
27
+ relevant: documentation_mismatches.any?,
28
+ mismatches: documentation_mismatches
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ module AssessDocumentationRelevance
5
+ module Subprocesses
6
+ # Compares stored and current file digests per documentation file
7
+ # to detect which documentation files may now be outdated.
8
+ class CompareDigests
9
+ # Returns a list of documentation files where any tracked source file changed.
10
+ #
11
+ # @param stored_digests [Hash<String, Hash<String, String>>] Currently mapping of documentation files to tracked source files
12
+ # and their SHA256 digests.
13
+ # @param current_digests [Hash<String, Hash<String, String?>>] New mapping of documentation files to tracked source files
14
+ # and their SHA256 digests (or nil if missing).
15
+ # @return [Array<String>] List of file paths with mismatched or missing digests.
16
+ def self.run(stored_digests: {}, current_digests: {})
17
+ (stored_digests.keys | current_digests.keys).each_with_object([]) do |documentation_file, mismatches|
18
+ stored = stored_digests[documentation_file] || {}
19
+ current = current_digests[documentation_file] || {}
20
+
21
+ (stored.keys | current.keys).each do |source_file|
22
+ stored[source_file] != current[source_file] ? mismatches.push(source_file) : next
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ module AssessDocumentationRelevance
5
+ module Subprocesses
6
+ # Loads previously stored digests from a JSON file.
7
+ class LoadStoredDigests
8
+ # Runs the subprocess to load stored digests from the configured file path.
9
+ #
10
+ # @param config [DocGuard::Config] The configuration containing the path to the digests store.
11
+ # @return [Hash<String, Hash<String, String>>] Mapping of documentation files to
12
+ # tracked file digests (e.g., { "docs/file.md" => { "app/file.rb" => "digest" } }),
13
+ # or an empty hash if the file does not exist.
14
+ def self.run(config: ::DocGuard::Config.new)
15
+ return {} unless File.exist?(config.digests_store_file_path)
16
+
17
+ JSON.parse(File.read(config.digests_store_file_path))
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ module AssessDocumentationRelevance
5
+ module Subprocesses
6
+ # Reports the result of the documentation relevance assessment.
7
+ class ReportAssessment
8
+ # Outputs the result and exits if documentation is outdated.
9
+ #
10
+ # @param assessment [Hash{Symbol => Object}]
11
+ # - :relevant [Boolean] Whether documentation is outdated.
12
+ # - :mismatches [Hash<String, Array<String>>] Mapping of documentation files to lists of tracked source file paths.
13
+ # @return [void]
14
+ def self.run(assessment: {})
15
+ if assessment[:relevant]
16
+ warn "‼️ Documentation may be outdated!"
17
+
18
+ assessment[:mismatches].each do |documentation_file, changed_files|
19
+ warn "\n📄 #{documentation_file}"
20
+ changed_files.each do |file|
21
+ warn "- #{file}"
22
+ end
23
+ end
24
+
25
+ exit(1)
26
+ else
27
+ puts "✅ Documentation is up to date."
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ # Command-line interface for DocGuard.
5
+ #
6
+ # Usage:
7
+ # doc_guard assess --documentation-path=docs --digests-store-file-path=.digests.json
8
+ # doc_guard record
9
+ #
10
+ class Cli < Thor
11
+ class_option :documentation_path, type: :string, desc: "Path to documentation files"
12
+ class_option :digests_store_file_path, type: :string, desc: "Path to the digests store file"
13
+ class_option :config_file_path, type: :string, default: ::DocGuard::Config::DEFAULT_CONFIG_FILE_PATH, desc: "Path to the config file"
14
+
15
+ desc "assess", "Assess documentation relevance"
16
+ # @option options [String] :documentation_path Path to documentation files
17
+ # @option options [String] :digests_store_file_path Path to digests file
18
+ # @option options [String] :config_file_path Path to config file
19
+ # @exitstatus 0 if documentation is up to date
20
+ # @exitstatus 1 if outdated
21
+ # @exitstatus 2 on errors
22
+ def assess
23
+ ::DocGuard::AssessDocumentationRelevance::Process.run(config: build_config_from_options)
24
+ rescue SystemExit => e
25
+ exit e.status
26
+ rescue StandardError => e
27
+ warn "‼️ Error: #{e.message}"
28
+ exit 2
29
+ end
30
+
31
+ desc "record", "Record current documentation state"
32
+ # @option options [String] :documentation_path Path to documentation files
33
+ # @option options [String] :digests_store_file_path Path to digests file
34
+ # @option options [String] :config_file_path Path to config file
35
+ # @exitstatus 0 if success
36
+ # @exitstatus 1 if failure (e.g. file error)
37
+ # @exitstatus 2 on errors
38
+ def record
39
+ ::DocGuard::RecordDocumentationRelevance::Process.run(config: build_config_from_options)
40
+ rescue SystemExit => e
41
+ exit e.status
42
+ rescue StandardError => e
43
+ warn "‼️ Error: #{e.message}"
44
+ exit 2
45
+ end
46
+
47
+ private
48
+
49
+ # Builds a configuration object from CLI options.
50
+ #
51
+ # @return [DocGuard::Config] The configuration instance initialized with CLI options or defaults.
52
+ def build_config_from_options
53
+ ::DocGuard::Config.new(
54
+ documentation_path: options[:documentation_path],
55
+ digests_store_file_path: options[:digests_store_file_path],
56
+ config_file_path: options[:config_file_path]
57
+ )
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ # Handles configuration for DocGuard by loading values from:
5
+ # - Explicit parameters (highest priority)
6
+ # - Environment variables
7
+ # - YAML config file (.doc_guard.yml)
8
+ # - Internal defaults (lowest priority)
9
+ #
10
+ # This class supports usage in CLI, CI/CD pipelines, or manual scripts
11
+ # without requiring any Rails integration.
12
+ class Config
13
+ DEFAULT_CONFIG_FILE_PATH = ".doc_guard.yml"
14
+ DEFAULT_DIGESTS_STORE_FILE_PATH = ".doc_guard_digests.json"
15
+ DEFAULT_DOCUMENTATION_PATH = "docs"
16
+
17
+ # Initialize configuration for DocGuard.
18
+ #
19
+ # You can pass `nil` to `digests_store_file_path` or `documentation_path` to allow fallback to ENV, config file, or defaults.
20
+ #
21
+ # @param digests_store_file_path [String, nil] Optional override path for storing digests. Defaults to `.doc_guard_digests.json`.
22
+ # @param documentation_path [String, nil] Optional override path to the documentation folder. Defaults to `docs`.
23
+ # @param config_file_path [String, nil] Optional override path to the config file (YAML). Defaults to `.doc_guard.yml`.
24
+ #
25
+ # @example Basic usage
26
+ # DocGuard::Config.new(
27
+ # digests_store_file_path: nil,
28
+ # documentation_path: nil
29
+ # )
30
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
31
+ def initialize(digests_store_file_path: nil, documentation_path: nil, config_file_path: nil)
32
+ @config_file_path =
33
+ config_file_path ||
34
+ ENV["DOC_GUARD_CONFIG_FILE_PATH"] ||
35
+ DEFAULT_CONFIG_FILE_PATH
36
+
37
+ config = load_config_file(@config_file_path)
38
+
39
+ @documentation_path =
40
+ documentation_path ||
41
+ ENV["DOC_GUARD_DOCUMENTATION_PATH"] ||
42
+ config["documentation_path"] ||
43
+ DEFAULT_DOCUMENTATION_PATH
44
+
45
+ @digests_store_file_path =
46
+ digests_store_file_path ||
47
+ ENV["DOC_GUARD_DIGESTS_STORE_FILE_PATH"] ||
48
+ config["digests_store_file_path"] ||
49
+ DEFAULT_DIGESTS_STORE_FILE_PATH
50
+ end
51
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
52
+
53
+ # @return [String] Resolved documentation path.
54
+ attr_reader :documentation_path
55
+
56
+ # @return [String] Resolved path to the digests JSON file.
57
+ attr_reader :digests_store_file_path
58
+
59
+ # @return [String] Resolved path to the digests JSON file.
60
+ attr_reader :config_file_path
61
+
62
+ private
63
+
64
+ # Loads a YAML config file if it exists, otherwise returns an empty hash.
65
+ #
66
+ # @param path [String] Absolute or relative path to the YAML config file.
67
+ # @return [Hash{String => String}] Parsed YAML contents or empty hash.
68
+ def load_config_file(config_file_path)
69
+ if File.exist?(config_file_path)
70
+ YAML.load_file(config_file_path) || {}
71
+ else
72
+ {}
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ module RecordDocumentationRelevance
5
+ # Main orchestration class that records the current documentation relevance state
6
+ # by capturing the latest digests of files referenced in the documentation.
7
+ class Process
8
+ # Entry point to run the relevance recording process.
9
+ #
10
+ # @param config [DocGuard::Config] Optional configuration.
11
+ # @return [void]
12
+ def self.run(config: ::DocGuard::Config.new)
13
+ new(config: config).call
14
+ end
15
+
16
+ # Initializes the process with the given configuration.
17
+ #
18
+ # @param config [DocGuard::Config]
19
+ def initialize(config: ::DocGuard::Config.new)
20
+ @config = config
21
+ end
22
+
23
+ # Executes the full relevance recording pipeline.
24
+ #
25
+ # @return [void]
26
+ def call
27
+ tracked_files = load_tracked_files
28
+ current_digests = calculate_current_digests(tracked_files)
29
+ recording = record_digests(current_digests)
30
+
31
+ report_recording(recording)
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :config
37
+
38
+ # Loads files tracked from documentation annotations.
39
+ #
40
+ # @return [Hash{String => Array<String>}] Mapping of documentation files to lists of tracked source file paths.
41
+ def load_tracked_files
42
+ ::DocGuard::Shared::Subprocesses::LoadTrackedFilesFromDocumentation.run(config: config)
43
+ end
44
+
45
+ # Calculates current digests of tracked files.
46
+ #
47
+ # @param tracked_files [Hash<String, Array<String>>] Mapping of documentation files to lists of tracked source file paths.
48
+ # @return [Hash<String, Hash<String, String?>>] Mapping of documentation files to tracked source files
49
+ # and their SHA256 digests (or nil if missing).
50
+ def calculate_current_digests(tracked_files)
51
+ ::DocGuard::Shared::Subprocesses::CalculateCurrentDigests.run(tracked_files: tracked_files)
52
+ end
53
+
54
+ # Records the given digests by saving them to the configured file path.
55
+ #
56
+ # @param digests [Hash<String, Hash<String, String?>>] Mapping of documentation files to tracked source files
57
+ # and their SHA256 digests (or nil if missing).
58
+ # @return [Hash{Symbol => Object}] The result hash from the RecordDigests subprocess,
59
+ # containing :status and optionally :error keys.
60
+ def record_digests(digests)
61
+ Subprocesses::RecordDigests.run(digests: digests, config: config)
62
+ end
63
+
64
+ # Outputs result message to the user.
65
+ #
66
+ # @param digests [Hash<String, String>] A map of file paths to their digest strings (current).
67
+ # @return [void]
68
+ def report_recording(recording)
69
+ Subprocesses::ReportRecording.run(recording: recording)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module DocGuard
6
+ module RecordDocumentationRelevance
7
+ module Subprocesses
8
+ # Records the current file digests into a configured file path.
9
+ class RecordDigests
10
+ # Writes the given digests as pretty-printed JSON to the configured file path.
11
+ #
12
+ # @param digests [Hash<String, Hash<String, String?>>] Mapping of documentation files to tracked source files
13
+ # and their SHA256 digests (or nil if missing).
14
+ # @param config [DocGuard::Config] Configuration object with `digests_store_file_path`.
15
+ # @return [Hash{Symbol => Symbol, Object}] Status result:
16
+ # - On success: { status: :ok }
17
+ # - On failure: { status: :error, error: error_object }
18
+ def self.run(digests: {}, config: ::DocGuard::Config.new)
19
+ File.write(config.digests_store_file_path, JSON.pretty_generate(digests))
20
+ { status: :ok }
21
+ rescue StandardError => error
22
+ { status: :error, error: error }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ module RecordDocumentationRelevance
5
+ module Subprocesses
6
+ # Outputs a message confirming the result of the documentation state recording.
7
+ class ReportRecording
8
+ # Runs the subprocess that reports the result of recording digests.
9
+ #
10
+ # @param recording [Hash{Symbol => Symbol, Object}] Status result:
11
+ # - On success: { status: :ok }
12
+ # - On failure: { status: :error, error: error_object }
13
+ # @return [void]
14
+ def self.run(recording: {})
15
+ case recording[:status]
16
+ when :ok
17
+ puts "✅ Documentation state recorded successfully."
18
+ when :error
19
+ warn "‼️ Failed to record documentation state: #{recording[:error].message}"
20
+ exit(1)
21
+ else
22
+ warn "⚠️ Unknown recording status."
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ module Shared
5
+ module Subprocesses
6
+ # Calculates SHA256 digests for a map of documentation files and their tracked source files.
7
+ #
8
+ # Input:
9
+ # {
10
+ # "docs/user.md" => ["app/models/user.rb", "app/services/authenticator.rb"],
11
+ # "docs/admin.md" => ["app/models/admin.rb"]
12
+ # }
13
+ #
14
+ # Output:
15
+ # {
16
+ # "docs/user.md" => {
17
+ # "app/models/user.rb" => "abcd1234...",
18
+ # "app/services/authenticator.rb" => "efgh5678..."
19
+ # },
20
+ # "docs/admin.md" => {
21
+ # "app/models/admin.rb" => nil # file missing
22
+ # }
23
+ # }
24
+ class CalculateCurrentDigests
25
+ # Runs the subprocess to calculate SHA256 digests per documentation file and its tracked source files.
26
+ #
27
+ # @param tracked_files [Hash<String, Array<String>>] Mapping of documentation files to lists of tracked source file paths.
28
+ # @return [Hash<String, Hash<String, String?>>] Mapping of documentation files to tracked source files
29
+ # and their SHA256 digests (or nil if missing).
30
+ def self.run(tracked_files: {})
31
+ tracked_files.transform_values do |source_files|
32
+ source_files.each_with_object({}) do |file, digest_map|
33
+ digest_map[file] = File.exist?(file) ? Digest::SHA256.hexdigest(File.read(file)) : nil
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ module Shared
5
+ module Subprocesses
6
+ # Loads a mapping between documentation files and the files they track.
7
+ #
8
+ # Scans all `.md` files under the configured documentation path,
9
+ # and builds a map of:
10
+ # documentation_file_path => [list of tracked source file paths]
11
+ #
12
+ # Example:
13
+ # {
14
+ # "docs/user.md" => ["app/models/user.rb", "app/services/authenticator.rb"],
15
+ # "docs/admin.md" => ["app/models/admin.rb"]
16
+ # }
17
+ class LoadTrackedFilesFromDocumentation
18
+ TAG_PATTERN = /<!--\s*doc_guard\s+files:\s*\[(?<files_to_track>[\s\S]*?)\]\s*-->/
19
+
20
+ # Builds a hash mapping documentation files to the files they track.
21
+ #
22
+ # @param config [DocGuard::Config]
23
+ # @return [Hash{String => Array<String>}] Mapping of documentation files to lists of tracked source file paths.
24
+ def self.run(config: ::DocGuard::Config.new)
25
+ mapping = {}
26
+
27
+ Dir.glob("#{config.documentation_path}/**/*.md") do |documentation_file|
28
+ tracked = []
29
+
30
+ content = File.read(documentation_file)
31
+ content.scan(TAG_PATTERN).each do |match|
32
+ files = match[0].split(",").map(&:strip)
33
+ tracked.concat(files)
34
+ end
35
+
36
+ mapping[documentation_file] = tracked.uniq unless tracked.empty?
37
+ end
38
+
39
+ mapping
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DocGuard
4
+ # Current version of the DocGuard gem.
5
+ #
6
+ # @return [String] The current gem version.
7
+ VERSION = "1.0.0"
8
+ end
data/lib/doc_guard.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "digest"
5
+ require "json"
6
+ require "thor"
7
+ require "yaml"
8
+
9
+ # Loads all Ruby files under the DocGuard directory.
10
+ Zeitwerk::Loader.new.tap do |loader|
11
+ loader.push_dir(File.join(__dir__))
12
+ loader.setup
13
+ end
14
+
15
+ # Top-level module for DocGuard gem functionality.
16
+ #
17
+ # @example Raise a custom DocGuard error
18
+ # raise DocGuard::Error, "something went wrong"
19
+ module DocGuard
20
+ # Base error class for DocGuard-related exceptions.
21
+ class Error < StandardError; end
22
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: doc_guard
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Patryk Gramatowski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-07-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: digest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.6'
55
+ description: |
56
+ DocGuard helps maintain up-to-date project documentation by tracking files referenced in your docs,
57
+ calculating file digests, and assessing whether code changes impact your documentation relevance.
58
+ It provides CLI commands to assess documentation relevance and record the current documentation state,
59
+ enabling automated enforcement and better documentation quality in Rails and Ruby projects.
60
+ email:
61
+ - patrick.gramatowski@gmail.com
62
+ executables:
63
+ - doc_guard
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - CHANGELOG.md
68
+ - LICENSE.txt
69
+ - README.md
70
+ - bin/doc_guard
71
+ - doc_guard.gemspec
72
+ - lib/doc_guard.rb
73
+ - lib/doc_guard/assess_documentation_relevance/process.rb
74
+ - lib/doc_guard/assess_documentation_relevance/subprocesses/assess_relevance.rb
75
+ - lib/doc_guard/assess_documentation_relevance/subprocesses/compare_digests.rb
76
+ - lib/doc_guard/assess_documentation_relevance/subprocesses/load_stored_digests.rb
77
+ - lib/doc_guard/assess_documentation_relevance/subprocesses/report_assessment.rb
78
+ - lib/doc_guard/cli.rb
79
+ - lib/doc_guard/config.rb
80
+ - lib/doc_guard/record_documentation_relevance/process.rb
81
+ - lib/doc_guard/record_documentation_relevance/subprocesses/record_digests.rb
82
+ - lib/doc_guard/record_documentation_relevance/subprocesses/report_recording.rb
83
+ - lib/doc_guard/shared/subprocesses/calculate_current_digests.rb
84
+ - lib/doc_guard/shared/subprocesses/load_tracked_files_from_documentation.rb
85
+ - lib/doc_guard/version.rb
86
+ homepage: https://github.com/patrickgramatowski/doc_guard
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ allowed_push_host: https://rubygems.org
91
+ rubygems_mfa_required: 'true'
92
+ homepage_uri: https://github.com/patrickgramatowski/doc_guard
93
+ source_code_uri: https://github.com/patrickgramatowski/doc_guard
94
+ changelog_uri: https://github.com/patrickgramatowski/doc_guard/CHANGELOG.md
95
+ post_install_message: "=> ✅ Setup complete! You can now use `doc_guard` on the command
96
+ line."
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '3.2'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.4.10
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: DocGuard helps maintain up-to-date project documentation by tracking files
115
+ referenced in your docs.
116
+ test_files: []