elenctic 1.0.0__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 (134) hide show
  1. elenctic-1.0.0/.dockerignore +15 -0
  2. elenctic-1.0.0/.github/workflows/ci.yml +42 -0
  3. elenctic-1.0.0/.github/workflows/release.yml +66 -0
  4. elenctic-1.0.0/.gitignore +10 -0
  5. elenctic-1.0.0/CHANGELOG.md +193 -0
  6. elenctic-1.0.0/Containerfile +24 -0
  7. elenctic-1.0.0/LICENSE +21 -0
  8. elenctic-1.0.0/PKG-INFO +165 -0
  9. elenctic-1.0.0/README.md +148 -0
  10. elenctic-1.0.0/check.sh +5 -0
  11. elenctic-1.0.0/docs/examples/clingcon.lp +11 -0
  12. elenctic-1.0.0/docs/examples/consequences.lp +6 -0
  13. elenctic-1.0.0/docs/examples/hidden-contrary.lp +6 -0
  14. elenctic-1.0.0/docs/examples/no-objective.lp +5 -0
  15. elenctic-1.0.0/docs/examples/optimum-wrong.lp +8 -0
  16. elenctic-1.0.0/docs/examples/optimum.lp +11 -0
  17. elenctic-1.0.0/docs/examples/theory-atoms.lp +4 -0
  18. elenctic-1.0.0/docs/examples/why-not.lp +4 -0
  19. elenctic-1.0.0/docs/explanation/semantics.md +63 -0
  20. elenctic-1.0.0/docs/how-to/guides.md +75 -0
  21. elenctic-1.0.0/docs/index.md +8 -0
  22. elenctic-1.0.0/docs/reference/examples.md +213 -0
  23. elenctic-1.0.0/docs/reference/output-v1.schema.json +749 -0
  24. elenctic-1.0.0/docs/reference/reference.md +334 -0
  25. elenctic-1.0.0/docs/tutorial/first-contract.md +33 -0
  26. elenctic-1.0.0/pixi.lock +4846 -0
  27. elenctic-1.0.0/pyproject.toml +189 -0
  28. elenctic-1.0.0/recipe/recipe.yaml +50 -0
  29. elenctic-1.0.0/scripts/capture_examples.py +45 -0
  30. elenctic-1.0.0/scripts/differential.py +159 -0
  31. elenctic-1.0.0/scripts/rename.sh +14 -0
  32. elenctic-1.0.0/scripts/write_output_schema.py +18 -0
  33. elenctic-1.0.0/src/elenctic/__init__.py +169 -0
  34. elenctic-1.0.0/src/elenctic/__main__.py +8 -0
  35. elenctic-1.0.0/src/elenctic/about.py +53 -0
  36. elenctic-1.0.0/src/elenctic/body.py +70 -0
  37. elenctic-1.0.0/src/elenctic/braces.py +37 -0
  38. elenctic-1.0.0/src/elenctic/by_answer_sets.py +144 -0
  39. elenctic-1.0.0/src/elenctic/check.py +220 -0
  40. elenctic-1.0.0/src/elenctic/classes.py +217 -0
  41. elenctic-1.0.0/src/elenctic/cli.py +419 -0
  42. elenctic-1.0.0/src/elenctic/contract.py +130 -0
  43. elenctic-1.0.0/src/elenctic/determination.py +213 -0
  44. elenctic-1.0.0/src/elenctic/directives.py +72 -0
  45. elenctic-1.0.0/src/elenctic/display.py +22 -0
  46. elenctic-1.0.0/src/elenctic/engine.py +255 -0
  47. elenctic-1.0.0/src/elenctic/extension.py +34 -0
  48. elenctic-1.0.0/src/elenctic/facts.py +165 -0
  49. elenctic-1.0.0/src/elenctic/gates.py +141 -0
  50. elenctic-1.0.0/src/elenctic/glyphs.py +41 -0
  51. elenctic-1.0.0/src/elenctic/grounded.py +259 -0
  52. elenctic-1.0.0/src/elenctic/human.py +309 -0
  53. elenctic-1.0.0/src/elenctic/json_view.py +212 -0
  54. elenctic-1.0.0/src/elenctic/judge.py +101 -0
  55. elenctic-1.0.0/src/elenctic/literals.py +85 -0
  56. elenctic-1.0.0/src/elenctic/normalize.py +102 -0
  57. elenctic-1.0.0/src/elenctic/operands.py +123 -0
  58. elenctic-1.0.0/src/elenctic/paint.py +31 -0
  59. elenctic-1.0.0/src/elenctic/partition.py +42 -0
  60. elenctic-1.0.0/src/elenctic/patterns.py +72 -0
  61. elenctic-1.0.0/src/elenctic/presentation.py +99 -0
  62. elenctic-1.0.0/src/elenctic/program.py +38 -0
  63. elenctic-1.0.0/src/elenctic/py.typed +0 -0
  64. elenctic-1.0.0/src/elenctic/queries.py +113 -0
  65. elenctic-1.0.0/src/elenctic/query.py +157 -0
  66. elenctic-1.0.0/src/elenctic/reader.py +94 -0
  67. elenctic-1.0.0/src/elenctic/readings.py +107 -0
  68. elenctic-1.0.0/src/elenctic/relation.py +62 -0
  69. elenctic-1.0.0/src/elenctic/report.py +64 -0
  70. elenctic-1.0.0/src/elenctic/rows.py +54 -0
  71. elenctic-1.0.0/src/elenctic/search.py +279 -0
  72. elenctic-1.0.0/src/elenctic/spans.py +277 -0
  73. elenctic-1.0.0/src/elenctic/substitution.py +32 -0
  74. elenctic-1.0.0/src/elenctic/syntax.py +118 -0
  75. elenctic-1.0.0/src/elenctic/tags.py +147 -0
  76. elenctic-1.0.0/src/elenctic/terms.py +162 -0
  77. elenctic-1.0.0/src/elenctic/theory.py +115 -0
  78. elenctic-1.0.0/src/elenctic/traversal.py +28 -0
  79. elenctic-1.0.0/src/elenctic/tree.py +89 -0
  80. elenctic-1.0.0/src/elenctic/unify.py +93 -0
  81. elenctic-1.0.0/src/elenctic/verdicts.py +68 -0
  82. elenctic-1.0.0/src/elenctic/wellformed.py +115 -0
  83. elenctic-1.0.0/src/elenctic/writer.py +87 -0
  84. elenctic-1.0.0/tests/conftest.py +10 -0
  85. elenctic-1.0.0/tests/corpus/cowardly.lp +53 -0
  86. elenctic-1.0.0/tests/corpus/orphans.lp +64 -0
  87. elenctic-1.0.0/tests/corpus/tweety.lp +82 -0
  88. elenctic-1.0.0/tests/corpus/uncaring.lp +67 -0
  89. elenctic-1.0.0/tests/interrupting.py +20 -0
  90. elenctic-1.0.0/tests/nesting.py +8 -0
  91. elenctic-1.0.0/tests/reference.py +83 -0
  92. elenctic-1.0.0/tests/runs.py +19 -0
  93. elenctic-1.0.0/tests/test_about.py +39 -0
  94. elenctic-1.0.0/tests/test_by_answer_sets.py +68 -0
  95. elenctic-1.0.0/tests/test_check.py +612 -0
  96. elenctic-1.0.0/tests/test_cli.py +692 -0
  97. elenctic-1.0.0/tests/test_cli_output.py +363 -0
  98. elenctic-1.0.0/tests/test_cli_version.py +250 -0
  99. elenctic-1.0.0/tests/test_clingcon.py +179 -0
  100. elenctic-1.0.0/tests/test_consistency.py +100 -0
  101. elenctic-1.0.0/tests/test_corpus.py +16 -0
  102. elenctic-1.0.0/tests/test_determination.py +60 -0
  103. elenctic-1.0.0/tests/test_diagnostic_wording.py +193 -0
  104. elenctic-1.0.0/tests/test_differential.py +80 -0
  105. elenctic-1.0.0/tests/test_display.py +22 -0
  106. elenctic-1.0.0/tests/test_engine.py +610 -0
  107. elenctic-1.0.0/tests/test_examples.py +31 -0
  108. elenctic-1.0.0/tests/test_extension.py +25 -0
  109. elenctic-1.0.0/tests/test_facts.py +45 -0
  110. elenctic-1.0.0/tests/test_glyphs.py +49 -0
  111. elenctic-1.0.0/tests/test_human.py +333 -0
  112. elenctic-1.0.0/tests/test_json_view.py +146 -0
  113. elenctic-1.0.0/tests/test_lexable.py +128 -0
  114. elenctic-1.0.0/tests/test_literals.py +71 -0
  115. elenctic-1.0.0/tests/test_normalize.py +91 -0
  116. elenctic-1.0.0/tests/test_paint.py +41 -0
  117. elenctic-1.0.0/tests/test_partition.py +41 -0
  118. elenctic-1.0.0/tests/test_presentation.py +186 -0
  119. elenctic-1.0.0/tests/test_public.py +150 -0
  120. elenctic-1.0.0/tests/test_query.py +271 -0
  121. elenctic-1.0.0/tests/test_reader.py +511 -0
  122. elenctic-1.0.0/tests/test_relation.py +41 -0
  123. elenctic-1.0.0/tests/test_report.py +36 -0
  124. elenctic-1.0.0/tests/test_source_boundaries.py +140 -0
  125. elenctic-1.0.0/tests/test_spans.py +81 -0
  126. elenctic-1.0.0/tests/test_syntax.py +225 -0
  127. elenctic-1.0.0/tests/test_tags.py +35 -0
  128. elenctic-1.0.0/tests/test_terms.py +126 -0
  129. elenctic-1.0.0/tests/test_theory.py +84 -0
  130. elenctic-1.0.0/tests/test_traversal.py +35 -0
  131. elenctic-1.0.0/tests/test_tree.py +49 -0
  132. elenctic-1.0.0/tests/test_unify.py +315 -0
  133. elenctic-1.0.0/tests/test_world_view_defaults.py +36 -0
  134. elenctic-1.0.0/uv.lock +684 -0
@@ -0,0 +1,15 @@
1
+ .venv
2
+ .pixi
3
+ .git
4
+ .pytest_cache
5
+ .mypy_cache
6
+ .ruff_cache
7
+ __pycache__
8
+ *.pyc
9
+ .coverage
10
+ htmlcov
11
+ dist
12
+ build
13
+ *.egg-info
14
+ /tmp
15
+ scratch
@@ -0,0 +1,42 @@
1
+ name: ci
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ concurrency:
7
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
8
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
9
+ jobs:
10
+ # A secondary check that the package installs and its tests pass under uv, on Python 3.14 and 3.15,
11
+ # from the tracked uv.lock. The gate itself is the pixi job below.
12
+ uv-install:
13
+ runs-on: ${{ matrix.os }}
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ os: [ubuntu-latest, macos-latest]
18
+ python: ["3.14", "3.15"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: astral-sh/setup-uv@v6
22
+ - run: uv sync --locked --python ${{ matrix.python }} --extra theory
23
+ - run: uv run pytest
24
+ pixi:
25
+ # The gate: format, lint, strict types, import contracts, tests.
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: prefix-dev/setup-pixi@v0.8.10
30
+ - run: pixi run -e theory check
31
+ recipe:
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ - uses: prefix-dev/setup-pixi@v0.8.10
36
+ - run: pixi global install rattler-build
37
+ - name: Build the recipe from this checkout
38
+ run: |
39
+ mkdir -p "$RUNNER_TEMP/recipe"
40
+ sed "s| url: .*| path: $GITHUB_WORKSPACE|; / sha256:/d" \
41
+ recipe/recipe.yaml > "$RUNNER_TEMP/recipe/recipe.yaml"
42
+ rattler-build build --recipe "$RUNNER_TEMP/recipe/recipe.yaml"
@@ -0,0 +1,66 @@
1
+ name: release
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+ permissions:
6
+ contents: read
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
12
+ with:
13
+ persist-credentials: false
14
+ - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
15
+ with:
16
+ python-version: "3.14"
17
+ - run: uv sync --locked --no-dev
18
+ - name: Check the tag and schema
19
+ env:
20
+ RELEASE_TAG: ${{ github.ref_name }}
21
+ run: |
22
+ uv run --locked --no-dev python - <<'PY'
23
+ import os
24
+ import runpy
25
+ import tomllib
26
+ from pathlib import Path
27
+
28
+ version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
29
+ if os.environ["RELEASE_TAG"] != f"v{version}":
30
+ raise SystemExit("tag must match the package version")
31
+ schema = runpy.run_path("scripts/write_output_schema.py")
32
+ if schema["TARGET"].read_text() != schema["render"]():
33
+ raise SystemExit("regenerate the output schema")
34
+ PY
35
+ - run: uv build --python 3.14
36
+ - name: Check the shipped schema
37
+ run: |
38
+ uv run --locked --no-dev python - <<'PYTHON'
39
+ from pathlib import Path
40
+ from zipfile import ZipFile
41
+
42
+ with ZipFile(next(Path("dist").glob("*.whl"))) as wheel:
43
+ if wheel.read("elenctic/output-v1.schema.json") != Path(
44
+ "docs/reference/output-v1.schema.json"
45
+ ).read_bytes():
46
+ raise SystemExit("wheel must ship the output schema")
47
+ PYTHON
48
+ - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
49
+ with:
50
+ name: distributions
51
+ path: dist/
52
+ if-no-files-found: error
53
+ publish:
54
+ needs: build
55
+ runs-on: ubuntu-latest
56
+ environment:
57
+ name: pypi
58
+ url: https://pypi.org/p/elenctic
59
+ permissions:
60
+ id-token: write
61
+ steps:
62
+ - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
63
+ with:
64
+ name: distributions
65
+ path: dist/
66
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ .mypy_cache/
4
+ .ruff_cache/
5
+ .pytest_cache/
6
+ .hypothesis/
7
+ .coverage
8
+ dist/
9
+ *.egg-info/
10
+ .pixi/
@@ -0,0 +1,193 @@
1
+ # Changelog
2
+
3
+ All notable changes are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project follows
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [1.0.0] - 2026-11-06
8
+
9
+ ### Added
10
+ - `--root DIR`: one root for every path and `#include` of a run, so a subtree whose cases include
11
+ files from a sibling directory can be tested on its own.
12
+ - `Containerfile`: a local multi-stage recipe for the CLI (uv sync from the lockfile with the
13
+ theory extra). Build it yourself; there is no published image and no CI image build.
14
+ - Pixi environments `py314` and `py315` for the test suite under those Pythons (`pixi run -e py314 test`).
15
+ - JSON Schema for the `--json` document: `docs/reference/output-v1.schema.json`, generated from the
16
+ result types `encode` walks (`elenctic.json_view.schema_document`); regenerate with
17
+ `scripts/write_output_schema.py`. A test fails if the committed file is stale.
18
+ - `python -m elenctic` runs the same CLI as the `elenctic` console script (`src/elenctic/__main__.py`).
19
+ - Import contracts (import-linter, in `pyproject.toml`): the layering, clingo reached only by the fifteen
20
+ modules that read and run programs, clingcon only by `theory`, and `spans` free of both. They run
21
+ in `pixi run check` and in CI.
22
+ - Library: `Program`, `ground`, `solve` with `Limits` and a cancelable `Interrupt`; results as
23
+ `Consistent | Inconsistent | Inconclusive` over `AnswerSets` or `Consequences`.
24
+ - Query kernel in the public API: `Literal`, `Pattern`, `Query` (`Literal | Pattern | All | Any`), `Answer`,
25
+ `answer`, `bindings`, over a `WorldView` that only an exhausted search yields.
26
+ - A row for every name of `elenctic.__all__` in `docs/reference/reference.md`, checked by a test.
27
+ - `mgu(first, second)` and `Unifier`: the most general unifier of two patterns, renamed apart, and
28
+ the pattern both become. Public for answering queries and for the interactive session to come.
29
+ - `unify(first, second)`: the same unifier for two patterns that share their variables by name, as a
30
+ `Substitution`. It is what query answering calls; against a ground pattern it agrees with `mgu`.
31
+ - Contracts read from comments with source spans: `@expect`, `@count`, `@answer-set`, `@cautious`,
32
+ `@brave`, `@query` with optional `= { bindings }`, and an `assuming { … }` form on any claim.
33
+ - `check` with the verdicts `pass`, `fail` and `undecided`.
34
+ - Command line with `--json`, `--verbose`, `--max-seconds` and `--max-answer-sets`; exit codes `CLAIMS_HOLD` (0), `CLAIM_UNMET` (1) and
35
+ `FAULT` (2), and `INTERNAL_ERROR` (3) for a failure of elenctic itself, reported as one line on
36
+ stderr. They are the members of `elenctic.report.Exit`, an `IntEnum`, the outcome of `test`; `version` has its
37
+ own, `elenctic.about.VersionExit` (`REPORTED` 0, `INTERNAL_ERROR` 3), so no member is shared.
38
+ - Native cautious and brave consequences (`Grounded.consequences`), used by `check` when a search
39
+ holds only such claims.
40
+ - Minimal unsatisfiable cores over assumptions: `Refutation` is `These | NotThese | NoAssumptions`.
41
+ - Optimization: `Grounded.optimize` returns `Optimum` or `Incumbent`; claims `@answer-set optimal`,
42
+ `@cautious optimal`, `@brave optimal`, `@optimal { L }`, `@count optimal n` and `@cost { … }`.
43
+ - `@query optimal …`: the same three-valued query path over Opt(P); unqualified `@query` still reads
44
+ AS(P).
45
+ - Preconditions that make a misreadable contract an error: theory atoms, an objective that grounds
46
+ away, a nested `@count optimal`.
47
+ - `Report.lints`: notes when a query reads a literal the program can derive and does not `#show`.
48
+ - Subcommands: `elenctic test` (the default) and the reserved `repl`, `why`, `ask`, `explain`.
49
+ - `elenctic version`, `--version` and `-V`: one text with the version, the license, the holder and the
50
+ versions of clingo and clingcon that are loaded. `--version` and `-V` are options of `elenctic`
51
+ itself: valid before the command and winning over the rest of the line, not taken by a command.
52
+ - Every type that a public signature names is exported, so a signature can be read and a value built
53
+ from `elenctic` alone: the readings `Satisfiable`, `Census`, `Witness`, `Entails`, `Asks`, `Binds`
54
+ and their union `Reading`, the query parts `All` and `Any`, `Shown`, `Found` and `Cut`. A test walks
55
+ the signatures of `__all__` and fails on a type left out. `elenctic.Any` is the disjunction of
56
+ queries, not `typing.Any`.
57
+ - Examples with real output in `docs/reference/examples.md`, checked by a test.
58
+ - Packaging for uv, pixi and conda-forge (`recipe/recipe.yaml`).
59
+ - clingcon: `% @elenctic solver clingcon` in a contract, claims `@assign [optimal] { x=1, … }` and
60
+ `@answer-set … where { x=1, … }`, the library types `Solver`, `Assignment`, `Valuation` and
61
+ `AnswerSet.assignment`. An answer under clingcon is a shown set with its assignment, and `@count`
62
+ counts those. Optional: `elenctic[theory]`, or the pixi `theory` environment; a contract that
63
+ declares clingcon without it is an error (`FAULT`, 2).
64
+
65
+ - `--color auto|always|never` (default `auto`) and `--icons[=WHEN]` (Nerd Font glyphs, off unless asked),
66
+ each decided per stream, and valid before the command as well as after it. `auto` follows
67
+ `NO_COLOR`, `TERM=dumb`, then whether the stream is a terminal, so a redirected stream stays plain; only `--color always` colors it. `CLICOLOR_FORCE`,
68
+ `CLICOLOR` and `FORCE_COLOR` are not read. This governs elenctic's own report and diagnostics;
69
+ the help and usage messages are colored as Python's `argparse` does by default. See the output
70
+ section of the reference.
71
+
72
+ ### Fixed
73
+ - Docs: JSON `exit_code` includes 3; guides state `world_view` on a truncation; `@expect`-only searches stop once satisfiability is decided.
74
+ - Guides: an interrupted `check` yields undecided findings; `solve` yields `Inconclusive`.
75
+ - An `#include` that resolves outside the root is refused before the case is run; the diagnostic names the escaping path and never its contents.
76
+ - Non-ASCII outside strings and comments is refused with a located diagnostic before clingo sees
77
+ it (program text, `#include`d files, and tag payloads), so a bad character never aborts the
78
+ process; `--json` still emits a document.
79
+ - Binding sets keep tuple structure: `{ (1,2) }` for one variable is one binding, and `{ ((1,2),3) }` for two is accepted; a bare `{ a, 1 }` for arity 2 is refused.
80
+ - A named `.lp` with no contract tag is a fault (exit 2), not a silent `0/0 passed`; a UTF-8 BOM is refused with a located diagnostic; a walked library whose `@tag` looks misspelled is a note.
81
+ - Search groups that read only shown identities project onto `#show`, so `@count` and `@answer-set` no longer enumerate hidden variation; optimal groups prove the cost first, then enumerate at that bound; witness-only groups stop once every claim is decided.
82
+
83
+
84
+ - Undecided truncated claims carry an `Ended` detail in JSON; `world_view` and `optimal_view` range over full answer sets by default; the hidden-read lint covers a query's contrary; `bindings` takes a single `Pattern`.
85
+
86
+ - JSON entries carry `verdict` and `solver`, omit program `text`; diagnostic `label`/`children` are optional in the schema; `$id` points at the raw file under `docs/reference/`; the schema ships in the wheel.
87
+
88
+ - An unexpected failure while checking one source is recorded for that source and the rest of the corpus still runs; the run exits 3.
89
+
90
+ - Satisfiability-only groups keep no answer-set symbols; signatures come from clingo; atom→literal is looked up on demand.
91
+
92
+ - A refutation core shows `not L` for an assumption that does not hold.
93
+ - A one-element tuple keeps its trailing comma when rendered.
94
+ - A query that is empty or uses `not` is refused as such; a variable query's help cites the user's pattern.
95
+ - `@count optimal 0` under `@expect sat` names `optimal` in the contradiction.
96
+ - Empty braces render as `{ }`; `@count` accepts only ASCII digits.
97
+ - At most one `@count` per scenario and range.
98
+ - `@expect` alone is admitted over a theory objective.
99
+
100
+ - A second SIGINT restores the default handler so the process can exit 130.
101
+
102
+ ### Changed
103
+ - The include refusal says `outside the root DIR` and has a help line.
104
+ - An unknown `@model` is told the tag is spelled `@answer-set`; a walked library or a named file
105
+ whose tags are not tags says so (`@model` is not a tag) with the same help as an unknown tag.
106
+ - A missing clingcon's help no longer names a pixi environment.
107
+ - Consistency across docs, help and internals: truncation prose; reserved-command helps as imperatives; `--help` says "answer set programs"; `(default: …)` in the reference; "source" for a checked unit; stderr prefix rule; `render_run`, `holds_flag`, `Classes.merge`; CLI writes with `write` only; American-spelling and reading-name notes in the README; mechanical checks in `tests/test_consistency.py`.
108
+ - Diagnostics follow the rustc model by default (`--format full`): `error: message`, ` --> path:line:col`, a source snippet with carets and a primary label, then `= note:` / `= help:` lines. `--format short` keeps today's one-line gcc form; `--json` is an alias of `--format json`. Messages are lowercase with no trailing period; fix-it wording stands only on `help`. Clingo's byte columns are converted to Unicode scalar columns so carets land. The JSON schema keeps `SCHEMA = 1` and adds optional `label`, `children`, and `code` on `diagnostic` (no codes in 1.0).
109
+ - CI cancels superseded pull-request runs (`concurrency` with `cancel-in-progress` on pull requests only).
110
+ - `uv.lock` pins clingo 5.8.3 and clingcon 5.2.2 so Python 3.15 installs from wheels.
111
+ - The bindings of a query pattern are its most general unifiers with the cautious consequences of
112
+ the full answer sets: yes where it unifies with one, no where its contrary does. The brave
113
+ consequences give only the unknown instances (entertained, neither yes nor no). A ground literal
114
+ is yes when cautious and no when its contrary is. A conjunction or disjunction is still answered
115
+ over each answer set. The answers are unchanged; how they are found is not.
116
+ - Limits are named for what they bound: `--max-seconds` (was `--budget`) and `--max-answer-sets` (was
117
+ `--models`), `Limits.max_seconds` and `Limits.max_answer_sets`, `DEFAULT_MAX_SECONDS` and
118
+ `DEFAULT_MAX_ANSWER_SETS`, and the truncations `MAX_SECONDS` and `MAX_ANSWER_SETS`. `UNLIMITED` is gone: use `Limits()`.
119
+ - One answer set is an answer set everywhere: the tag `@answer-set` (was `@model`), the types `AnswerSet` (was
120
+ `Model`) and `AnswerSets` (was `Models`), `Atoms` (was `AnswerSet`), `Optimum.answer_sets`,
121
+ `Incumbent.answer_set`, `Seen.answer_set` and `search.answer_sets()`, and the same keys in the JSON.
122
+ The old names are gone.
123
+ - Every enum value in the JSON is snake_case, and each is its member's name in lower case, so the
124
+ truncations are `max_answer_sets`, `max_seconds` and `interrupted`; the command-line flags keep their
125
+ hyphens.
126
+ - The `kind` of every JSON object is its type's name in snake case (`report`, `entails`, `answer_set`), derived in
127
+ one place, `json_view.kind_of`.
128
+ - A search cut short is worded for people by the text view: `search ended: answer-set limit reached
129
+ (--max-answer-sets)`, `time limit reached (--max-seconds)` or `interrupted`, where the text said
130
+ `max_answer_sets`. A finding's `detail` is text or `Ended` (the `Truncation`, and whether an optimum went
131
+ unproven), which is an object in the JSON; `Detail` and `Ended` are exported. The option names come from
132
+ `determination.option`, which the command line uses too.
133
+
134
+ - Diagnostics for a source that could not be checked go to stderr, with the reserved-command note
135
+ and the internal-error line; verdicts and the summary stay on stdout. `--json` is all on stdout.
136
+ - The terminal text is `elenctic.human`, the JSON is `elenctic.json_view` (`encode`, moved from
137
+ `elenctic.report`, which holds the run's values); escape sequences are written by `elenctic.paint`
138
+ alone.
139
+ - `pixi run -e theory check` is the one documented gate; `check.sh` only runs it. `uv.lock` is tracked.
140
+ The CI uv job is a secondary install check (`uv sync --locked`, then the tests).
141
+ - `--max-seconds` and `--max-answer-sets` take a positive number or the word `all` (no limit); `--max-answer-sets` defaults to
142
+ 100,000 (`DEFAULT_MAX_ANSWER_SETS`), so a search that would read more is `undecided`; `--max-answer-sets all` lifts
143
+ it. The library default stays `Limit.UNBOUNDED`. `0` and negative values are a usage error: `--max-seconds 0` used to mean no limit and `--max-answer-sets 0`
144
+ an immediate cut. `Limits.max_seconds` and `Limits.max_answer_sets` are a number or `Limit.UNBOUNDED`, replacing
145
+ `None`.
146
+ - A refutation by assumptions whose core may not be minimal (the search was cut short) now says so.
147
+ - `@cost` over `#maximize` is accepted: costs are clingo's minimized sums, so a maximized level is
148
+ negated.
149
+ - A query that reads a literal the program can derive and does not `#show` is noted (a lint on the
150
+ report), not refused; the note does not change the exit. The old refusal of such queries, and the
151
+ refusal of a `no`/`unknown` query whose contrary was derivable and unshown, are gone: full
152
+ answer-set evaluation makes both gates redundant for correctness. A later `--strict` may promote
153
+ the notes to faults; that flag is not built yet.
154
+ - A theory atom with no `#theory` declaration is reported with elenctic's own message and location
155
+ rather than clingo's.
156
+ - The theory-atom error points at the first theory atom, not at line 1.
157
+
158
+ ### Removed
159
+ - `Pattern.match`, `Pattern.bindings_in` and the module `elenctic.matching`, a one-sided matcher of a
160
+ pattern against ground symbols. Query answering unifies instead (`unify`, the algorithm of `mgu`);
161
+ there is one mechanism for "does this pattern have this instance".
162
+
163
+ ### Fixed
164
+ - Reference: `elenctic version` exits `REPORTED` (0), not `CLAIMS_HOLD`.
165
+ - A binding query with an anonymous variable, `@query yes { p(_, X) } = { … }`, is refused where it
166
+ is read (`FAULT`, 2: "an anonymous variable has no binding to state: name it") instead of
167
+ failing inside elenctic (`INTERNAL_ERROR`, 3); `bindings` raises `ValueError` for it. An instance
168
+ of such a pattern is not ground, so it has no answer to give.
169
+ - Documented that an objective only ranks answer sets and never removes any: unqualified claims and
170
+ every `@query` read all answer sets, and only `optimal` claims and `@cost` read the optimal ones
171
+ (`docs/explanation/semantics.md`, which the reference and the guides point to). The behavior is unchanged.
172
+ - Queries evaluate over the full answer sets, not the shown projection: `-b. #show b/0.` with
173
+ `@query yes { -b }` is yes, and `-p. #show p/0.` with `@query yes { p }` reports no (not unknown).
174
+ - An empty `no` binding is no longer a vacuous pass when a contrary holds:
175
+ `-p(1). #show p/1.` with `@query no { p(X) } = { }` fails; the no-set is `{1}`.
176
+ - A variable `#show X : q(X).` no longer makes every predicate "shown" for coverage checks, and a
177
+ conditional `#show p : r.` no longer counts as unconditionally showing `p`.
178
+ - `Program.read` accepts any path-like (`str | PathLike`).
179
+ - A search is about 2.3 times faster: an answer set is read into literals only once it is kept, and each
180
+ clingo symbol is converted once.
181
+ - Native `@cautious`/`@brave` under a limit on answer sets are decided from the solver, not the cut search.
182
+ - A minimal core is tested on a non-trivial case and under a time limit (`minimal=False`).
183
+ - A native `@cautious`/`@brave` claim is judged only from a search that ran to its end; if the search
184
+ was cut by a limit on seconds or answer sets, or by an interrupt, it is undecided, where it was reported as a pass or a
185
+ fail on the partial set.
186
+ - A term nested thousands deep in a theory atom, an objective or a `#show` is checked or refused
187
+ instead of raising `RecursionError`.
188
+ - Program text in the terminal report is shown with control characters escaped, so a program cannot
189
+ write terminal control sequences; JSON is unchanged.
190
+ - A file that is not UTF-8 is a refusal of the request (`FAULT`, 2), not a traceback.
191
+ - `pixi run check` prints the number of tests: pytest was given `-q` twice.
192
+
193
+ [1.0.0]: https://github.com/GregoryGelfond/elenctic/releases/tag/v1.0.0
@@ -0,0 +1,24 @@
1
+ # syntax=docker/dockerfile:1
2
+ # Minimal image whose only purpose is running elenctic (CLI entrypoint; clingo and clingcon included).
3
+ #
4
+ # Build: docker build -f Containerfile -t elenctic .
5
+ # Run: docker run --rm -v "$PWD":/work:ro elenctic [elenctic arguments]
6
+ # No published image; no CI build — ship the recipe only.
7
+
8
+ FROM docker.io/library/python:3.14-slim-trixie@sha256:af8a5aa14780025dbfbcfbc81bd8a91ebd87711dd5b0f48ed71090949dd7cb1d AS build
9
+ COPY --from=docker.io/astral/uv:0.13.7@sha256:96f40d3904aad8d0bd010539001d36c8e2188b981e1334471a90c4a1d93bb490 /uv /bin/uv
10
+ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never
11
+ WORKDIR /src
12
+ COPY pyproject.toml uv.lock README.md ./
13
+ COPY src ./src
14
+ COPY docs/reference/output-v1.schema.json ./docs/reference/output-v1.schema.json
15
+ RUN uv venv /opt/elenctic \
16
+ && UV_PROJECT_ENVIRONMENT=/opt/elenctic uv sync --frozen --no-dev --extra theory --no-editable \
17
+ && /opt/elenctic/bin/python -c "import clingo, clingcon; import elenctic"
18
+
19
+ FROM docker.io/library/python:3.14-slim-trixie@sha256:af8a5aa14780025dbfbcfbc81bd8a91ebd87711dd5b0f48ed71090949dd7cb1d
20
+ COPY --from=build /opt/elenctic /opt/elenctic
21
+ ENV PATH=/opt/elenctic/bin:$PATH
22
+ USER 65534:65534
23
+ WORKDIR /work
24
+ ENTRYPOINT ["elenctic"]
elenctic-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gregory Gelfond
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.5
2
+ Name: elenctic
3
+ Version: 1.0.0
4
+ Summary: Contracts and queries for answer set programs: a library first, a command line second
5
+ Project-URL: Homepage, https://github.com/GregoryGelfond/elenctic
6
+ Project-URL: Documentation, https://github.com/GregoryGelfond/elenctic/blob/main/docs/index.md
7
+ Project-URL: Changelog, https://github.com/GregoryGelfond/elenctic/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/GregoryGelfond/elenctic/issues
9
+ Author-email: Gregory Gelfond <gregory_gelfond@fastmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Requires-Python: >=3.14
13
+ Requires-Dist: clingo<5.9,>=5.8.2
14
+ Provides-Extra: theory
15
+ Requires-Dist: clingcon<5.3,>=5.2.1; extra == 'theory'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # elenctic
19
+
20
+ [![CI](https://github.com/GregoryGelfond/elenctic/actions/workflows/ci.yml/badge.svg)](https://github.com/GregoryGelfond/elenctic/actions/workflows/ci.yml)
21
+ [![Python 3.14 | 3.15](https://img.shields.io/badge/python-3.14%20%7C%203.15-blue)](https://www.python.org/downloads/)
22
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
23
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
24
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
25
+ [![Pixi](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh)
26
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
27
+ [![PyPI](https://img.shields.io/pypi/v/elenctic)](https://pypi.org/project/elenctic/)
28
+ <!-- TODO(conda-forge): enable once the package is available on conda-forge.
29
+ [![conda-forge](https://img.shields.io/conda/vn/conda-forge/elenctic)](https://anaconda.org/conda-forge/elenctic)
30
+ -->
31
+
32
+ A testing and analysis library for answer set programs, with a thin CLI. Claims about a program
33
+ live in its own comments (`@expect`, `@count`, `@answer-set`, `@cautious`, `@brave`, `@query`,
34
+ `@cost`) and are checked under clingo or clingcon.
35
+
36
+ *Elenctic*, from Greek ἐλεγκτικός (ἔλεγχος, *elenchos*), "serving to refute by
37
+ cross-examination": the Socratic elenchus. A contract is a thesis about the program; `FAIL` is
38
+ its refutation, and `UNDECIDED` is aporia, an honest "cannot say" rather than a false "no".
39
+
40
+ ## Install
41
+
42
+ Requires Python 3.14+ and clingo 5.8. The optional `theory` extra pulls in clingcon 5.2 for
43
+ contracts that declare `% @elenctic solver clingcon`.
44
+
45
+ ```bash
46
+ # pip
47
+ pip install elenctic
48
+ pip install 'elenctic[theory]'
49
+
50
+ # uv — as a project dependency, or as a tool on PATH
51
+ uv add elenctic
52
+ uv add 'elenctic[theory]'
53
+ uv tool install elenctic
54
+ uv tool install 'elenctic[theory]'
55
+
56
+ # pixi — from PyPI
57
+ pixi add --pypi elenctic
58
+ pixi add --pypi 'elenctic[theory]'
59
+ ```
60
+
61
+ <!-- TODO(conda-forge): enable once the package is available on conda-forge.
62
+ ```bash
63
+ # pixi — from conda-forge
64
+ pixi add elenctic
65
+
66
+ # conda / mamba (conda-forge)
67
+ conda install -c conda-forge elenctic
68
+ mamba install -c conda-forge elenctic
69
+ ```
70
+ -->
71
+
72
+ From source, straight from Git or from a clone:
73
+
74
+ ```bash
75
+ pip install 'elenctic[theory] @ git+https://github.com/GregoryGelfond/elenctic'
76
+
77
+ git clone https://github.com/GregoryGelfond/elenctic && cd elenctic
78
+ pip install '.[theory]'
79
+ uv tool install '.[theory]'
80
+ pixi install -e theory # the locked environment
81
+ ```
82
+
83
+ ### Container
84
+
85
+ No prebuilt image is published. The `Containerfile` is a local recipe: build it yourself with
86
+ Docker or Podman, then mount a directory of `.lp` files and run a check.
87
+
88
+ ```bash
89
+ docker build -f Containerfile -t elenctic .
90
+ podman build -f Containerfile -t elenctic .
91
+
92
+ docker run --rm -v "$PWD":/work:ro elenctic drinks.lp
93
+ podman run --rm -v "$PWD":/work:ro elenctic drinks.lp
94
+ ```
95
+
96
+ The image includes the `theory` extra (clingo and clingcon). The entrypoint is `elenctic`; pass
97
+ the same arguments you would on the host.
98
+
99
+ ## Quick start
100
+
101
+ Save this as `drinks.lp`:
102
+
103
+ ```
104
+ % @expect sat
105
+ % @count 2
106
+ % @brave { tea, coffee }
107
+ % @cautious { biscuit }
108
+ % @query unknown { tea }
109
+ { tea; coffee } = 1.
110
+ biscuit.
111
+ #show tea/0.
112
+ #show coffee/0.
113
+ #show biscuit/0.
114
+ ```
115
+
116
+ ```bash
117
+ $ elenctic drinks.lp
118
+ 1/1 passed
119
+ ```
120
+
121
+ Change `@count 2` to `@count 3` and run again:
122
+
123
+ ```bash
124
+ $ elenctic drinks.lp
125
+ drinks.lp — FAIL
126
+ [FAIL] @count 3 (line 2): 3 expected, 2 distinct shown answer sets
127
+
128
+ 0/1 passed, 1 fail
129
+ ```
130
+
131
+ `elenctic PATH…` is `elenctic test PATH…`. From Python the same check is silent and returns a
132
+ value:
133
+
134
+ ```python
135
+ from elenctic import Limits, Program, check
136
+
137
+ report = check(Program.read("drinks.lp"), limits=Limits(max_seconds=10))
138
+ ```
139
+
140
+ ## Usage
141
+
142
+ | | |
143
+ |---|---|
144
+ | `elenctic [test] PATH…` | check programs against their contracts ([reference](docs/reference/reference.md#command-line)) |
145
+ | `--format full\|short\|json` | rustc multi-line diagnostics (default), one-line short, or JSON ([output](docs/reference/reference.md#output), [diagnostics](docs/reference/reference.md#diagnostics)) |
146
+ | Exit `0` / `1` / `2` / `3` | claims hold, claim unmet, fault, internal error ([exit codes](docs/reference/reference.md#exit-codes)) |
147
+
148
+ Claims with real command output: [docs/reference/examples.md](docs/reference/examples.md).
149
+ Tutorial: [docs/tutorial/first-contract.md](docs/tutorial/first-contract.md). Full map:
150
+ [docs/index.md](docs/index.md).
151
+
152
+ ## Develop
153
+
154
+ ```bash
155
+ pixi install -e theory
156
+ pixi run -e theory check # format, lint, strict types, import contracts, tests
157
+ pixi run -e py314 test # or -e py315
158
+ ```
159
+
160
+ `pixi run check` is the one gate; `./check.sh` runs it. `uv.lock` is tracked so
161
+ `uv sync --locked` reproduces the same dependencies.
162
+
163
+ ## License
164
+
165
+ Licensed under the [MIT license](LICENSE). Changes are in the [changelog](CHANGELOG.md).