disposita 0.1.0 → 0.2.1

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: 2fb4585036294fbe8e0879b28e7a2dcba238402e3719cdd4fabacf016af8f1ee
4
- data.tar.gz: 6761b686729f6fb6042fb8e204c990bad8f7839197b2a9251bd2eb4b07b4c785
3
+ metadata.gz: 203e98bee659889337318d3bf9ad108b91858fd45dd2e37402544393befb7595
4
+ data.tar.gz: ea9ae2539c28a871a64d1578ea1da0246f0e11f7e30710db5b033468c22117a1
5
5
  SHA512:
6
- metadata.gz: 99b4d980ac2044614b82a938f44edb69f63128ffa4e0edb86b2c3179e7905941d8de0daa5631c57e36a3a0dee8b6a9af2655e1d550c997503e86c266bb17cff3
7
- data.tar.gz: 3c6c118173f448fc1b830c584f90d5dab96a641c6bfd3ab1b6ad28364ff3eb6ae28b85875b5c87055989d5a6fbcf07b7d73ce97cfb1810f9696f48953bdb6103
6
+ metadata.gz: c81811f52d09d016ae10fc3f60c39b653428b0f2716014fd2d001db93277f9834611ee6f1dc6b2dc31aa1e0c0f29183821eb9011ea3010d41017bbe7cab364e0
7
+ data.tar.gz: 17075e0f5241d5df25c7b13238f597fd49832a17db504059fadac2bd4d7269b3d4d6ea7cfa83e3656ea312ae6a53b4286b5b797f1eb7d0dc9bae46e4fc9c78bd
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # YARD 0.9.45's default/module/setup.rb renders its special dynamic-method
4
+ # section without running the visibility/API verifier. Standard @api private,
5
+ # @private, private visibility, --hide-api, --no-private and --query therefore
6
+ # still expose method_missing there, even when it is absent from method lists.
7
+ # Apply the same verifier used by normal sections so API marked private stays
8
+ # out of public HTML (including RubyDoc-style output). This changes only docs.
9
+ # Remove this override once upstream applies its verifier to this section.
10
+ def methodmissing
11
+ methods = object.meths(inherited: true, included: true)
12
+ @mm = run_verifier(methods).find { |method| method.name == :method_missing && method.scope == :instance }
13
+ erb(:methodmissing) if @mm
14
+ end
data/.yardopts ADDED
@@ -0,0 +1,10 @@
1
+ --markup markdown
2
+ --readme README.md
3
+ --title "Disposita Documentation"
4
+ --no-private
5
+ --hide-api private
6
+ --template-path .yard/templates
7
+ lib/**/*.rb
8
+ -
9
+ README.md
10
+ CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -4,17 +4,54 @@ All notable changes to Disposita will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows Semantic Versioning.
6
6
 
7
- ## [Unreleased]
7
+ ## [0.2.1] - 2026-09-09
8
8
 
9
9
  ### Fixed
10
10
 
11
- - Load YARD correctly and keep Markdown out of Ruby source parsing; validate documentation in CI.
12
- - Redact secret defaults in schema metadata and preserve caller-owned path arrays.
11
+ - Include `.yardopts` and the custom YARD template in the gem so documentation generated from the package preserves public API filtering.
12
+ - Centralize SimpleCov configuration in `.simplecov`, preserving line >=95% and branch >=90% thresholds.
13
+ - Add the public-gem metadata `homepage_uri` and `allowed_push_host` required by Rubcraft.
14
+ - Run dependency audit and gem build in CI, and verify documentation generated from the unpacked gem in CI and release verification.
15
+
16
+ ## [0.2.0] - 2026-09-09
17
+
18
+ ### Breaking
19
+
20
+ - Setting and namespace names must be non-empty String or Symbol path segments without dots.
21
+
22
+ - Renamed `Disposita.define` to `Disposita.define_schema`, without compatibility aliases.
23
+ - Removed `Schema#load`; `Schema#resolve(sources: [])` is the only resolution API and accepts no positional arguments.
24
+ - Sources are now listed from highest to lowest precedence: the first source wins, recursively within namespaces; arrays are selected whole. Defaults remain the final fallback.
25
+ - Removed special `env`, `env_prefix`, and runtime `overrides` handling from Schema. Environment and runtime values are explicit Sources.
26
+ - Renamed `Sources::Hash` to `Sources::Memory`, including its require path, without an alias.
27
+ - Removed public `Schema#settings` and `Schema#setting`. Use `describe` and `each_setting` metadata instead.
28
+ - Renamed `Source#allows_secrets?` to `allows_secret_persistence?` to distinguish writing policy from reading secrets. File's explicit `allow_secrets:` opt-in remains.
29
+ - Made YAML serialization and platform detection helpers private; excluded internal classes, constructors and dot-access mechanisms from public YARD documentation.
13
30
 
14
31
  ### Added
15
32
 
16
33
  - Complete namespace/helper documentation and regression coverage for source contracts, safe YAML and failed atomic writes.
17
34
 
35
+ - Public `Schema#each_setting` introspection for custom Sources, with deeply frozen metadata and redacted secret defaults.
36
+ - Regression coverage for first-source-wins nested merges, source fallback, provenance, API boundaries and explicit ENV resolution.
37
+
38
+ ### Changed
39
+
40
+ - Rewrote README and YARD examples for the explicit source-based 0.2.0 API.
41
+ - Provenance and `explain` now report the first source that supplies a value. `explain` keeps its selected-value format.
42
+ - A default may satisfy `required: true`; `required: true, optional: true` remains invalid.
43
+ - Resolution copies selected values before freezing, preserving caller-owned source data.
44
+
45
+ ### Fixed
46
+
47
+ - Copy and deeply freeze schema defaults at declaration time so later mutation of caller-owned containers or strings cannot change the schema.
48
+
49
+ - Reject traversal in user configuration application names and handle filesystem-root project paths.
50
+ - Reject unknown empty mappings instead of silently ignoring them.
51
+ - Redact secret values from coercion errors.
52
+ - Load YARD correctly and keep Markdown out of Ruby source parsing; validate documentation in CI.
53
+ - Redact secret defaults in schema metadata and preserve caller-owned path arrays.
54
+
18
55
  ## [0.1.0] - 2026-09-04
19
56
 
20
57
  ### Added
data/README.md CHANGED
@@ -1,322 +1,276 @@
1
1
  # Disposita
2
2
 
3
- Declarative, typed and layered configuration infrastructure for Ruby applications, gems and CLIs.
4
-
5
- Disposita provides the mechanics of configuration while leaving ownership and meaning with the consumer. It does not know what `:ssh`, `production` or a timeout mean to your application; it only knows how those values are declared, loaded, coerced, validated, layered and persisted.
6
-
7
- ## Why Disposita?
8
-
9
- Configuration tends to grow into repeated infrastructure: parsers, defaults, environment variables, per-user paths, project overrides, validation, persistence and diagnostics. Disposita centralizes that infrastructure without making a toolkit or framework the owner of every consumer's configuration.
10
-
11
- ## Installation
12
-
13
- Add to your Gemfile:
14
-
15
- ```ruby
16
- gem "disposita"
17
- ```
18
-
19
- Then run `bundle install`.
3
+ Declarative, typed configuration for Ruby applications and gems. Define the settings your application owns, choose explicit sources, and resolve an immutable configuration.
20
4
 
21
5
  ## Define a schema
22
6
 
23
7
  ```ruby
24
8
  require "disposita"
25
9
 
26
- SCMConfig = Disposita.define(:scm, version: 1) do
27
- namespace :git do
28
- setting :default_remote,
29
- type: String,
30
- default: "origin",
31
- description: "Default Git remote"
32
-
33
- setting :transport,
34
- type: Disposita::Types.enum(:ssh, :https),
35
- default: :ssh,
36
- env: "SCM_GIT_TRANSPORT"
37
-
38
- setting :timeout,
39
- type: Integer,
40
- default: 30
41
-
42
- setting :token,
43
- type: String,
44
- secret: true,
45
- optional: true
10
+ AppSchema = Disposita.define_schema(:app, version: 1) do
11
+ namespace :server do
12
+ setting :host, type: String, default: "localhost"
13
+ setting :port, type: Integer, default: 3000
46
14
  end
47
15
  end
48
16
  ```
49
17
 
50
- `Disposita.define` returns a schema object. It does not register global mutable state and it performs no filesystem or environment reads by itself.
18
+ `Disposita.define_schema` returns a `Disposita::Schema`. Requiring the gem and defining a schema do not read configuration files or ENV, create directories, or register global configuration state.
51
19
 
52
- ### Setting options
20
+ ## Resolve defaults
53
21
 
54
- Within `Disposita.define`, `namespace(name) { ... }` groups settings; namespaces can nest.
55
- `setting(name, type:, ...) { |value| ... }` declares a leaf. Its optional validator runs after coercion and must return a truthy value.
22
+ ```ruby
23
+ config = AppSchema.resolve
56
24
 
57
- | Option | Default | Meaning |
58
- | --- | --- | --- |
59
- | `type:` | Required | Ruby class, a built-in type helper, or an object implementing `valid?` and optionally `coerce`. Otherwise validation uses `===`. |
60
- | `default:` | Absent | Value used when no source provides one. An explicit `nil` still counts as a default and must satisfy the declared type. |
61
- | `required:` | `false` | Reject resolution if the setting is absent. Cannot be combined with a default or `optional: true`. |
62
- | `optional:` | `false` | Explicitly documents that absence is allowed; settings are already optional unless required. |
63
- | `env:` | `nil` | Explicit variable name, taking precedence over generated names. |
64
- | `secret:` | `false` | Redact diagnostics and deny ordinary file persistence. |
65
- | `description:` | `nil` | Consumer-facing text returned by schema introspection. |
66
- | `coerce:` | `true` | Convert raw input before checking semantic validation. Set false for strict values. |
25
+ config.server.host # => "localhost"
26
+ config.server.port # => 3000
27
+ ```
67
28
 
68
- `Schema#load` accepts no arguments for defaults-only resolution. `Schema#resolve` requires an explicit source or array; use `resolve([])` for defaults alone.
29
+ No external source participates unless supplied explicitly. Declaring a setting's `env:` name does not enable ENV reads by itself.
69
30
 
70
- ## Resolve layers
31
+ ## Resolve multiple sources
32
+
33
+ Building on `AppSchema` above:
71
34
 
72
35
  ```ruby
73
- global = Disposita::Sources::File.new(
74
- File.join(Disposita::Paths.user_config("scm"), "config.yml"),
75
- name: :global
36
+ config = AppSchema.resolve(
37
+ sources: [
38
+ Disposita::Sources::Environment.new(prefix: "APP", name: :environment),
39
+ Disposita::Sources::File.new(".app.yml", name: :project)
40
+ ]
76
41
  )
42
+ ```
77
43
 
78
- project = Disposita::Sources::File.new(
79
- ".scm.yml",
80
- name: :project
81
- )
44
+ Sources are ordered from highest to lowest precedence. For each setting, Disposita uses the first source that provides a value, falling back through the remaining sources and finally to the schema default.
82
45
 
83
- config = SCMConfig.load(
84
- sources: [global, project],
85
- env: ENV,
86
- env_prefix: "SCM",
87
- overrides: { git: { timeout: 10 } }
88
- )
46
+ For example, with `APP_SERVER_PORT=5000` and this `.app.yml`:
47
+
48
+ ```yaml
49
+ server:
50
+ host: example.com
51
+ port: 4000
89
52
  ```
90
53
 
91
- Precedence is explicit and follows source order. Schema defaults are always the lowest layer; runtime overrides supplied to `load` are the highest layer.
54
+ The result is `config.server.port == 5000` and `config.server.host == "example.com"`. ENV supplies the port, the project supplies the host, and defaults fill any remaining settings.
92
55
 
93
- ```text
94
- defaults < global < project < environment < runtime
95
- ```
56
+ Namespaces merge recursively. Scalars and arrays are selected whole from the first source providing them; arrays are never concatenated. An explicit `false`, `nil`, or empty array is a supplied value, not absence, and must satisfy the schema's type. An invalid winning value raises an error instead of falling back to another source. Every source is checked for unknown settings and unsupported schema versions, even if its values are shadowed.
57
+
58
+ ## Installation
96
59
 
97
- ## Typed access
60
+ Disposita 0.2.1 requires Ruby 3.2 or newer. After the release is published, use:
98
61
 
99
62
  ```ruby
100
- config.git.default_remote # => "origin"
101
- config.git.transport # => :ssh
102
- config.git.timeout # => 10
63
+ gem "disposita", "~> 0.2.1"
103
64
  ```
104
65
 
105
- Resolved configuration is immutable. `to_h` returns a detached copy for interoperability.
66
+ Then run `bundle install`. Breaking changes from the previous release are recorded in [CHANGELOG.md](CHANGELOG.md).
106
67
 
107
- ## Coercion
108
-
109
- Disposita performs conservative coercion when a setting allows it (the default):
68
+ ## Public model
110
69
 
111
70
  ```text
112
- "5432" -> Integer
113
- "1.5" -> Float
114
- "ssh" -> Symbol / enum value
115
- "false" -> Boolean
71
+ define_schema → Schema → Sources → resolve → Configuration
116
72
  ```
117
73
 
118
- Use `coerce: false` to require an already-typed value.
74
+ - `Disposita.define_schema` describes which settings exist.
75
+ - `Source#read(schema)` obtains partial raw data.
76
+ - `Schema#resolve(sources: [])` applies precedence, recursive merge, defaults, coercion and validation, and records provenance.
77
+ - `Configuration` is the final immutable result.
78
+ - `Schema#write(source, data)` persists explicitly to one selected source.
119
79
 
120
- ```ruby
121
- setting :strict_port, type: Integer, coerce: false
122
- ```
123
-
124
- ## Validation
125
-
126
- A setting can add consumer-owned semantic validation:
80
+ ## Namespaces, types and validation
127
81
 
128
82
  ```ruby
129
- setting :timeout, type: Integer, default: 30 do |value|
130
- value.positive?
83
+ ServiceSchema = Disposita.define_schema(:service) do
84
+ namespace :git do
85
+ setting :transport, type: Disposita::Types.enum(:ssh, :https), default: :ssh
86
+ setting :mirrors, type: Disposita::Types.array(String), default: []
87
+ end
88
+ setting :enabled, type: Disposita::Types.boolean, default: true
89
+ setting :ratio, type: Float, default: 1.25
90
+ setting :mode, type: Symbol, default: :development
91
+ setting :timeout, type: Integer, required: true, default: 30 do |value|
92
+ value.positive?
93
+ end
131
94
  end
132
95
  ```
133
96
 
134
- Disposita runs the rule; the consumer defines what the rule means.
97
+ Namespaces can nest. Each setting or namespace name must be a non-empty String or Symbol without `.`; dots separate path segments, so use a namespace to declare `server.port`. No additional naming convention is imposed. Use Ruby `String`, `Integer`, `Float`, and `Symbol`, plus `Types.boolean`, `Types.enum(...)` and `Types.array(member_type)`. Use these factories instead of instantiating their implementation classes. Disposita remains a small configuration library with no Typio or Rails dependency.
135
98
 
136
- ## Environment variables
99
+ Coercion follows the declared type: `"5432"` becomes integer `5432`, `"1.25"` becomes float `1.25`, `"true"`/`"false"` become booleans, and `"ssh"` becomes `:ssh` for Symbol or an appropriate enum. Strings remain strings when `type: String` is declared. Array members use their declared member type; ENV strings are not implicitly split into arrays. Sources return raw data and do not infer types.
137
100
 
138
- A setting may name its environment variable explicitly:
101
+ | Setting option | Default | Meaning |
102
+ | --- | --- | --- |
103
+ | `type:` | Required | Ruby class or configuration type. Custom type objects may implement `valid?` and optionally `coerce`; otherwise validation uses `===`. |
104
+ | `default:` | Absent | Final fallback, coerced and validated like source data. Explicit `nil` counts as present and must satisfy the type. |
105
+ | `required:` | `false` | A value must exist after all sources and defaults resolve. May be satisfied by a default. |
106
+ | `optional:` | `false` | Documents that absence is allowed. Cannot be true together with `required: true`. |
107
+ | `env:` | `nil` | Explicit environment variable name, used only by an Environment source. |
108
+ | `secret:` | `false` | Redact diagnostics and apply persistence policy. |
109
+ | `description:` | `nil` | Text included in public metadata. |
110
+ | `coerce:` | `true` | Set false to require already typed values. |
139
111
 
140
- ```ruby
141
- setting :transport, type: Symbol, env: "SCM_GIT_TRANSPORT"
142
- ```
112
+ Defaults are copied and deeply frozen when the setting is declared. Mutating the original arrays, hashes or strings afterward does not change the schema. Metadata returned by `describe` is also deeply frozen and cannot modify the stored default.
143
113
 
144
- Or a source may generate names from a prefix and setting path:
114
+ `required: true` means the setting must have a value in the final Configuration; it does not require the consumer to supply it explicitly. For example, `required: true, default: 30` resolves to `30` without an external source. Combining `required: true` with `optional: true` remains invalid.
145
115
 
146
- ```ruby
147
- Disposita::Sources::Environment.new(prefix: "SCM")
148
- # git.transport -> SCM_GIT_TRANSPORT
149
- ```
116
+ Settings without `required: true` may be absent. Missing values are omitted from `to_h`. A validator block runs after coercion and must return a truthy value. Required absence raises `MissingSettingError`; coercion and custom validation failures raise `CoercionError` and `ValidationError`. Unknown keys raise `UnknownSettingError`, including unknown empty mappings. Typos are never silently ignored.
150
117
 
151
- ## Safe YAML
118
+ ## Environment source
152
119
 
153
- The bundled file source uses `Psych.safe_load` and disables Ruby-object deserialization and YAML aliases. Symbols are serialized as strings and coerced back according to the schema.
120
+ ```ruby
121
+ AuthSchema = Disposita.define_schema(:auth) do
122
+ setting :token, type: String, secret: true, env: "MY_SPECIAL_TOKEN"
123
+ setting :timeout, type: Integer, default: 30
124
+ end
154
125
 
155
- ```yaml
156
- version: 1
157
- git:
158
- transport: ssh
126
+ environment = Disposita::Sources::Environment.new(
127
+ env: { "MY_SPECIAL_TOKEN" => "example-token", "APP_TIMEOUT" => "10" },
128
+ prefix: "APP",
129
+ name: :environment
130
+ )
131
+ config = AuthSchema.resolve(sources: [environment])
159
132
  ```
160
133
 
161
- Disposita intentionally ships YAML only in 0.1.0. The format boundary is isolated so JSON or TOML can be added without changing schema ownership or resolution semantics.
162
-
163
- ## Explicit writes
134
+ `env:` on the source defaults to `ENV` because creating that source is an explicit choice. `prefix:` derives uppercase names from the full dotted path: `server.port` becomes `APP_SERVER_PORT`. A setting's explicit name replaces the derived name; if the explicit variable is absent, the derived name is not used. Without a prefix, only explicitly named variables are read. Unrelated environment variables are ignored.
164
135
 
165
- Reading may combine many layers. Writing always targets one explicit writable source.
136
+ ## Memory source and runtime values
166
137
 
167
138
  ```ruby
168
- project = Disposita::Sources::File.new(".scm.yml", name: :project)
169
-
170
- SCMConfig.write(project, git: { transport: :https })
139
+ runtime = Disposita::Sources::Memory.new({ server: { port: 9292 } }, name: :runtime)
140
+ environment = Disposita::Sources::Environment.new(prefix: "APP")
141
+ project = Disposita::Sources::File.new(".app.yml", name: :project)
142
+ global = Disposita::Sources::File.new(
143
+ File.join(Disposita::Paths.user_config("app"), "config.yml"), name: :global
144
+ )
145
+ config = AppSchema.resolve(sources: [runtime, environment, project, global])
171
146
  ```
172
147
 
173
- Writes are validated and performed atomically through a temporary file followed by rename.
174
-
175
- Defaults are not written automatically: consumers persist only the overrides they choose.
176
-
177
- ## Secrets
148
+ The priority here is runtime, environment, project, global, then defaults. Memory is a read-only source for tests, embedding and programmatic values, with default name `:memory`. It normalizes mapping keys without coercing values. There is no special runtime argument on Schema.
178
149
 
179
- A setting can be marked as sensitive:
150
+ ## Configuration, provenance and explain
180
151
 
181
152
  ```ruby
182
- setting :token, type: String, secret: true
153
+ config = AppSchema.resolve(sources: [
154
+ Disposita::Sources::Memory.new({ server: { port: "5000" } }, name: :runtime)
155
+ ])
156
+ config.server.port # => 5000
157
+ config[:server][:host] # => "localhost"
158
+ config.source_of("server.port") # => :runtime
159
+ config.source_of([:server, :host]) # => :default
160
+ config.explain("server.port")
161
+ # => { path: "server.port", value: 5000, source: :runtime }
162
+ copy = config.to_h
183
163
  ```
184
164
 
185
- The actual value remains available to application code:
165
+ Resolved values are recursively frozen; `to_h` returns a detached mutable copy. Resolution does not freeze caller-owned source values. `source_of` returns nil when no value was supplied. `explain` reports the selected value and source, not the full candidate chain. For an absent optional setting it reports nil value/source; for an unknown setting it raises `KeyError`. `inspect` and `explain` redact secrets; ordinary access and `to_h` return actual values.
186
166
 
187
- ```ruby
188
- config.git.token
189
- ```
167
+ ## File source and safe YAML
190
168
 
191
- But diagnostics redact it:
169
+ Missing files are empty sources. Files use a codec with `load(String)` and `dump(Hash)` methods, supplied through `format:`; the bundled codec is `Disposita::Formats::YAML`.
192
170
 
193
- ```ruby
194
- config.inspect
195
- # => #<Disposita::Configuration git=#<... token=[REDACTED]>>
196
- ```
171
+ YAML loading uses Psych safe APIs with Ruby objects, arbitrary classes, Symbol tags and aliases disabled. Runtime Symbols are dumped as plain strings and recovered through schema coercion. No JSON or TOML codecs are included.
197
172
 
198
- Normal file sources reject secret persistence by default. A consumer must opt in explicitly:
173
+ YAML interprets some unquoted values as special types before schema coercion. For example, `release_date: 2026-09-09` is interpreted as a Date and rejected by the safe loader, even if the setting declares `type: String`. Quote such values to keep them as strings:
199
174
 
200
- ```ruby
201
- private_store = Disposita::Sources::File.new(
202
- "~/.config/scm/private.yml",
203
- name: :private,
204
- allow_secrets: true
205
- )
175
+ ```yaml
176
+ release_date: "2026-09-09"
206
177
  ```
207
178
 
208
- Secret-aware behavior is not encryption. Disposita 0.1.0 deliberately does not implement cryptographic storage, key management, Vault, KMS or OS keychains.
179
+ Date is not a permitted class in Disposita 0.2.1.
209
180
 
210
- ## Provenance
181
+ ## Explicit persistence
211
182
 
212
- Disposita tracks which layer supplied the winning value:
183
+ Reading may combine many sources. Writing always targets exactly one source.
213
184
 
214
185
  ```ruby
215
- config.source_of("git.transport")
216
- # => :project
217
-
218
- config.explain("git.transport")
219
- # => { path: "git.transport", value: :https, source: :project }
186
+ project = Disposita::Sources::File.new(".app.yml", name: :project)
187
+ AppSchema.write(project, server: { port: 9292 })
220
188
  ```
221
189
 
222
- Secret values are redacted from `explain`.
190
+ `write` validates only the explicitly supplied partial values and adds the schema version. It replaces the target's contents with that payload. It never automatically persists ENV, runtime, defaults, other sources or a resolved configuration; there is no `config.save!`.
223
191
 
224
- ## Paths
192
+ File writes create a temporary file in the destination directory, write, flush, fsync and rename it into place. Temporary files are cleaned up on failure. Files have restrictive permissions (`0600` on supported platforms). Failed replacement leaves the existing file intact. Parent directories are created only during explicit writes.
225
193
 
226
- User-level configuration paths follow platform conventions:
194
+ ## Secrets
227
195
 
228
- - Linux: `$XDG_CONFIG_HOME/<app>` or `~/.config/<app>`
229
- - macOS: `~/Library/Application Support/<app>`
230
- - Windows: `%APPDATA%\<app>` (falling back to `%LOCALAPPDATA%`)
196
+ `secret: true != encryption`. It means diagnostic redaction, protection against accidental disclosure and an explicit persistence policy. Disposita is not a secret manager.
231
197
 
232
- Project paths remain consumer-owned:
198
+ Every source may read secret values. `Source#allows_secret_persistence?` describes permission to **write** them, and defaults to false. File rejects secret writes unless explicitly opted in:
233
199
 
234
200
  ```ruby
235
- Disposita::Paths.project(Dir.pwd, ".rubcraft/scm.yml")
201
+ private_store = Disposita::Sources::File.new(
202
+ "~/.config/app/private.yml", name: :private, allow_secrets: true
203
+ )
204
+ AuthSchema.write(private_store, token: "example-token")
236
205
  ```
237
206
 
238
- Disposita never imposes `.rubcraft`, `.disposita`, or another project directory.
207
+ Secret defaults are redacted in metadata; secret values are redacted in `inspect`, `explain` and coercion errors. Direct access and exports contain real secrets, so they are not logging APIs.
239
208
 
240
- ## Schema introspection
209
+ ## Paths
241
210
 
242
- ```ruby
243
- SCMConfig.describe("git.transport")
244
- # => {
245
- # path: "git.transport",
246
- # type: "enum(:ssh, :https)",
247
- # default: :ssh,
248
- # has_default: true,
249
- # required: false,
250
- # secret: false,
251
- # env: "SCM_GIT_TRANSPORT",
252
- # description: nil
253
- # }
254
- ```
211
+ `Paths.user_config("app")` follows Linux/XDG (`$XDG_CONFIG_HOME` or `~/.config`), macOS Application Support, and Windows APPDATA with LOCALAPPDATA fallback. The application name must be a single directory name without traversal or path separators. Optional `env:` and `host_os:` arguments allow deterministic path selection.
255
212
 
256
- This metadata is intended to support future CLI help, documentation and UI tooling without coupling Disposita to a particular CLI framework. Secret defaults appear as `[REDACTED]`, while `has_default` still indicates whether a default exists. Direct configuration access and `to_h` return actual values.
213
+ `Paths.project(root, relative)` resolves a consumer-selected project path and rejects traversal outside the root. These helpers compute paths without creating directories. The consumer chooses the layout; no `.disposita` or other project directory is imposed.
257
214
 
258
- Prefer `describe` for diagnostic tooling. The lower-level `setting` and `settings` methods expose internal definition objects, including actual defaults; they are not safe logging representations.
215
+ ## Introspection and custom Sources
259
216
 
260
- ## Schema versions
217
+ `Schema#describe(path)` returns a deeply frozen metadata hash, or nil for an unknown path. `Schema#each_setting` yields the same hashes in declaration order and returns an Enumerator without a block.
261
218
 
262
- Each schema has a version and file sources may persist it. Disposita rejects configuration produced by a newer schema version. Automatic migrations are intentionally deferred beyond 0.1.0 so the migration contract can be designed without freezing a premature API.
219
+ ```ruby
220
+ AppSchema.describe("server.port")
221
+ # => { path: "server.port", type: "Integer", default: 3000,
222
+ # has_default: true, required: false, secret: false, env: nil, description: nil }
223
+ AppSchema.each_setting.map { |metadata| metadata[:path] }
224
+ # => ["server.host", "server.port"]
225
+ ```
263
226
 
264
- ## Ownership model
227
+ Secret defaults appear as `[REDACTED]`. Metadata never exposes internal definition or type adapter objects.
265
228
 
266
- Disposita owns:
229
+ A custom Source can work entirely through public metadata:
267
230
 
268
- - schema infrastructure
269
- - type checking and configuration-oriented coercion
270
- - loading and persistence primitives
271
- - layering and provenance
272
- - conventional user paths
273
- - validation mechanics
274
- - secret-aware diagnostics
231
+ ```ruby
232
+ class DatabaseSource < Disposita::Source
233
+ def initialize(rows, name: :database)
234
+ super(name: name)
235
+ @rows = rows # An application-owned mapping of dotted paths to raw values.
236
+ end
275
237
 
276
- Consumers own:
238
+ def read(schema)
239
+ schema.each_setting.each_with_object({}) do |metadata, data|
240
+ path = metadata.fetch(:path)
241
+ next unless @rows.key?(path)
277
242
 
278
- - the schema itself
279
- - project file locations
280
- - layer policy and precedence
281
- - what each setting means
282
- - semantic validation rules
283
- - whether and where secrets may be persisted
243
+ segments = path.split(".").map(&:to_sym)
244
+ parent = segments[0...-1].reduce(data) { |node, key| node[key] ||= {} }
245
+ parent[segments.last] = @rows.fetch(path)
246
+ end
247
+ end
248
+ end
284
249
 
285
- A library such as SCM can remain configuration-agnostic while `SCM CLI`, Rubcraft Toolkit, or another application defines its own Disposita schema around SCM.
250
+ config = AppSchema.resolve(sources: [DatabaseSource.new({ "server.port" => "6000" })])
251
+ config.server.port # => 6000
252
+ ```
286
253
 
287
- ## Non-goals for 0.1.0
254
+ Custom Sources implement `read(schema)` and inherit `name`, read-only `writable?`, and denied `allows_secret_persistence?`. Writable adapters implement `writable?`, `write(schema, data)` and enforce their persistence policy. Schema handles validation; sources own storage mechanics.
288
255
 
289
- Disposita does not aim to be a general-purpose type system, secret manager, encryption framework, Rails settings singleton, command-line parser or business-rule engine.
256
+ ## Schema versions
290
257
 
291
- ## Development
258
+ `Disposita.define_schema(:app, version: 1)` uses a consumer-owned version independent of `Disposita::VERSION` (`0.2.1`). Explicit writes include `version:`. Resolution rejects persisted versions newer than the schema and malformed version values. Complex migrations are not included.
292
259
 
293
- ```bash
260
+ ## Development and API documentation
261
+
262
+ ```sh
294
263
  bundle install
295
- bundle exec rspec
296
- bundle exec rubocop
297
- bundle exec rake
298
264
  COVERAGE=true bundle exec rspec
265
+ bundle exec rubocop
299
266
  bundle exec rake yard
267
+ bundle exec rake build yard:package
300
268
  ```
301
269
 
302
- The test suite is organized by public behavior and subsystem, with integration-style schema specs separated from source/format/path specs.
270
+ `bundle exec rake` runs RuboCop, specs, Bundler Audit, YARD and packaged-documentation verification. `bundle exec rake yard:package` builds and unpacks the gem into a temporary directory, generates YARD using its bundled configuration and template, and rejects internal API leakage. CI and release verification run this check before publication. Coverage thresholds are configured in `.simplecov`: line >=95% and branch >=90%. CI tests Ruby 3.2, 3.3, 3.4 and 4.0. YARD uses README as the homepage, writes documentation to `doc/`, and treats warnings as failures.
271
+
272
+ Public contracts are `Disposita.define_schema`, Schema, Configuration, Source and its three built-in implementations, the Types factories, Paths and the YAML codec. `Internal::*`, namespace implementation nodes, constructors for resolved objects, and dot-access machinery are implementation details excluded from public documentation.
303
273
 
304
274
  ## License
305
275
 
306
276
  MIT.
307
-
308
- ## API documentation
309
-
310
- Disposita's public API is documented with YARD comments that explain not only
311
- method signatures, but also ownership, precedence, persistence safety and common
312
- usage patterns. Generate the local documentation with:
313
-
314
- ```sh
315
- bundle exec rake yard
316
- ```
317
-
318
- The generated site is written to `doc/`. CI generates it with warnings treated as failures. Internal implementation objects are
319
- marked with `@api private`; applications should build against the documented
320
- public objects such as `Disposita`, `Disposita::Schema`,
321
- `Disposita::Configuration`, `Disposita::Source`, `Disposita::Sources::*`,
322
- `Disposita::Types` and `Disposita::Paths`.