conclude 1.0.1__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {conclude-1.0.1 → conclude-1.0.2}/.gitignore +6 -0
  2. {conclude-1.0.1 → conclude-1.0.2}/CHANGELOG.md +6 -0
  3. {conclude-1.0.1 → conclude-1.0.2}/PKG-INFO +16 -16
  4. {conclude-1.0.1 → conclude-1.0.2}/README.md +13 -13
  5. conclude-1.0.2/RELEASING.md +56 -0
  6. {conclude-1.0.1 → conclude-1.0.2}/docs/comparison.md +1 -1
  7. conclude-1.0.2/docs/concept.md +401 -0
  8. conclude-1.0.2/hatch_build.py +33 -0
  9. {conclude-1.0.1 → conclude-1.0.2}/pyproject.toml +21 -4
  10. conclude-1.0.2/spec/README.md +72 -0
  11. conclude-1.0.2/spec/casters.json +438 -0
  12. conclude-1.0.2/spec/config_layers.json +571 -0
  13. conclude-1.0.2/spec/config_tables.json +221 -0
  14. conclude-1.0.2/spec/dotenv.json +239 -0
  15. conclude-1.0.2/spec/gitignore.json +189 -0
  16. conclude-1.0.2/spec/guard.json +273 -0
  17. conclude-1.0.2/spec/invocation.json +1241 -0
  18. conclude-1.0.2/spec/merge.json +1076 -0
  19. conclude-1.0.2/spec/naming.json +94 -0
  20. conclude-1.0.2/spec/sources.json +315 -0
  21. conclude-1.0.2/spec/templates.json +603 -0
  22. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/__init__.py +1 -1
  23. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_comparison.py +15 -7
  24. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_docs.py +22 -7
  25. conclude-1.0.2/test/test_repo_hygiene.py +413 -0
  26. conclude-1.0.2/test/test_spec.py +473 -0
  27. conclude-1.0.1/tests/test_repo_hygiene.py +0 -208
  28. {conclude-1.0.1 → conclude-1.0.2}/LICENSE +0 -0
  29. {conclude-1.0.1 → conclude-1.0.2}/docs/guide.md +0 -0
  30. {conclude-1.0.1 → conclude-1.0.2}/docs/reference.md +0 -0
  31. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/app.py +0 -0
  32. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/casters.py +0 -0
  33. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/developer.py +0 -0
  34. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/env.py +0 -0
  35. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/files.py +0 -0
  36. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/formatters.py +0 -0
  37. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/guard.py +0 -0
  38. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/infer.py +0 -0
  39. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/merge.py +0 -0
  40. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/naming.py +0 -0
  41. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/paths.py +0 -0
  42. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/py.typed +0 -0
  43. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/templates.py +0 -0
  44. {conclude-1.0.1 → conclude-1.0.2}/src/conclude/tomlwrite.py +0 -0
  45. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_app.py +0 -0
  46. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_casters.py +0 -0
  47. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_developer.py +0 -0
  48. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_dotenv_guard.py +0 -0
  49. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_env.py +0 -0
  50. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_files.py +0 -0
  51. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_formatters.py +0 -0
  52. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_guard.py +0 -0
  53. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_infer.py +0 -0
  54. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_merge.py +0 -0
  55. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_naming.py +0 -0
  56. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_templates.py +0 -0
  57. {conclude-1.0.1/tests → conclude-1.0.2/test}/test_tomlwrite_and_paths.py +0 -0
@@ -19,6 +19,12 @@ htmlcov/
19
19
  # Build artifacts
20
20
  build/
21
21
  dist/
22
+ *.tgz
23
+ *.tsbuildinfo
24
+
25
+ # Node
26
+ node_modules/
27
+ .verdaccio-storage/
22
28
 
23
29
  # Virtual environments
24
30
  .venv/
@@ -4,6 +4,12 @@ All notable changes to this project are documented in this file. The
4
4
  format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.0.2] - 2026-09-26
8
+
9
+ ### Fixed
10
+
11
+ - Python tree is now under `python/` to match `node/` tree.
12
+
7
13
  ## [1.0.1] - 2026-09-20
8
14
 
9
15
  ### Fixed
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: conclude
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Set up your defaults; conclude infers the env vars, config-file keys, and CLI flags, and resolves them all into one settings object.
5
5
  Project-URL: Homepage, https://github.com/tanakapayam/conclude
6
- Project-URL: Documentation, https://github.com/tanakapayam/conclude/blob/main/docs/guide.md
6
+ Project-URL: Documentation, https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md
7
7
  Project-URL: Repository, https://github.com/tanakapayam/conclude
8
8
  Project-URL: Issues, https://github.com/tanakapayam/conclude/issues
9
- Project-URL: Changelog, https://github.com/tanakapayam/conclude/blob/main/CHANGELOG.md
9
+ Project-URL: Changelog, https://github.com/tanakapayam/conclude/blob/main/python/CHANGELOG.md
10
10
  Author: Payam Tanaka
11
11
  License-Expression: MIT
12
12
  License-File: LICENSE
@@ -92,7 +92,7 @@ conclude deliberately does not do schema validation (values are cast,
92
92
  not validated), secret-manager backends, YAML or JSON files, or named
93
93
  environments; the table says where to look for those. For a dated,
94
94
  feature-by-feature comparison, see
95
- [How conclude compares](https://github.com/tanakapayam/conclude/blob/main/docs/comparison.md).
95
+ [How conclude compares](https://github.com/tanakapayam/conclude/blob/main/docs/python/comparison.md).
96
96
 
97
97
  ## Design principles
98
98
 
@@ -195,28 +195,28 @@ app = conclude.App(
195
195
  The developer file is always guarded, and `.env` is when you ask: the
196
196
  file is used only if it exists, sits in a git working tree, and is
197
197
  gitignored. Otherwise it is skipped quietly, and `describe_sources()`
198
- says why. Details: [the `.env` file](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#6-local-development-a-env-file), [the developer file](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#8-a-private-developer-config-file).
198
+ says why. Details: [the `.env` file](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#6-local-development-a-env-file), [the developer file](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#8-a-private-developer-config-file).
199
199
 
200
200
  ## What's in it
201
201
 
202
202
  - **Inference** of casters, formatters, CLI flags, env var names and
203
203
  config keys from the defaults alone; `opt(type)` for a setting that
204
- starts out unset ([guide, section 1](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#1-the-bare-minimum), [section 3](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#3-when-inference-isnt-quite-enough-casters-and-formatters)).
204
+ starts out unset ([guide, section 1](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#1-the-bare-minimum), [section 3](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#3-when-inference-isnt-quite-enough-casters-and-formatters)).
205
205
  - **Config-file tables**, including a positional shorthand that picks a
206
- table per recipient/deck/profile ([section 4](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#4-a-positional-shorthand--per-recipient-config-tables)).
206
+ table per recipient/deck/profile ([section 4](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#4-a-positional-shorthand--per-recipient-config-tables)).
207
207
  - **`--print-invocation`** and `App.format_invocation()`: print the
208
208
  command line that reproduces a resolved configuration
209
- ([section 5](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#5-debugging-and-documentation---print-invocation)).
209
+ ([section 5](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#5-debugging-and-documentation---print-invocation)).
210
210
  - **A `.env` fallback**, optionally required to be gitignored, sitting
211
- beneath real environment variables ([section 6](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#6-local-development-a-env-file)).
212
- - **A system-wide config file**, the lowest-priority file ([section 7](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#7-machine-wide-defaults-a-system-config-file)).
211
+ beneath real environment variables ([section 6](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#6-local-development-a-env-file)).
212
+ - **A system-wide config file**, the lowest-priority file ([section 7](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#7-machine-wide-defaults-a-system-config-file)).
213
213
  - **A private developer file**, TOML or dotenv, that beats ambient
214
- environment variables ([section 8](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#8-a-private-developer-config-file)).
214
+ environment variables ([section 8](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#8-a-private-developer-config-file)).
215
215
  - **`describe_sources()`** for `--help`: which sources are in play, and
216
- why a given one isn't ([section 9](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#9-turning-off-a-source-and-telling-the-user)).
216
+ why a given one isn't ([section 9](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#9-turning-off-a-source-and-telling-the-user)).
217
217
  - **Templates and docs generated from your defaults**:
218
218
  `format_env()`, `format_toml()`, `format_cli()`
219
- ([section 10](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#10-generating-docs-and-templates)).
219
+ ([section 10](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#10-generating-docs-and-templates)).
220
220
  - Standard library only, fully typed (`py.typed`), Python 3.11+. The
221
221
  gitignore check (developer file, strict `.env`) uses the optional
222
222
  `pathspec` package.
@@ -237,11 +237,11 @@ pip install 'conclude[gitignore]'
237
237
 
238
238
  ## Documentation
239
239
 
240
- - [Guide](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md) -- builds a small CLI, `remind`, one
240
+ - [Guide](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md) -- builds a small CLI, `remind`, one
241
241
  idea at a time.
242
- - [Reference](https://github.com/tanakapayam/conclude/blob/main/docs/reference.md) -- every class, method, and
242
+ - [Reference](https://github.com/tanakapayam/conclude/blob/main/docs/python/reference.md) -- every class, method, and
243
243
  module.
244
- - [Changelog](https://github.com/tanakapayam/conclude/blob/main/CHANGELOG.md).
244
+ - [Changelog](https://github.com/tanakapayam/conclude/blob/main/python/CHANGELOG.md).
245
245
 
246
246
  ## Development
247
247
 
@@ -61,7 +61,7 @@ conclude deliberately does not do schema validation (values are cast,
61
61
  not validated), secret-manager backends, YAML or JSON files, or named
62
62
  environments; the table says where to look for those. For a dated,
63
63
  feature-by-feature comparison, see
64
- [How conclude compares](https://github.com/tanakapayam/conclude/blob/main/docs/comparison.md).
64
+ [How conclude compares](https://github.com/tanakapayam/conclude/blob/main/docs/python/comparison.md).
65
65
 
66
66
  ## Design principles
67
67
 
@@ -164,28 +164,28 @@ app = conclude.App(
164
164
  The developer file is always guarded, and `.env` is when you ask: the
165
165
  file is used only if it exists, sits in a git working tree, and is
166
166
  gitignored. Otherwise it is skipped quietly, and `describe_sources()`
167
- says why. Details: [the `.env` file](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#6-local-development-a-env-file), [the developer file](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#8-a-private-developer-config-file).
167
+ says why. Details: [the `.env` file](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#6-local-development-a-env-file), [the developer file](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#8-a-private-developer-config-file).
168
168
 
169
169
  ## What's in it
170
170
 
171
171
  - **Inference** of casters, formatters, CLI flags, env var names and
172
172
  config keys from the defaults alone; `opt(type)` for a setting that
173
- starts out unset ([guide, section 1](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#1-the-bare-minimum), [section 3](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#3-when-inference-isnt-quite-enough-casters-and-formatters)).
173
+ starts out unset ([guide, section 1](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#1-the-bare-minimum), [section 3](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#3-when-inference-isnt-quite-enough-casters-and-formatters)).
174
174
  - **Config-file tables**, including a positional shorthand that picks a
175
- table per recipient/deck/profile ([section 4](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#4-a-positional-shorthand--per-recipient-config-tables)).
175
+ table per recipient/deck/profile ([section 4](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#4-a-positional-shorthand--per-recipient-config-tables)).
176
176
  - **`--print-invocation`** and `App.format_invocation()`: print the
177
177
  command line that reproduces a resolved configuration
178
- ([section 5](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#5-debugging-and-documentation---print-invocation)).
178
+ ([section 5](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#5-debugging-and-documentation---print-invocation)).
179
179
  - **A `.env` fallback**, optionally required to be gitignored, sitting
180
- beneath real environment variables ([section 6](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#6-local-development-a-env-file)).
181
- - **A system-wide config file**, the lowest-priority file ([section 7](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#7-machine-wide-defaults-a-system-config-file)).
180
+ beneath real environment variables ([section 6](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#6-local-development-a-env-file)).
181
+ - **A system-wide config file**, the lowest-priority file ([section 7](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#7-machine-wide-defaults-a-system-config-file)).
182
182
  - **A private developer file**, TOML or dotenv, that beats ambient
183
- environment variables ([section 8](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#8-a-private-developer-config-file)).
183
+ environment variables ([section 8](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#8-a-private-developer-config-file)).
184
184
  - **`describe_sources()`** for `--help`: which sources are in play, and
185
- why a given one isn't ([section 9](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#9-turning-off-a-source-and-telling-the-user)).
185
+ why a given one isn't ([section 9](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#9-turning-off-a-source-and-telling-the-user)).
186
186
  - **Templates and docs generated from your defaults**:
187
187
  `format_env()`, `format_toml()`, `format_cli()`
188
- ([section 10](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#10-generating-docs-and-templates)).
188
+ ([section 10](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md#10-generating-docs-and-templates)).
189
189
  - Standard library only, fully typed (`py.typed`), Python 3.11+. The
190
190
  gitignore check (developer file, strict `.env`) uses the optional
191
191
  `pathspec` package.
@@ -206,11 +206,11 @@ pip install 'conclude[gitignore]'
206
206
 
207
207
  ## Documentation
208
208
 
209
- - [Guide](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md) -- builds a small CLI, `remind`, one
209
+ - [Guide](https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md) -- builds a small CLI, `remind`, one
210
210
  idea at a time.
211
- - [Reference](https://github.com/tanakapayam/conclude/blob/main/docs/reference.md) -- every class, method, and
211
+ - [Reference](https://github.com/tanakapayam/conclude/blob/main/docs/python/reference.md) -- every class, method, and
212
212
  module.
213
- - [Changelog](https://github.com/tanakapayam/conclude/blob/main/CHANGELOG.md).
213
+ - [Changelog](https://github.com/tanakapayam/conclude/blob/main/python/CHANGELOG.md).
214
214
 
215
215
  ## Development
216
216
 
@@ -0,0 +1,56 @@
1
+ # Releasing conclude (Python)
2
+
3
+ ```
4
+ release published (tag python-v<version>)
5
+ |
6
+ v
7
+ ci ---> build ---> publish-testpypi (workflow_dispatch, target testpypi: a dry run)
8
+ | |
9
+ | +-> publish-pypi waits for a reviewer to approve the `pypi`
10
+ | environment, then publishes with trusted
11
+ | publishing (no stored token)
12
+ |
13
+ +-- one sdist + wheel, built once, uploaded as an artifact and promoted unchanged
14
+ ```
15
+
16
+ TestPyPI is the dry run here (there is no separate staging registry, unlike the Node
17
+ package's GitHub Packages stage): `workflow_dispatch` with `target: testpypi` builds,
18
+ checks, and publishes to TestPyPI, and installs nothing back automatically -- try it
19
+ yourself with `pip install -i https://test.pypi.org/simple/ conclude`. A real release
20
+ (`target: pypi`, or a published GitHub Release) is checked, gated behind a required
21
+ reviewer on the `pypi` environment, then published to PyPI.
22
+
23
+ ## One-time setup
24
+
25
+ 1. **Environments** (repository Settings, Environments):
26
+ - `testpypi`: no protection needed.
27
+ - `pypi`: add yourself (or a team) under *Required reviewers*; that is the approval
28
+ gate.
29
+ 2. **Trusted publisher**, on both <https://pypi.org> and <https://test.pypi.org>, for the
30
+ project `conclude`: this repository, workflow **`python-publish.yml`**, environment
31
+ `pypi` (`testpypi` on TestPyPI). For a project that does not exist yet, use
32
+ "Publishing" -> "Add a new pending publisher".
33
+ 3. Nothing to store: publishing uses OIDC. No PyPI token exists.
34
+
35
+ > **If this workflow was ever registered under a different filename** (for example the
36
+ > project's original `publish.yml`, before this repository moved every language into its
37
+ > own top-level directory): trusted publishing is matched by *workflow filename*, so the
38
+ > existing entries on PyPI and TestPyPI must be updated to `python-publish.yml` -- edit
39
+ > them under "Publishing" on each site -- or the next release fails with a permission
40
+ > error, not a helpful one.
41
+
42
+ ## Releasing
43
+
44
+ 1. Bump `__version__` in `src/conclude/__init__.py` (the single source of truth; see
45
+ `[tool.hatch.version]` in `pyproject.toml`), and the version in
46
+ `docs/python/comparison.md`'s "Verified" line if it's stale. Put a dated entry in
47
+ `CHANGELOG.md` (the release is refused while it says *Unreleased*).
48
+ 2. Merge to `main`.
49
+ 3. Create a GitHub Release from a new tag `python-v<version>` on `main`.
50
+ 4. `publish-pypi` waits for a reviewer to **approve the `pypi` deployment**; approve it
51
+ when you are happy.
52
+
53
+ ## Rehearsing
54
+
55
+ Actions, Publish, Run workflow, target `testpypi` -- builds, checks, and publishes to
56
+ TestPyPI without touching the `pypi` environment or a real version on PyPI.
@@ -3,7 +3,7 @@
3
3
  A feature-by-feature comparison with five libraries people often reach
4
4
  for instead. **Verified 2026-09-19** against each project's
5
5
  documentation and its PyPI metadata; the versions checked are
6
- conclude 1.0.1, ConfigArgParse 1.7.7, jsonargparse 4.52.0,
6
+ conclude 1.0.2, ConfigArgParse 1.7.7, jsonargparse 4.52.0,
7
7
  pydantic-settings 2.15.0, Dynaconf 3.3.5 and python-decouple 3.8.
8
8
 
9
9
  A dash (--) means no built-in support that the project's documentation
@@ -0,0 +1,401 @@
1
+ # The conclude concept
2
+
3
+ conclude is one idea, implemented once per language: **write your settings
4
+ down once, as a set of defaults, and derive everything else.** This
5
+ document is the language-neutral statement of that idea -- what a
6
+ conforming implementation does, independent of syntax. The Python package
7
+ is the reference implementation ([guide](python/guide.md), [reference](python/reference.md));
8
+ other implementations follow this document and pass the conformance
9
+ fixtures in [`spec/`](../spec/README.md).
10
+
11
+ This is **concept version 1**, which is **spec version 1** of the fixtures
12
+ (the Python package 1.0.x implements it). The words "must" and "may" are
13
+ meant literally: a conforming implementation does what "must" says, and
14
+ anything a sentence does not require is up to the implementation.
15
+
16
+ ## 1. The idea
17
+
18
+ A setting that can come from a flag, an environment variable, or a config
19
+ file usually gets written down three times, each with its own name, its own
20
+ type conversion, and its own copy of the default. conclude takes one
21
+ declaration -- a name, a type and a starting value -- and derives:
22
+
23
+ 1. **the interfaces**: the environment variable, the config-file key, the
24
+ CLI flag, and the caster that turns a raw value from any of them into the
25
+ right type;
26
+ 2. **the resolution**: a fixed, layered precedence that merges every source
27
+ into one settings object; and
28
+ 3. **the documentation**: templates and reports generated from the same
29
+ declaration, so they cannot drift from it.
30
+
31
+ ## 2. Vocabulary
32
+
33
+ - A **setting** is a *key*, a *declared type* and a *default*.
34
+ - A **key** is an identifier. Its canonical form is `snake_case`.
35
+ - The **declared types** are `bool`, `int`, `float`, `str` and `list`.
36
+ - A **default** is a value of the declared type, or **unset** (written
37
+ `null` in the fixtures): an *optional* setting declares its type but
38
+ starts out without a value.
39
+ - A **layer** is a source of raw values; a **raw value** is what the layer
40
+ gave (environment values are always text; config-file values are whatever
41
+ TOML gave; CLI values arrive already parsed).
42
+ - **No opinion** means a layer says nothing about a key. It is expressed by
43
+ the key being absent or `null`, and never overrides a lower layer.
44
+
45
+ Settings are ordered (declaration order): generated templates keep it.
46
+
47
+ ## 3. Declaring settings
48
+
49
+ | Token | Meaning | Casts to |
50
+ | --- | --- | --- |
51
+ | `bool` | a flag | true or false |
52
+ | `int` | a number that prints as a whole number when it is one | an integer, or a fraction if that is what was given |
53
+ | `float` | a number | a number |
54
+ | `str` | text | text, or unset |
55
+ | `list` | a list of text items | a list of text, or unset |
56
+
57
+ The declared type -- not the runtime shape of the default -- decides the
58
+ caster and how a default is rendered in a template. A language that can
59
+ infer the type from the default (Python) may do so; one that cannot tell `3`
60
+ from `3.0` (JavaScript) must let the author say it.
61
+
62
+ ## 4. Names (`naming.json`)
63
+
64
+ - **Config key**: the key itself.
65
+ - **Environment variable**: the application name and the key, each with
66
+ every character outside `0-9`, `A-Z`, `a-z` and `_` replaced by `_`,
67
+ joined with `_` and upper-cased (ASCII); a name that would start with a
68
+ digit gets a leading `_`. `("my-app", "foo_bar")` is `MY_APP_FOO_BAR`. A
69
+ setting may override its name.
70
+ - **CLI flag**: `--` and the key with `_` replaced by `-`; case is kept.
71
+ `filter_col` is `--filter-col`.
72
+
73
+ Two application names that differ only in which non-identifier character
74
+ they use sanitize to the same prefix; that is a known, accepted limit.
75
+
76
+ ## 5. Casting (`casters.json`)
77
+
78
+ Every layer's value passes through the caster for its setting's declared
79
+ type. A caster either returns a value, returns **unset**, or fails with an
80
+ error; it never guesses.
81
+
82
+ - **`bool`**: a true boolean passes through. Anything else is turned into
83
+ text, trimmed and lower-cased: `1 true yes on y` are true, `0 false no off
84
+ n` are false, and empty text is false. Any other text is an error (so the
85
+ typo `tru` is caught, not treated as false).
86
+ - **`int`**: `null` and empty text are unset. A number is itself (a whole
87
+ number stays whole). Text is trimmed, then read as an integer -- optionally
88
+ with a redundant all-zero decimal part (`5`, `5.0`, `5.00`) -- or else as a
89
+ decimal or exponent number (`1.5`, `1e3`), which is returned as an integer
90
+ when it is whole. Anything else, including text that is only whitespace and
91
+ hexadecimal text, is an error.
92
+ - **`float`**: `null` and empty text are unset; otherwise the trimmed value
93
+ must be a decimal or exponent number (`0.5`, `.5`, `5.`, `1e-2`), else an
94
+ error.
95
+ - **`str`**: `null` and empty text are unset; anything else becomes text,
96
+ untouched (no trimming).
97
+ - **`list`**: `null` and empty text are unset. A list has each item turned
98
+ into trimmed text, dropping empty ones. Text is split on commas the same
99
+ way, so `a,,b,` is `["a", "b"]` and `,` is an empty list.
100
+
101
+ ## 6. Layers and precedence (`merge.json`)
102
+
103
+ From lowest to highest priority:
104
+
105
+ ```
106
+ defaults < system < user < project < env < developer < CLI
107
+ ```
108
+
109
+ `system`, `user` and `project` are config files (section 7); `env` is the
110
+ process environment, with an optional `.env` file beneath it (section 8);
111
+ `developer` is a private local file (section 10).
112
+
113
+ 1. Layers are merged from lowest to highest, key by key, each value cast as
114
+ it lands; a higher layer replaces a lower one outright (lists are
115
+ replaced, not concatenated).
116
+ 2. A `null` (or absent) value is *no opinion*. An **empty string is a
117
+ value**: it is cast like any other, so `""` overrides a lower layer and
118
+ resolves to unset for `int`, `str` and `list`, and to false for `bool`.
119
+ To have no opinion, leave the key out.
120
+ 3. Keys that no setting declares are ignored, so a file or an environment can
121
+ carry unrelated keys.
122
+ 4. Defaults are cast too, and every declared key is present in the result;
123
+ optional settings that nothing set are `null`.
124
+ 5. A cast failure is an error wherever the bad value came from -- even in a
125
+ layer that a higher one would have overridden.
126
+ 6. Whether a still-unset setting is acceptable is the application's concern,
127
+ not the merge's.
128
+
129
+ ## 7. Config files (`config_layers.json`, `config_tables.json`)
130
+
131
+ | Layer | Location | On by default? |
132
+ | --- | --- | --- |
133
+ | system | `/etc/<name>/config.toml` | no (opt-in) |
134
+ | user | `~/.config/<name>/config.toml` | yes |
135
+ | project | `./.config.toml` | yes |
136
+ | project siblings | `./.config.*.toml`, sorted by name | yes (the pattern is configurable; it can be switched off alone) |
137
+
138
+ The files are merged key by key in that order, so a key set in a later file
139
+ wins; each source may be switched off, and a missing file contributes
140
+ nothing.
141
+
142
+ Files are TOML. A setting lives in a **table** named after the application
143
+ (or another chosen name). A table path has one or two levels, written
144
+ `PARENT` or `PARENT.CHILD`; with two, the parent table supplies shared
145
+ values and the child table overrides them. Only keys that a setting declares
146
+ are read. A missing table, or a name that is not a table, contributes
147
+ nothing; a file that is not valid TOML is a loud error that names the file.
148
+
149
+ **Choosing the table.** An explicit selection (a flag, or its environment
150
+ variable) wins outright. Otherwise a bare positional *shorthand* is looked
151
+ up in the files themselves -- across every file and sibling, as a union: a
152
+ top-level `[SHORTHAND]` table wins, then a nested `[<name>.SHORTHAND]`,
153
+ else the application's own table. Text with an empty part (`a..b`, `a.`,
154
+ `.a`) or more than two levels is an error, not a spelling of something else.
155
+
156
+ ## 8. The `.env` file (`dotenv.json`)
157
+
158
+ An opt-in **fallback beneath the real environment**: a variable that is
159
+ actually set in the process always wins, and the file only supplies the rest.
160
+ Its keys are environment variable names.
161
+
162
+ The file is UTF-8 on every platform, and a leading byte-order mark is
163
+ ignored. One variable per line: `NAME=value`, with an optional leading
164
+ `export `; blank lines and lines starting with `#` are skipped, as are lines
165
+ without an `=`. The key and the value are trimmed, only the first `=`
166
+ separates them, and a later duplicate wins. A value wrapped in matching
167
+ quotes has them removed: single quotes are literal, double quotes also decode
168
+ `\n`, `\t`, `\r`, `\\` and `\"` (any other backslash sequence is left as
169
+ written). Not supported, on purpose: multi-line values, inline comments, and
170
+ `${VAR}` interpolation.
171
+
172
+ An application may require the file to pass the gitignore guard (section 9)
173
+ before it is read; if it fails, nothing is read from it.
174
+
175
+ ## 9. The gitignore guard (`guard.json`, `gitignore.json`)
176
+
177
+ A file that beats -- or quietly backstops -- the environment must never come
178
+ from somewhere it shouldn't: a fresh clone, a CI checkout, a deployed image.
179
+ "Inside a git working tree, and ignored by it" is the practical proxy for
180
+ "this is somebody's private local file", and it can be checked without a
181
+ `git` executable.
182
+
183
+ The checks run in this order and the first failure is the **reason**:
184
+
185
+ 1. **Kill switch**: if a kill-switch variable is configured and the real
186
+ environment sets it (trimmed, case-insensitive) to `0`, `off`, `false` or
187
+ `no`: `disabled by <VAR>=<value>` (the trimmed value, as written).
188
+ 2. The file does not exist: `file not found`.
189
+ 3. It is not inside a git working tree -- no ancestor directory holds a
190
+ `.git` (a directory, or a file, as in a worktree or submodule):
191
+ `not inside a git working tree`.
192
+ 4. No ignore rule matches it: `not covered by .gitignore`.
193
+ 5. Otherwise the file is **active**.
194
+
195
+ The kill switch is a control knob, not a setting: it is read only from the
196
+ real environment, and it is checked first so it also avoids needing the
197
+ matcher.
198
+
199
+ **Matching the rules.** The rules are the `.gitignore` files from the
200
+ repository root down to the file's directory, plus the repository's
201
+ `.git/info/exclude`. Nearer `.gitignore` files override farther ones, and
202
+ `info/exclude` has the lowest priority; within a file the last matching
203
+ pattern wins (so `!pattern` un-ignores); and an ignored directory ignores
204
+ everything beneath it, whatever the files inside say. The global
205
+ `core.excludesFile` is not consulted, and the check looks at patterns, not
206
+ at git's index -- a *tracked* file that matches a pattern still counts as
207
+ ignored. Both limits fail closed except that last one.
208
+
209
+ If the matching capability is unavailable (an optional dependency that is not
210
+ installed), that is a **setup error**, reported as such rather than as an
211
+ ordinary inactive file -- see the quiet-versus-loud rule below.
212
+
213
+ ## 10. The developer layer
214
+
215
+ An opt-in, private, project-local file that sits **above the environment**
216
+ and below the CLI. Environment variables are process-wide and can leak
217
+ between projects; a file whose location the project decides is scoped to
218
+ exactly the project it belongs to, so it wins over ambient state.
219
+
220
+ - **Where it is declared** is a per-ecosystem *manifest binding* (section
221
+ 14): the committed project manifest names the file, relative to the
222
+ manifest's directory.
223
+ - **Format follows the name.** A name ending in `.toml` is TOML, read with
224
+ the same tables as any config file (section 7). Any other name (say
225
+ `.env.local`) is dotenv (section 8), keyed by the application's
226
+ environment variable names, so the same private file can serve other tools;
227
+ dotenv has no tables, and an empty value is cast like an empty environment
228
+ variable (section 6), not skipped.
229
+ - **The guard is mandatory** (section 9), with a kill switch named after the
230
+ application: `<APP>_DEVELOPER_CONFIG`, using the environment naming of
231
+ section 4.
232
+
233
+ The layer is always in one of four states, which reports show verbatim:
234
+
235
+ | State | Meaning |
236
+ | --- | --- |
237
+ | `not opted in` | the application never asked for the layer |
238
+ | `not configured` | opted in, but the manifest names no file |
239
+ | `configured, inactive` | a file is named but a guard check failed; the reason says which |
240
+ | `configured, active` | the file is read |
241
+
242
+ An `.env` file held to the guard reports `disabled` (no path), `active` or
243
+ `inactive` in the same spirit (for example `.env -- active (gitignored)` or
244
+ `.env -- inactive (not covered by .gitignore)`); without the guard it is
245
+ simply `active` whenever it has a path.
246
+
247
+ **Quiet for ordinary situations, loud for broken setups.** Every state above
248
+ is ordinary -- a teammate who has not made their file yet, a CI run, a
249
+ deploy -- so an inactive layer contributes nothing and raises nothing;
250
+ reports say why. Two things are setup errors and raise: the missing matching
251
+ capability (section 9), and an *active* file that is not valid TOML. Status
252
+ and report calls never raise.
253
+
254
+ ## 11. Generated templates and invocations (`templates.json`, `invocation.json`)
255
+
256
+ Every template renders the same declared settings, in order, with their
257
+ defaults; options can skip settings, replace a default with the *effective*
258
+ one, rename an environment variable, choose the TOML table (or drop its
259
+ header), and rename a CLI metavar.
260
+
261
+ A default's **plain text** is: `true` or `false` for a `bool`; the items
262
+ joined with `,` for a `list`; a `float` with a decimal point even when whole
263
+ (`3.0`); otherwise the value's text. A setting with no default has no plain
264
+ text and renders as a placeholder.
265
+
266
+ **Environment template**: one `NAME=value` line per setting, or `# NAME=` for
267
+ an unset one. The value is written bare if it consists only of letters,
268
+ digits and `_` (from any script) and the punctuation `. / : @ % + , -`
269
+ (that is, `[\w./:@%+,-]*`, so the empty string is bare); otherwise in single
270
+ quotes if it holds no `'` and no newline, tab or carriage return; otherwise in
271
+ double quotes with `\\`, `\"`, `\n`, `\t` and `\r` escaped.
272
+
273
+ **TOML template**: a `[table]` header (the application name by default; a
274
+ name that is not a bare key -- `A-Za-z0-9_-` -- is quoted), then one
275
+ `key = value` line per setting with native values: `true`/`false`, numbers, a
276
+ list as `["a", "b"]`, and text as a double-quoted string in which `\`, `"`,
277
+ newline, tab and carriage return are escaped as `\\`, `\"`, `\n`, `\t`, `\r`
278
+ and every other control character (U+0000-U+001F and U+007F) as `\uXXXX` with
279
+ upper-case hexadecimal. An unset setting is `# key =`. An empty table path
280
+ means no header.
281
+
282
+ **CLI reference**: one line per flag. A `bool` is the bare flag; any other
283
+ setting is the flag and `<METAVAR>` (the key upper-cased, or the override).
284
+ Each line ends with `(default: text)`, where an unset default reads `none`,
285
+ an empty text reads `""`, and the flag column is padded to the widest so the
286
+ `(default:` parts align, with one space after it.
287
+
288
+ **Reproducing a run.** The inverse of resolving: given a resolved configuration,
289
+ a standalone command line that gets back to it with no environment variables,
290
+ config files or shorthand -- what a `--print-invocation` flag prints. Settings
291
+ are visited in declaration order, and each is written as `--flag=value` (a bare
292
+ `--flag` for a `bool` that is on) unless it is left out. A setting is left out
293
+ when it equals its default -- omitting a flag already reproduces its default, and
294
+ this covers every setting automatically -- when it is a `bool` that is off (there
295
+ is no flag to turn one off), when its value is unset, or when the caller skips it
296
+ (`skip` beats `alwaysInclude`, which forces a setting that equals its default to
297
+ be written). `compareDefaults` replaces what counts as each setting's default for
298
+ one call, for a setting whose effective default is substituted after resolving.
299
+ The program name, if given, comes first. Values are POSIX shell-quoted: bare if
300
+ every character is a letter or digit from ASCII, or one of `_ @ % + = : , . / -`,
301
+ otherwise in single quotes with each `'` written as `'"'"'` (so non-ASCII letters
302
+ are quoted, and an empty text is `''`); a list is its items joined with `,`, then
303
+ quoted; a `float` keeps its decimal point (`3.0`).
304
+
305
+ ## 12. Reports (`sources.json`)
306
+
307
+ A report of the sources in play, for a `--help` epilog, is a header line
308
+ `config sources:` and then one row per source, lowest priority first:
309
+
310
+ ```
311
+ config sources:
312
+ system config disabled
313
+ user config /home/me/.config/myapp/config.toml
314
+ project config .config.toml, .config.*.toml
315
+ .env file disabled
316
+ developer config not opted in
317
+ ```
318
+
319
+ Each row is two spaces, the label padded to the longest label
320
+ (`developer config`), two more spaces, and the value. The value of the
321
+ config-file rows is the path, or `disabled` when that source is switched off;
322
+ the project row also lists the sibling pattern (in the same directory as the
323
+ project file) unless siblings are off. The `.env` row is `disabled`, its path
324
+ (when no guard is required), or -- for a guarded file --
325
+ `<path> -- active (gitignored)` or `<path> -- inactive (<reason>)`. The
326
+ developer row is one of the four states of section 10, with the guard's reason
327
+ for an inactive file.
328
+
329
+ ## 13. Conformance
330
+
331
+ An implementation conforms to spec version 1 when it passes every fixture in
332
+ [`spec/`](../spec/README.md). The fixtures are data, not code; each
333
+ implementation writes a small adapter from a fixture file to its own API.
334
+
335
+ | Fixture | Sections | What it covers |
336
+ | --- | --- | --- |
337
+ | `naming.json` | 4 | environment variable, flag and key names |
338
+ | `casters.json` | 3, 5 | casting for every declared type |
339
+ | `dotenv.json` | 8 | the `.env` dialect |
340
+ | `merge.json` | 6 | precedence, no-opinion, empty values, errors |
341
+ | `config_layers.json` | 7 | merging system, user, project and sibling files; tables |
342
+ | `config_tables.json` | 7 | table selection and the positional shorthand |
343
+ | `templates.json` | 11 | the environment, TOML and CLI templates |
344
+ | `invocation.json` | 11 | reproducing a resolved configuration as a command line |
345
+ | `gitignore.json` | 9 | rule matching, judged by real `git` |
346
+ | `guard.json` | 9 | the guard's checks and reasons |
347
+ | `sources.json` | 10, 12 | the report of which sources are in play |
348
+
349
+ Not yet covered by fixtures: how each binding reads its manifest (section 10;
350
+ the wording of a `not configured` reason is binding-specific), and how a
351
+ language exposes flags to its CLI parser.
352
+
353
+ ## 14. What varies by language
354
+
355
+ | Concern | Python (reference) | TypeScript ([`node/`](../node/README.md)) |
356
+ | --- | --- | --- |
357
+ | Declaring settings | a dict of defaults; `opt(type)` for an unset one | an object of defaults, with `int()`, `float()`, `str()`, ... for numbers and unset settings |
358
+ | Type inference | the default's runtime type | the default's runtime type, plus explicit `int` and `float` (D2) |
359
+ | Key style in the API | `snake_case` | `camelCase`, converted (D1) |
360
+ | Manifest binding | `pyproject.toml`, `[tool.conclude.developer] config = "..."` | `package.json`, `"conclude": { "developer": { "config": "..." } }` (D3) |
361
+ | CLI | builds `argparse` flags | flag definitions in `node:util.parseArgs`' shape, plus a thin `parseArgs` wrapper (D4) |
362
+ | TOML | standard library | a small dependency (D5) |
363
+ | Gitignore matching | the optional `pathspec` extra | an optional dependency (D5) |
364
+
365
+ **Decisions for other bindings.** The TypeScript package follows all five (its
366
+ [guide](node/guide.md) and [reference](node/reference.md) show how):
367
+
368
+ - **D1: canonical names in files and the environment.** Config-file keys and
369
+ environment variable names use the canonical `snake_case` of section 4, so
370
+ one config file or `.env` serves every implementation; a binding's API may
371
+ use its own casing and converts (`filterCol` is `filter_col`).
372
+ - **D2: numeric kinds are explicit** where the language cannot distinguish
373
+ them, and templates render per declared kind (section 11).
374
+ - **D3: each ecosystem uses its own manifest** to name the developer file;
375
+ in a repository with several, they may name the same file.
376
+ - **D4: no bundled CLI parser.** The binding exposes derived flag
377
+ definitions rather than owning the command line.
378
+ - **D5: dependencies are minimal.** A TOML parser is required; gitignore
379
+ matching is optional, and its absence is a setup error (section 9).
380
+
381
+ ## 15. Implementation-defined
382
+
383
+ The fixtures deliberately say nothing about these; implementations may differ
384
+ and should document what they do:
385
+
386
+ - integers beyond 2^53, `inf` and `nan`, underscores or non-ASCII digits in
387
+ numerals, and how a native boolean is cast by `int` or `str`;
388
+ - line breaks other than `\n`, `\r\n` and `\r` in a `.env` file;
389
+ - how a very small or very large `float` is written (exponent form) in a
390
+ template;
391
+ - reproducing a run for a `bool` whose default is true (there is no flag to turn
392
+ it off, so it cannot be reproduced), and for a non-`bool` setting whose
393
+ resolved value is unset while its default is not (Python writes `None`; the
394
+ TypeScript binding leaves it out);
395
+ - a default of empty text on a `str` setting when it is *resolved* (the
396
+ template shows it; the merge casts it to unset);
397
+ - what a boolean, a list or another non-text value does when cast to `str`,
398
+ and what a boolean does when cast to `int` or `float` (Python converts;
399
+ the TypeScript binding rejects them);
400
+ - the wording of the manifest-related reasons (`not configured`), and of
401
+ errors.