kinemata 0.2.0__tar.gz → 0.3.0.dev1__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 (90) hide show
  1. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/CHANGELOG.md +25 -0
  2. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/PKG-INFO +6 -5
  3. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/README.md +5 -4
  4. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/design.md +4 -2
  5. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/introduction.md +85 -9
  6. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/pyproject.toml +1 -1
  7. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/claims.py +10 -0
  8. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/cli.py +26 -0
  9. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/config.py +24 -4
  10. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/parity.py +91 -22
  11. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/shape.py +5 -3
  12. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/PKG-INFO +6 -5
  13. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_parity.py +127 -0
  14. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/AIPOLICY.md +0 -0
  15. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/LICENSE.md +0 -0
  16. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/MANIFEST.in +0 -0
  17. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/american-english.toml +0 -0
  18. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/bibliography.toml +0 -0
  19. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/citations.md +0 -0
  20. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/resources.toml +0 -0
  21. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/structure.md +0 -0
  22. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/examples/ci-github-actions.yml +0 -0
  23. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/kinemata.toml +0 -0
  24. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/setup.cfg +0 -0
  25. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/__init__.py +0 -0
  26. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/access.py +0 -0
  27. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/__init__.py +0 -0
  28. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/bibliography.py +0 -0
  29. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/constants.py +0 -0
  30. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/mapping.py +0 -0
  31. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/patterns.py +0 -0
  32. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/substitutions.py +0 -0
  33. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/values.py +0 -0
  34. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/baseline.py +0 -0
  35. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/bypass.py +0 -0
  36. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/citations.py +0 -0
  37. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/confirm.py +0 -0
  38. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/context.py +0 -0
  39. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/contract.py +0 -0
  40. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/exclusion.py +0 -0
  41. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/gates.py +0 -0
  42. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/interpose.py +0 -0
  43. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/literals.py +0 -0
  44. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/probe.py +0 -0
  45. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/projection.py +0 -0
  46. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/prose.py +0 -0
  47. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/provenance.py +0 -0
  48. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/pytest_plugin.py +0 -0
  49. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/report.py +0 -0
  50. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/resources.py +0 -0
  51. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/stamps.py +0 -0
  52. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/targets.py +0 -0
  53. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/SOURCES.txt +0 -0
  54. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/dependency_links.txt +0 -0
  55. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/entry_points.txt +0 -0
  56. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/requires.txt +0 -0
  57. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/top_level.txt +0 -0
  58. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_access.py +0 -0
  59. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_against_example.py +0 -0
  60. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_baseline.py +0 -0
  61. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_bibliography.py +0 -0
  62. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_bypass.py +0 -0
  63. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_catch_a_ratchet.py +0 -0
  64. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_claims.py +0 -0
  65. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_claims_declared.py +0 -0
  66. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_claims_ratchet.py +0 -0
  67. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_config_keys.py +0 -0
  68. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_confirm.py +0 -0
  69. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_context.py +0 -0
  70. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_contract.py +0 -0
  71. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_corpus_validation.py +0 -0
  72. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_delivery.py +0 -0
  73. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_docstring_claims.py +0 -0
  74. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_exclusion.py +0 -0
  75. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_gates.py +0 -0
  76. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_import_kind.py +0 -0
  77. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_interpose.py +0 -0
  78. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_literals.py +0 -0
  79. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_packaging.py +0 -0
  80. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_probe.py +0 -0
  81. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_prose.py +0 -0
  82. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_provenance.py +0 -0
  83. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_registry_where.py +0 -0
  84. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_resources.py +0 -0
  85. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_section_path.py +0 -0
  86. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_shape.py +0 -0
  87. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_stamps.py +0 -0
  88. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_substitutions.py +0 -0
  89. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_targets.py +0 -0
  90. {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_toml_value.py +0 -0
@@ -9,6 +9,31 @@ a project outside this repository runs against kinemata and upgrades between pub
9
9
  Commits are in `git log`; the reasoning behind a design decision stays in the document that
10
10
  carries the decision. A changelog repeating either becomes a second carrier and goes stale.
11
11
 
12
+ ## Unreleased
13
+
14
+ **New**
15
+
16
+ - `[[parity]] translate_identifier`: a second declared translation, for the identifiers, in a run
17
+ that also compares values. `translate` lands on whichever side the parity compares — the values
18
+ when a `field` is named — so a declaration whose keys are spelled one way and whose values
19
+ another had no form for the first, and membership runs in every parity, so both spellings came
20
+ back as findings. The only way to make such a run green was to have the *oracle* re-key its own
21
+ output, which puts the hop where no reader of the config can see it: the thing `translate` exists
22
+ to prevent. Refused alongside a bare `translate` when no `field` is named, where there is one
23
+ side and two keys naming it would be a declaration saying the same thing twice.
24
+
25
+ **Fixed**
26
+
27
+ - `parity` now prints `set-valued: N` for declared cells that held a list, on every run, clean or
28
+ failing. A list is compared as a set on purpose — a reorder is not a finding, and that rule is
29
+ unchanged — but nothing said so, so a declaration pinning an enum's `choices` against the code's
30
+ own tuple reported `agreeing on choices` while an oracle listing the same three values in reverse
31
+ agreed with it. Measured against a real adopter manifest, not constructed. This was the last
32
+ suppression in the tool that reported nothing; the others each gained a line earlier, `negated:`
33
+ most recently. The count is deliberately not a list of sites: a list cell is a suppression the
34
+ project declared by writing a list and can see in its own file, where a negation is one the tool
35
+ inferred from prose nobody wrote for it.
36
+
12
37
  ## 0.2.0
13
38
 
14
39
  **Fixed**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemata
3
- Version: 0.2.0
3
+ Version: 0.3.0.dev1
4
4
  Summary: One declared place per fact; find the code that re-derives it.
5
5
  Author-email: Jei Blanchard <jjb@eng.ufl.edu>
6
6
  License-Expression: GPL-3.0-only
@@ -309,9 +309,10 @@ that has since been renamed: the claim survives, its provenance does not.
309
309
  size of the exemption list **on every run, including clean ones**. `review` is
310
310
  deliberately unaffected — the ratchet governs the gate, not the advice.
311
311
 
312
- **One list, three gates.** `claims` and `undeclared` read the same baseline, so a
313
- documentation tree or a closed registry that is already failing can be adopted
314
- the same way. A second exemption list was the alternative and is the failure
312
+ **One list, several gates**, and the number is deliberately not given here — it
313
+ said *three* and was overtaken twice. Every gate that ratchets reads the same
314
+ baseline, so a documentation tree or a closed registry that is already failing
315
+ can be adopted the same way; `kinemata baseline` prints the scopes. A second exemption list was the alternative and is the failure
315
316
  mode: two lists eventually disagree about what a project accepted, and the one
316
317
  nobody reads is the one still exempting something real. Records carry the check
317
318
  that produced them, so no gate reports another's as fixed — it names them
@@ -516,7 +517,7 @@ removal, visible in a diff.
516
517
  by hand can at least declare that the instruction to run them still exists. That
517
518
  is a reminder about a reminder, and worth what it sounds like.
518
519
 
519
- The companion guard is a number with an oracle. This suite is **1101 tests**, and
520
+ The companion guard is a number with an oracle. This suite is **1107 tests**, and
520
521
  `kinemata claims` settles that figure against `pytest --collect-only`, so a
521
522
  suite that silently shrinks fails the gate rather than passing faster.
522
523
 
@@ -285,9 +285,10 @@ that has since been renamed: the claim survives, its provenance does not.
285
285
  size of the exemption list **on every run, including clean ones**. `review` is
286
286
  deliberately unaffected — the ratchet governs the gate, not the advice.
287
287
 
288
- **One list, three gates.** `claims` and `undeclared` read the same baseline, so a
289
- documentation tree or a closed registry that is already failing can be adopted
290
- the same way. A second exemption list was the alternative and is the failure
288
+ **One list, several gates**, and the number is deliberately not given here — it
289
+ said *three* and was overtaken twice. Every gate that ratchets reads the same
290
+ baseline, so a documentation tree or a closed registry that is already failing
291
+ can be adopted the same way; `kinemata baseline` prints the scopes. A second exemption list was the alternative and is the failure
291
292
  mode: two lists eventually disagree about what a project accepted, and the one
292
293
  nobody reads is the one still exempting something real. Records carry the check
293
294
  that produced them, so no gate reports another's as fixed — it names them
@@ -492,7 +493,7 @@ removal, visible in a diff.
492
493
  by hand can at least declare that the instruction to run them still exists. That
493
494
  is a reminder about a reminder, and worth what it sounds like.
494
495
 
495
- The companion guard is a number with an oracle. This suite is **1101 tests**, and
496
+ The companion guard is a number with an oracle. This suite is **1107 tests**, and
496
497
  `kinemata claims` settles that figure against `pytest --collect-only`, so a
497
498
  suite that silently shrinks fails the gate rather than passing faster.
498
499
 
@@ -380,8 +380,10 @@ bibliography: a reference key is recognizable because the stamp's delimiters wer
380
380
  so, which is why that one registry can answer, and until a bibliography was declared `kinemata
381
381
  undeclared` run here refused rather than reporting a false clean.
382
382
 
383
- ⚑ **Closability is a boundary for two of the three kinds, settled 2026-09-13** rather than left as
384
- an open gap. `code-patterns` and `substitutions` are **negative** registries — they declare what
383
+ ⚑ **Closability is a boundary for `code-patterns` and `substitutions`, and a deliberate gap for
384
+ `python-constants`, settled 2026-09-13** rather than left as an open gap. ⚑ **Named rather than
385
+ counted**: this read *"two of the three kinds"* until 2026-09-19, when a fourth kind existed and
386
+ the phrase had quietly become a claim about the whole adapter set rather than about these three. `code-patterns` and `substitutions` are **negative** registries — they declare what
385
387
  must *not* appear, and the complement of "not a forbidden spelling" is every other string in the
386
388
  language, so there is no membership list to enumerate and no closed-world question to ask. They
387
389
  will never be closable, and that is a property of the data model. `python-constants` is a genuine
@@ -253,13 +253,28 @@ extract = '(?m)^(\S+)=(.*)$'
253
253
  field = ["default", "primary"]
254
254
  authority = "declared"
255
255
 
256
- # At most one translation, on the declared side, so a reader of the config can
257
- # see that a comparison is not literal. `map` or `pattern`/`replacement`,
258
- # never both.
256
+ # A translation, on the declared side, so a reader of the config can see that a
257
+ # comparison is not literal. `map` or `pattern`/`replacement`, never both.
258
+ # `translate` lands on whichever side this parity compares: the values when a
259
+ # `field` is named, the identifiers when one is not.
259
260
  [parity.translate]
260
261
  pattern = '/$' # the manifest writes a directory prefix with a
261
262
  replacement = '' # trailing separator; the code carries none
262
263
 
264
+ # The second hop, for a declaration whose KEYS are spelled one way and whose
265
+ # VALUES are spelled another. Membership runs in every parity, so without this
266
+ # a value run compared identifiers raw and reported both spellings as findings
267
+ # -- and the only way to green it was to have the ORACLE re-key its own output,
268
+ # which puts the hop where no reader of the config can see it. That escape is
269
+ # the reason this key exists rather than a convenience.
270
+ #
271
+ # Refused alongside a bare `translate` when no `field` is named: there is one
272
+ # side to translate then, and two keys naming it is a declaration saying the
273
+ # same thing twice.
274
+ [parity.translate_identifier]
275
+ pattern = '^agent\.<agent>\.' # the manifest declares one templated row
276
+ replacement = 'agent.claude.' # the code emits it per agent
277
+
263
278
  # `relation` says what the two sets are claimed to be. Without it a parity
264
279
  # means `equal`, which is what it has always meant. The others are weaker or
265
280
  # inverted claims, spelled as what IS claimed rather than as a direction
@@ -294,6 +309,21 @@ replacement = '' # trailing separator; the code carries none
294
309
  # below can be `disjoint`); a list of what is broken is not. Measured both ways
295
310
  # on a scratch tree rather than reasoned about, after an adopter asked whether
296
311
  # artifact freshness was a shape this tool wanted.
312
+ #
313
+ # **AND THE DECLARED SIDE MUST NOT BE DERIVED FROM THE PRODUCED ONE.** A
314
+ # registry enumerated from the same place the oracle reads compares that place
315
+ # with itself: whatever leaves, leaves both sides at once, and the run reports
316
+ # agreement. The adopter who took the freshness shape hit this and reported it
317
+ # with a control -- a registry read off the artifact directory cannot see an
318
+ # artifact DELETED, because the row and the file vanish together, while a
319
+ # hand-maintained declaration reds immediately. **A declaration is a statement
320
+ # of what ought to be there**, which is exactly what it cannot be if the tree
321
+ # writes it.
322
+ #
323
+ # This is `docs/design.md`'s rule about `declared()` arriving from the other
324
+ # side: the declaration and the thing it judges must not share a source. There
325
+ # it is an adapter calling the project's own classifier; here it is a registry
326
+ # generated from the directory being checked. Same defect, and both pass.
297
327
  [[parity]]
298
328
  registry = "denials"
299
329
  relation = "declared_contains"
@@ -1255,12 +1285,21 @@ limit is closed, in the same commit that closes it.**
1255
1285
  ⚑ **A fourth cause is not a spelling divergence at all** and is worth separating, because no
1256
1286
  translation form whatever would reach it: a **parametric row with no code-side member**. That one
1257
1287
  is the registry's addressing, not the comparison's notation.
1258
- ⚑ **And the sharpest consequence is structural rather than a matter of degree.** The translation
1259
- has one target, chosen by whether `field` is declared — so a comparison that names a `field`
1260
- translates the **values** and compares the **identifiers raw**. Membership runs either way and is
1261
- not made optional by a value declaration, which is deliberate; but it means a declaration whose
1262
- identifiers need a spelling map and whose values need comparing has **no form for the first
1263
- one at all**.
1288
+ ⚑ **~~And the sharpest consequence is structural rather than a matter of degree.~~ Closed
1289
+ 2026-09-19 by `translate_identifier`.** The translation had one target, chosen by whether `field`
1290
+ was declared — so a comparison naming a `field` translated the **values** and compared the
1291
+ **identifiers raw**, and membership runs either way. A declaration whose identifiers need a
1292
+ spelling map *and* whose values need comparing had no form for the first, and the only way to
1293
+ green one was to have the oracle re-key its own output.
1294
+ ⚑ **That workaround is why it was built, and the general form of the argument is worth more than
1295
+ the key.** A capability whose absence is routed around **silently** is not absent — it is
1296
+ undeclared, and the routing lands in the one place this entry already says a reader cannot see.
1297
+ Measured on an adopter's real manifest row rather than argued: the re-keyed oracle turns the run
1298
+ green, and nothing in the config says a hop happened.
1299
+ ⚑ **It moved no reach figure**, which is stated because the temptation is to claim one: the row
1300
+ it was measured against is still unreached, on an assertion about a *templated* declaration row
1301
+ standing for several produced identifiers, which is the fourth cause named above and is the
1302
+ registry's addressing rather than the comparison's notation.
1264
1303
  ⚑ **“No real row has needed two” was already false when it was published**, by a measurement in
1265
1304
  this project's own records rather than by the one above: of an adopting project's five translated
1266
1305
  rows, one needs two — a reference hop *and* a segment substitution — and the characterization
@@ -1571,6 +1610,26 @@ down, and the two shapes at the end are the findings that matter most.
1571
1610
  that is neither equality nor membership**: an **ordering** (a list is compared as a set, on
1572
1611
  purpose), an **inequality**, and a second declared **translation**. Three conformance rows in the
1573
1612
  measured population, one each.
1613
+ ⚑ **All three were run against that project's real files on 2026-09-19 rather than reasoned
1614
+ about, and two were reachable only in a form worse than the gap.** An oracle can re-key its
1615
+ output to the declaration's spelling, which carried the second translation — and spent the
1616
+ visibility `translate` exists for. An oracle can print a **verdict** per row against a declared
1617
+ constant, which carries the inequality — and then the comparison is the oracle's and nothing
1618
+ checks it. **Neither counts as reach**, on the rule that an oracle answers what the code
1619
+ *produces* while a declaration answers what it *must be*: once the oracle answers whether it is
1620
+ **right**, the declaration is a rubber stamp.
1621
+ ⚑ **The translation half was then closed properly**, the same day, by `translate_identifier` —
1622
+ the entry above carries it. **Finding the silent workaround is what justified building it**, not
1623
+ the row: a capability an adopter can route around without saying so is undeclared rather than
1624
+ absent. The inequality stands, and so does the ordering.
1625
+ ⚑ **The ordering half is disclosed rather than silent** — `set-valued: N` on every run, clean or
1626
+ failing, whose declared cell held a list. The set rule is deliberate and stays; what was wrong is
1627
+ that a declaration pinning an enum's `choices` printed *agreeing on choices* while the oracle
1628
+ listed the same three values in **reverse**, so the run read exactly like one that had checked
1629
+ the order. **The count does not say the comparison was wrong. It says which question was
1630
+ answered.** This was the last suppression here that reported nothing, after `negated:`.
1631
+ **What the claim itself would take:** an opt-in ordered comparison — a form question rather than
1632
+ a missing capability, since the rendering already knows the cell was a list.
1574
1633
  ⚑ **The other half — a declared entry's *field* against what the code prints for it — landed the
1575
1634
  same day**, with the translation and the authority marker the same project's worked rows asked
1576
1635
  for. **The entry stays open and `accepted`**, and what is left is of two different kinds. One is
@@ -1720,6 +1779,23 @@ down, and the two shapes at the end are the findings that matter most.
1720
1779
  ⚑ **One residual is the rule language rather than the adapter, and it has its own entry below.**
1721
1780
  Recorded separately rather than quietly reclassified, because the adapter reaches exactly what the
1722
1781
  measurement said it would.
1782
+ - **accepted** · **There is no expression language in the config, in any mechanism.** A `[[shape]]`
1783
+ guard and claim, a `[[probe]]` outcome and a `[[parity]]` comparison are each a table of fixed
1784
+ operators, so a project wanting a relation none of them spells — an ordering between two values,
1785
+ an arithmetic bound, a condition joining two fields — reaches the one escape there is: a
1786
+ **predicate the project names**, which costs it a module to import and a function to maintain.
1787
+ **What it would take:** a syntax, a parser, and a defined semantic for every type a declaration
1788
+ can hold. The core takes no runtime dependency, so nothing could be borrowed and it would be
1789
+ written here — a second, worse copy of a solved thing, inside the package whose subject is code
1790
+ that re-derives what is already declared. That is the whole of the argument, and it is an
1791
+ argument about **cost**: no part of the design forecloses one.
1792
+ ⚑ **What would revive it: projects reaching for the predicate escape to express *relations
1793
+ between declared values* rather than models of their own data.** The escape exists for the
1794
+ second. A population using it for the first says the operator tables are the wrong **shape**
1795
+ rather than merely short, which is the finding no single unusual claim can establish — so the
1796
+ evidence is a count across projects, not one project's case.
1797
+ ⚑ **Said explicitly because the phrasing here used to be *"refused rather than deferred"***, which
1798
+ reads as a promise never to revisit, and a disposition has no standing to make one.
1723
1799
  - **accepted** · **A rule speaks about the selected set, not about each group within it.** With a
1724
1800
  flattened registry every cell of a matrix is an entry, so a rule can claim something about *all*
1725
1801
  cells. *"Every row's columns are exactly the relations axis"* is a claim about **each first-level
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kinemata"
7
- version = "0.2.0"
7
+ version = "0.3.0.dev1"
8
8
  description = "One declared place per fact; find the code that re-derives it."
9
9
  readme = "README.md" # without this a built distribution ships no
10
10
  # long description at all: the PyPI page and
@@ -164,6 +164,16 @@ EXAMPLE_STEM = re.compile(r"(^|/)[a-z]\.[a-z]+$")
164
164
  #: Prefixes that put a path outside the tree, where this cannot verify it.
165
165
  #: ``%`` joined them after a Windows ``%APPDATA%\\httpie\\config.json`` in
166
166
  #: httpie's documentation was reported as a dead path in httpie's own tree.
167
+ #:
168
+ #: **This skip is silent and stays silent, re-examined 2026-09-19 against the
169
+ #: negation count and deliberately not given one.** An adopter noticed the
170
+ #: asymmetry -- a ``~/`` spelling would have made their failing header pass --
171
+ #: and declined to use it, on the grounds that going green by hiding from the
172
+ #: checker is not the same as going green. They were right about their header
173
+ #: and the asymmetry is not the same shape: a token starting with one of these
174
+ #: **was never a claim about this tree**, which is a classification, where a
175
+ #: negated token *was* one and prose withdrew it. Counting a claim that never
176
+ #: existed would put a number on every absolute path in every document.
167
177
  EXTERNAL_PREFIXES = ("~", "/", "#", "@", "$", "%")
168
178
 
169
179
  #: A backslash means this is not a path *here*: a Windows path, which cannot be
@@ -754,6 +754,30 @@ def cmd_undeclared(args: argparse.Namespace) -> int:
754
754
  return 0
755
755
 
756
756
 
757
+ def _set_valued(result: Parity, args: argparse.Namespace) -> None:
758
+ """Say when a value comparison was a *set* comparison.
759
+
760
+ A list cell is compared as a set on purpose, so this is a disclosure and
761
+ never a finding -- but it was the one suppression here that printed
762
+ nothing, which is exactly what made a withdrawn path claim invisible in
763
+ ``claims`` until 0.2.0.
764
+
765
+ **A count and no identifiers**, deliberately, and the distinction is the
766
+ one that release already drew: a negation is a suppression the tool
767
+ *inferred* from prose nobody wrote for it, so the sites are the only way to
768
+ judge it, while a list cell is a suppression the project **declared** by
769
+ writing a list. Its author can see which rows those are; what they could
770
+ not see is that order went unchecked.
771
+ """
772
+ if not result.set_valued or args.quiet:
773
+ return
774
+ count = len(result.set_valued)
775
+ print(
776
+ f" set-valued: {count} declared cell(s) hold a list, compared as "
777
+ "sets -- order is not part of the claim"
778
+ )
779
+
780
+
757
781
  def _parity(
758
782
  args: argparse.Namespace, settings: Settings, target: Path
759
783
  ) -> list[Parity]:
@@ -852,10 +876,12 @@ def cmd_parity(args: argparse.Namespace) -> int:
852
876
  f"# {result.registry}: {result.declared} declared, "
853
877
  f"{result.produced} produced, {held}{also}."
854
878
  )
879
+ _set_valued(result, args)
855
880
  continue
856
881
  print(f"# {result.registry}")
857
882
  for item in shown:
858
883
  print(f" {item}")
884
+ _set_valued(result, args)
859
885
  failed += len(shown)
860
886
 
861
887
  if split.accepted and not args.quiet:
@@ -782,7 +782,7 @@ COUNT_KEYS = frozenset(
782
782
  PARITY_KEYS = frozenset(
783
783
  {
784
784
  "command", "run", "args", "directory", "registry", "extract", "field",
785
- "authority", "translate", "relation",
785
+ "authority", "translate", "translate_identifier", "relation",
786
786
  }
787
787
  )
788
788
  SHAPE_KEYS = frozenset({"registry", "rule"})
@@ -2010,6 +2010,19 @@ def _build_parities(
2010
2010
  "this relation is about. Declare one or the other."
2011
2011
  )
2012
2012
 
2013
+ # Without a field there is one side to translate and `translate`
2014
+ # already reaches it, so a second key naming that side is a declaration
2015
+ # saying the same thing twice -- and refusing beats picking a winner,
2016
+ # since either choice makes the other line inert.
2017
+ if spec.get("translate_identifier") is not None and not value_field:
2018
+ raise ConfigError(
2019
+ f"{path}: [[parity]] {index} declares 'translate_identifier' "
2020
+ "and compares no field, so it already translates its "
2021
+ "identifiers through 'translate'. Use one key: "
2022
+ "'translate_identifier' is for a parity that compares values "
2023
+ "as well, where 'translate' is spent on those."
2024
+ )
2025
+
2013
2026
  argv += _argv(spec.get("args"), f"{path}: [[parity]] {index}", "args")
2014
2027
  built.append(
2015
2028
  Oracle(
@@ -2020,6 +2033,10 @@ def _build_parities(
2020
2033
  field=value_field,
2021
2034
  authority=authority,
2022
2035
  translate=_build_translation(spec.get("translate"), path, index),
2036
+ translate_identifier=_build_translation(
2037
+ spec.get("translate_identifier"), path, index,
2038
+ "translate_identifier",
2039
+ ),
2023
2040
  relation=relation,
2024
2041
  )
2025
2042
  )
@@ -2500,18 +2517,21 @@ def _shape_guard(declared: object, where: str) -> Condition | Predicate:
2500
2517
 
2501
2518
 
2502
2519
  def _build_translation(
2503
- declared: object, path: Path, index: int
2520
+ declared: object, path: Path, index: int, key: str = "translate"
2504
2521
  ) -> Translation | None:
2505
- """The one translation a ``[[parity]]`` may declare, or a refusal.
2522
+ """One translation a ``[[parity]]`` declares, or a refusal.
2506
2523
 
2507
2524
  Compiled here rather than at the comparison, unlike ``extract``: an
2508
2525
  unusable ``extract`` makes a run fail loudly as blocked, while an unusable
2509
2526
  translation would make every comparison in it wrong. A project's mistake
2510
2527
  belongs where a project's mistakes are refused.
2528
+
2529
+ ``key`` names the one being built, so a refusal about
2530
+ ``translate_identifier`` does not point at the other key's spelling.
2511
2531
  """
2512
2532
  if declared is None:
2513
2533
  return None
2514
- where = f"{path}: [[parity]] {index}'s translate"
2534
+ where = f"{path}: [[parity]] {index}'s {key}"
2515
2535
  if not isinstance(declared, dict):
2516
2536
  raise ConfigError(f"{where} must be a table, not a {type(declared).__name__}")
2517
2537
  table, pattern = declared.get("map"), declared.get("pattern")
@@ -151,6 +151,12 @@ class Translation:
151
151
  ``docs/introduction.md`` [0TN7FP9-Pa0004] § Known limits, which carries the
152
152
  measurement that withdrew it and what a wider form would take.
153
153
 
154
+ ⚑ **One per SIDE, since 2026-09-19.** A value comparison spends this on the
155
+ values, so :attr:`Oracle.translate_identifier` is the identifier hop -- see
156
+ that attribute for why it is a capability rather than a convenience. Still
157
+ one translation per side: a pipeline of them is what nobody can read off a
158
+ config.
159
+
154
160
  Two forms, never both, and each is deliberately dull:
155
161
 
156
162
  ``table``
@@ -227,12 +233,24 @@ class Oracle:
227
233
  #: identifiers when :attr:`field` is empty, the values when it is not. One
228
234
  #: target, so what a translation touches is a thing a reader can settle from
229
235
  #: the config rather than infer.
230
- #:
231
- #: ⚑ **The cost of that is a declaration comparing values cannot translate
232
- #: its identifiers at all**, and membership runs regardless -- see
233
- #: ``docs/introduction.md`` [0TN7FP9-Pa0004] § Known limits, where the reach
234
- #: of one translation is measured and dispositioned.
235
236
  translate: Translation | None = None
237
+ #: Applied to the declared **identifiers**, in a run that also compares
238
+ #: values. Without it such a run has no way to spell an identifier hop, and
239
+ #: membership runs regardless -- so a declaration whose keys are spelled one
240
+ #: way and whose values are spelled another could be made to pass only by
241
+ #: having the **oracle** re-key its own output.
242
+ #:
243
+ #: ⚑ **That escape is why this exists rather than being a convenience.**
244
+ #: Measured on an adopter's real manifest: the oracle route turns the run
245
+ #: green and puts the hop somewhere no reader of the config can see it,
246
+ #: which is the exact visibility :class:`Translation` was built to buy.
247
+ #: A capability whose absence is routed around silently is not absent, it
248
+ #: is undeclared.
249
+ #:
250
+ #: Refused alongside a bare :attr:`translate` when :attr:`field` is empty:
251
+ #: there is one side to translate then, and two keys naming it is a
252
+ #: declaration saying the same thing twice.
253
+ translate_identifier: Translation | None = None
236
254
  #: What this declaration claims about the two sets, from :data:`RELATIONS`.
237
255
  #: ``equal`` is the default and is what every parity meant before the key
238
256
  #: existed, so no declaration written without it changes.
@@ -392,6 +410,19 @@ class Parity:
392
410
  #: Read by :meth:`scopes` rather than inferred from :attr:`divergent`, which
393
411
  #: is empty both when nothing was compared and when everything agreed.
394
412
  compared: str = ""
413
+ #: Entries whose declared cell held a **list**, and whose comparison was
414
+ #: therefore a set comparison with the declaration's order discarded.
415
+ #:
416
+ #: **Reported because it is a suppression that reported nothing.** A
417
+ #: declaration pinning ``choices`` against the code's own tuple reads
418
+ #: ``in agreement, agreeing on choices`` while the code's *order* changes
419
+ #: underneath it -- measured against an adopter's real manifest, where an
420
+ #: oracle printing the three access tiers reversed still agreed. The
421
+ #: set rule itself is deliberate and argued in
422
+ #: ``test_a_list_valued_field_is_compared_as_a_set``; what was missing is
423
+ #: any way for a reader of the run to learn that the weaker question was
424
+ #: the one answered. Every other suppression in this package prints a line.
425
+ set_valued: tuple[str, ...] = ()
395
426
 
396
427
  @property
397
428
  def failed(self) -> bool:
@@ -534,9 +565,32 @@ def _rendered(value: object) -> str | None:
534
565
  return str(value) if isinstance(value, _SCALARS) else None
535
566
 
536
567
 
568
+ @dataclass(frozen=True)
569
+ class _DeclaredSide:
570
+ """One entry's declared side, rendered for comparison.
571
+
572
+ **A named field rather than a tuple**, on the same reasoning
573
+ :class:`kinemata.targets.Target` records: :attr:`listed` was added to a
574
+ three-tuple every caller unpacked positionally, and a fourth slot nobody
575
+ can read at the call site is how a fact gets carried and then ignored.
576
+ """
577
+
578
+ #: The values to compare, or ``None`` when the cell cannot be rendered.
579
+ values: frozenset[str] | None
580
+ #: The declaration records nothing here, which is a claim rather than a gap.
581
+ absent: bool
582
+ #: Why it could not be rendered, empty when it could.
583
+ problem: str
584
+ #: The cell held a **list**, so the comparison that follows is a set
585
+ #: comparison and its order was discarded. Carried out of here rather than
586
+ #: re-derived by the caller: the lookup is a path walk, and doing it twice
587
+ #: to learn one fact about the first walk is this package's own subject.
588
+ listed: bool
589
+
590
+
537
591
  def _declared_values(
538
592
  entry: Entry, spec: Oracle, translate: Translation
539
- ) -> tuple[frozenset[str] | None, bool, str]:
593
+ ) -> _DeclaredSide:
540
594
  """One entry's declared side: the values, whether absent, and why not.
541
595
 
542
596
  A container that is not a flat list is **refused** rather than rendered. It
@@ -551,18 +605,21 @@ def _declared_values(
551
605
  # decides membership rather than a value.
552
606
  raw = None if found is MISSING else found
553
607
  if raw is None:
554
- return frozenset(), True, ""
555
- items = list(raw) if isinstance(raw, (list, tuple)) else [raw]
608
+ return _DeclaredSide(frozenset(), True, "", False)
609
+ listed = isinstance(raw, (list, tuple))
610
+ items = list(raw) if listed else [raw]
556
611
  rendered = [_rendered(item) for item in items]
557
612
  if any(item is None for item in rendered):
558
613
  kinds = ", ".join(sorted({type(item).__name__ for item in items}))
559
- return None, False, (
614
+ return _DeclaredSide(None, False, (
560
615
  f"declares {entry.id} with a {spell_path(spec.field)!r} holding {kinds}, "
561
616
  "which "
562
617
  "is not a scalar or a list of them and has no spelling an oracle "
563
618
  "could be expected to print"
564
- )
565
- return frozenset(translate.apply(item) for item in rendered), False, ""
619
+ ), listed)
620
+ return _DeclaredSide(
621
+ frozenset(translate.apply(item) for item in rendered), False, "", listed
622
+ )
566
623
 
567
624
 
568
625
  def compare(
@@ -626,13 +683,15 @@ def compare(
626
683
  printed, why = produced(spec, root, timeout)
627
684
  entries = list(registry.entries())
628
685
  translate = spec.translate or Translation()
629
- # The translation has one target, and which one depends on what is being
630
- # compared: a membership declaration's own side is its identifiers, a value
631
- # declaration's is its values. Applying it to both would be two
632
- # translations wearing one name.
633
- declared = {
634
- entry.id if spec.field else translate.apply(entry.id) for entry in entries
635
- }
686
+ # `translate` keeps its published target -- the identifiers of a membership
687
+ # declaration, the values of a value one -- and `translate_identifier` is
688
+ # the second hop, which only a value run can need: without a field there is
689
+ # one side and `translate` already reaches it. The config refuses the pair
690
+ # in that case rather than silently preferring one.
691
+ identify = spec.translate_identifier or (
692
+ Translation() if spec.field else translate
693
+ )
694
+ declared = {identify.apply(entry.id) for entry in entries}
636
695
  if printed is None:
637
696
  return Parity(
638
697
  registry=spec.registry,
@@ -677,10 +736,19 @@ def compare(
677
736
 
678
737
  values = printed.values or {}
679
738
  divergent: list[Divergence] = []
739
+ set_valued: list[str] = []
680
740
  for entry in entries:
681
- if entry.id not in values:
741
+ # Paired on the identifier as *translated*, which is the vocabulary
742
+ # both sides were compared in above. A finding names that spelling for
743
+ # the same reason: it is the one the oracle printed, so a reader can
744
+ # find it in the run they are holding.
745
+ key = identify.apply(entry.id)
746
+ if key not in values:
682
747
  continue # membership above has already said so
683
- mine, absent, problem = _declared_values(entry, spec, translate)
748
+ side = _declared_values(entry, spec, translate)
749
+ mine, absent, problem = side.values, side.absent, side.problem
750
+ if side.listed:
751
+ set_valued.append(key)
684
752
  if mine is None:
685
753
  # **Membership rides along**, and leaving it out was the defect an
686
754
  # adopter reported: 18 of their 66 rows hold a dict, so one
@@ -697,12 +765,12 @@ def compare(
697
765
  relation=spec.relation,
698
766
  **membership,
699
767
  )
700
- theirs = values[entry.id]
768
+ theirs = values[key]
701
769
  if mine != theirs:
702
770
  divergent.append(
703
771
  Divergence(
704
772
  registry=spec.registry,
705
- identifier=entry.id,
773
+ identifier=key,
706
774
  field=spell_path(spec.field),
707
775
  authority=spec.authority,
708
776
  declared=tuple(sorted(mine)),
@@ -716,6 +784,7 @@ def compare(
716
784
  produced=len(printed.ids),
717
785
  divergent=tuple(divergent),
718
786
  compared=spell_path(spec.field),
787
+ set_valued=tuple(sorted(set_valued)),
719
788
  relation=spec.relation,
720
789
  **membership,
721
790
  )
@@ -21,9 +21,11 @@ rewarded (the edit that omits the flag is shorter).
21
21
  adopter's own suite calls these cases throughout, and it does not promise JSON
22
22
  Schema by increments. There is no expression syntax here, no coercion and no
23
23
  inference: two small tables of dull operators, and one escape to a predicate the
24
- project names. An expression language was refused rather than deferred -- it
25
- would re-derive a solved thing, the core takes no runtime dependency so none
26
- could be borrowed, and the result would be a second worse copy.
24
+ project names. An expression language was declined **on its cost** -- it would
25
+ re-derive a solved thing, the core takes no runtime dependency so none could be
26
+ borrowed, and the result would be a second worse copy. Nothing in this design
27
+ forecloses one: ``docs/introduction.md`` [0TN7FP9-Pa0004] § Known limits carries
28
+ the disposition and what would revive it.
27
29
 
28
30
  **One form throughout: a rule is a guard and a claim.** A rule with no guard
29
31
  judges every entry; a guarded rule judges the group its guard selects. That
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinemata
3
- Version: 0.2.0
3
+ Version: 0.3.0.dev1
4
4
  Summary: One declared place per fact; find the code that re-derives it.
5
5
  Author-email: Jei Blanchard <jjb@eng.ufl.edu>
6
6
  License-Expression: GPL-3.0-only
@@ -309,9 +309,10 @@ that has since been renamed: the claim survives, its provenance does not.
309
309
  size of the exemption list **on every run, including clean ones**. `review` is
310
310
  deliberately unaffected — the ratchet governs the gate, not the advice.
311
311
 
312
- **One list, three gates.** `claims` and `undeclared` read the same baseline, so a
313
- documentation tree or a closed registry that is already failing can be adopted
314
- the same way. A second exemption list was the alternative and is the failure
312
+ **One list, several gates**, and the number is deliberately not given here — it
313
+ said *three* and was overtaken twice. Every gate that ratchets reads the same
314
+ baseline, so a documentation tree or a closed registry that is already failing
315
+ can be adopted the same way; `kinemata baseline` prints the scopes. A second exemption list was the alternative and is the failure
315
316
  mode: two lists eventually disagree about what a project accepted, and the one
316
317
  nobody reads is the one still exempting something real. Records carry the check
317
318
  that produced them, so no gate reports another's as fixed — it names them
@@ -516,7 +517,7 @@ removal, visible in a diff.
516
517
  by hand can at least declare that the instruction to run them still exists. That
517
518
  is a reminder about a reminder, and worth what it sounds like.
518
519
 
519
- The companion guard is a number with an oracle. This suite is **1101 tests**, and
520
+ The companion guard is a number with an oracle. This suite is **1107 tests**, and
520
521
  `kinemata claims` settles that figure against `pytest --collect-only`, so a
521
522
  suite that silently shrinks fails the gate rather than passing faster.
522
523
 
@@ -360,6 +360,133 @@ def test_a_list_valued_field_is_compared_as_a_set(tmp_path):
360
360
  assert not result.failed
361
361
 
362
362
 
363
+ def test_a_value_parity_can_translate_its_identifiers_as_well(tmp_path):
364
+ """The second hop, and the reason it is a capability rather than a nicety.
365
+
366
+ A declaration whose keys are spelled one way and whose values are spelled
367
+ another needs a hop on each side of the seam. With one translation, the
368
+ side it lands on is decided by ``field`` -- so a value run left identifiers
369
+ untranslated and reported both as membership findings, and the only way to
370
+ green it was to have the *oracle* re-key its own output, putting the hop
371
+ where no reader of the config can see it. Measured on an adopter's real
372
+ manifest before this existed.
373
+ """
374
+ result = compare(
375
+ Registry("keyspace.old", extra={"keyspace.old": {"default": "THEME"}}),
376
+ value_oracle(
377
+ command=("{python}", "-c", "print('keyspace.new=theme')"),
378
+ translate=Translation(pattern="^THEME$", replacement="theme"),
379
+ translate_identifier=Translation(pattern=r"\.old$", replacement=".new"),
380
+ ),
381
+ tmp_path,
382
+ )
383
+ assert not result.failed
384
+ assert result.undeclared == () and result.unproduced == ()
385
+
386
+
387
+ def test_a_divergence_names_the_identifier_as_translated(tmp_path):
388
+ """The spelling the oracle printed, which is the run the reader is holding.
389
+
390
+ Membership is compared in the translated vocabulary, so a value finding
391
+ naming the untranslated one would make a single run speak two languages.
392
+ """
393
+ result = compare(
394
+ Registry("keyspace.old", extra={"keyspace.old": {"default": "THEME"}}),
395
+ value_oracle(
396
+ command=("{python}", "-c", "print('keyspace.new=other')"),
397
+ translate_identifier=Translation(pattern=r"\.old$", replacement=".new"),
398
+ ),
399
+ tmp_path,
400
+ )
401
+ (found,) = result.divergent
402
+ assert found.identifier == "keyspace.new"
403
+
404
+
405
+ def test_translate_identifier_without_a_field_is_refused(tmp_path):
406
+ """Two keys naming one side is a declaration saying the same thing twice.
407
+
408
+ Without a field there is only the identifier side, and `translate` already
409
+ reaches it -- so this is refused rather than resolved by preferring one,
410
+ which would leave the other line inert in a config that reads as if both
411
+ applied.
412
+ """
413
+ declare(tmp_path, extra=(
414
+ 'translate_identifier = { pattern = "^a$", replacement = "b" }'
415
+ ))
416
+ with pytest.raises(ConfigError) as caught:
417
+ load(Path(cfg(tmp_path)))
418
+ assert "already translates its identifiers" in str(caught.value)
419
+
420
+
421
+ def test_a_list_valued_field_says_it_was_compared_as_a_set(tmp_path):
422
+ """The rule above is deliberate; reporting nothing about it was not.
423
+
424
+ Measured against an adopter's real manifest, a declaration pinning an enum's
425
+ ``choices`` against the code's own tuple printed *in agreement, agreeing on
426
+ choices* while the oracle listed the three tiers in **reverse** -- and
427
+ nothing in the run said the order half had gone unchecked. Every other
428
+ suppression in this package prints a line; this was the one that did not,
429
+ which is the same defect ``claims`` carried until a withdrawn path claim
430
+ gained ``negated:``.
431
+ """
432
+ result = compare(
433
+ Registry("app.name", extra={"app.name": {"default": ["b", "a"]}}),
434
+ value_oracle(
435
+ command=("{python}", "-c",
436
+ "print('app.name=a'); print('app.name=b')"),
437
+ ),
438
+ tmp_path,
439
+ )
440
+ assert not result.failed
441
+ assert result.set_valued == ("app.name",)
442
+
443
+
444
+ def test_a_scalar_field_is_not_called_set_valued(tmp_path):
445
+ """The disclosure is about lists, not about every value comparison.
446
+
447
+ A line on every run is a line nobody reads, and the fact being disclosed --
448
+ that an order was discarded -- is not true of a scalar.
449
+ """
450
+ result = compare(
451
+ Registry("app.name", extra={"app.name": {"default": "truecolor"}}),
452
+ value_oracle(),
453
+ tmp_path,
454
+ )
455
+ assert not result.failed
456
+ assert result.set_valued == ()
457
+
458
+
459
+ def test_the_command_discloses_a_set_comparison_on_a_CLEAN_run(tmp_path, capsys):
460
+ """The clean run is the whole point: a failing one is already being read."""
461
+ write(tmp_path, "keys.yaml",
462
+ 'keys:\n app.name:\n spec: "§1"\n default:\n - a\n - b\n')
463
+ write(tmp_path, "src/a.py", 'NAME = "app.name"\n')
464
+ write(tmp_path, "kinemata.toml", """
465
+ [project]
466
+ root = "."
467
+
468
+ [[registry]]
469
+ name = "keyspace"
470
+ kind = "yaml-mapping"
471
+ source = "keys.yaml"
472
+ section = "keys"
473
+ clause_field = "spec"
474
+ syntax = '\\bapp\\.[a-z_]+'
475
+
476
+ [[parity]]
477
+ registry = "keyspace"
478
+ command = ["{python}", "-c", "print('app.name=b'); print('app.name=a')"]
479
+ extract = '(?m)^(\\S+)=(.*)$'
480
+ field = "default"
481
+ authority = "declared"
482
+ """)
483
+ assert main(["parity", "-c", cfg(tmp_path)]) == 0
484
+ out = capsys.readouterr().out
485
+ assert "in agreement, agreeing on default" in out
486
+ assert "set-valued: 1" in out
487
+ assert "order is not part of the claim" in out
488
+
489
+
363
490
  def test_a_set_valued_divergence_names_the_side_each_difference_is_on(tmp_path):
364
491
  result = compare(
365
492
  Registry("app.name", extra={"app.name": {"default": ["a", "gone"]}}),
File without changes
File without changes
File without changes
File without changes
File without changes