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.
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/CHANGELOG.md +25 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/PKG-INFO +6 -5
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/README.md +5 -4
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/design.md +4 -2
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/introduction.md +85 -9
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/pyproject.toml +1 -1
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/claims.py +10 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/cli.py +26 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/config.py +24 -4
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/parity.py +91 -22
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/shape.py +5 -3
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/PKG-INFO +6 -5
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_parity.py +127 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/AIPOLICY.md +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/LICENSE.md +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/MANIFEST.in +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/american-english.toml +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/bibliography.toml +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/citations.md +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/resources.toml +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/docs/structure.md +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/examples/ci-github-actions.yml +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/kinemata.toml +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/setup.cfg +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/__init__.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/access.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/__init__.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/bibliography.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/constants.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/mapping.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/patterns.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/substitutions.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/adapters/values.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/baseline.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/bypass.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/citations.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/confirm.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/context.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/contract.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/exclusion.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/gates.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/interpose.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/literals.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/probe.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/projection.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/prose.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/provenance.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/pytest_plugin.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/report.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/resources.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/stamps.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata/targets.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/SOURCES.txt +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/dependency_links.txt +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/entry_points.txt +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/requires.txt +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/src/kinemata.egg-info/top_level.txt +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_access.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_against_example.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_baseline.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_bibliography.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_bypass.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_catch_a_ratchet.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_claims.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_claims_declared.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_claims_ratchet.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_config_keys.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_confirm.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_context.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_contract.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_corpus_validation.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_delivery.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_docstring_claims.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_exclusion.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_gates.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_import_kind.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_interpose.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_literals.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_packaging.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_probe.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_prose.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_provenance.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_registry_where.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_resources.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_section_path.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_shape.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_stamps.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_substitutions.py +0 -0
- {kinemata-0.2.0 → kinemata-0.3.0.dev1}/tests/test_targets.py +0 -0
- {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.
|
|
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,
|
|
313
|
-
|
|
314
|
-
|
|
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 **
|
|
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,
|
|
289
|
-
|
|
290
|
-
|
|
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 **
|
|
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
|
|
384
|
-
|
|
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
|
-
#
|
|
257
|
-
#
|
|
258
|
-
#
|
|
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
|
-
⚑
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
one
|
|
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.
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
630
|
-
#
|
|
631
|
-
#
|
|
632
|
-
#
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
768
|
+
theirs = values[key]
|
|
701
769
|
if mine != theirs:
|
|
702
770
|
divergent.append(
|
|
703
771
|
Divergence(
|
|
704
772
|
registry=spec.registry,
|
|
705
|
-
identifier=
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
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,
|
|
313
|
-
|
|
314
|
-
|
|
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 **
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|