mathema 0.6.0__py3-none-any.whl
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.
- mathema/__init__.py +806 -0
- mathema/_brute_force.py +154 -0
- mathema/_deprecation.py +49 -0
- mathema/_indexed.py +145 -0
- mathema/_math_vocab.py +181 -0
- mathema/_providers.py +100 -0
- mathema/_render_mode.py +41 -0
- mathema/_sampling.py +182 -0
- mathema/_scan.py +134 -0
- mathema/_shrink.py +77 -0
- mathema/_target_resolvers.py +51 -0
- mathema/_tier_text.py +114 -0
- mathema/_timeout.py +155 -0
- mathema/acceptance.py +1282 -0
- mathema/analysis.py +1019 -0
- mathema/audit.py +1365 -0
- mathema/auth.py +334 -0
- mathema/authoring.py +727 -0
- mathema/badges.py +870 -0
- mathema/centrality.py +119 -0
- mathema/claim_families.py +2762 -0
- mathema/cli.py +3055 -0
- mathema/compendium/__init__.py +544 -0
- mathema/compendium/export.py +189 -0
- mathema/compiled.py +190 -0
- mathema/concepts.py +298 -0
- mathema/conjecture.py +5050 -0
- mathema/corroboration.py +185 -0
- mathema/diagnostics.py +592 -0
- mathema/dimensions.py +241 -0
- mathema/docstring.py +932 -0
- mathema/domain.py +1820 -0
- mathema/equivalence.py +827 -0
- mathema/f.py +59 -0
- mathema/families.py +287 -0
- mathema/fetch.py +91 -0
- mathema/finite_sets.py +123 -0
- mathema/forms.py +210 -0
- mathema/gates.py +826 -0
- mathema/grammar.py +3057 -0
- mathema/hazards.py +730 -0
- mathema/identity.py +166 -0
- mathema/impl_coverage.py +457 -0
- mathema/intent.py +162 -0
- mathema/interfaces/__init__.py +15 -0
- mathema/interfaces/extension.py +165 -0
- mathema/interfaces/mcp/__init__.py +18 -0
- mathema/interfaces/mcp/resources.py +222 -0
- mathema/interfaces/mcp/server.py +132 -0
- mathema/interfaces/mcp/tools.py +685 -0
- mathema/interfaces/runtime.py +147 -0
- mathema/inventory.py +999 -0
- mathema/lexicon.py +1085 -0
- mathema/linalg.py +233 -0
- mathema/locks.py +126 -0
- mathema/matrices.py +388 -0
- mathema/moved.py +137 -0
- mathema/partiality.py +43 -0
- mathema/probing.py +1211 -0
- mathema/py.typed +0 -0
- mathema/reason_codes.py +936 -0
- mathema/records.py +321 -0
- mathema/representations.py +102 -0
- mathema/review.py +195 -0
- mathema/routes.py +232 -0
- mathema/spec.py +2297 -0
- mathema/suggest.py +727 -0
- mathema/symbolic/__init__.py +47 -0
- mathema/symbolic/_base.py +1154 -0
- mathema/symbolic/_conditioned.py +1037 -0
- mathema/symbolic/_coupled.py +205 -0
- mathema/symbolic/_dot.py +188 -0
- mathema/symbolic/_extensive.py +769 -0
- mathema/symbolic/_fold.py +917 -0
- mathema/symbolic/_forms.py +250 -0
- mathema/symbolic/_guard_points.py +184 -0
- mathema/symbolic/_loop_shapes.py +108 -0
- mathema/symbolic/_matrix.py +400 -0
- mathema/symbolic/_normalize.py +422 -0
- mathema/symbolic/_partiality.py +591 -0
- mathema/symbolic/_proof_support.py +2823 -0
- mathema/symbolic/_prove.py +3402 -0
- mathema/symbolic/_recurrence.py +275 -0
- mathema/symbolic/_residues.py +908 -0
- mathema/symbolic/_seq_common.py +698 -0
- mathema/symbolic/_smt.py +309 -0
- mathema/symbolic/_strategies.py +345 -0
- mathema/symbolic/_sum.py +584 -0
- mathema/sync.py +444 -0
- mathema/targets.py +306 -0
- mathema/tiers.py +268 -0
- mathema/types.py +527 -0
- mathema/verify.py +1109 -0
- mathema-0.6.0.dist-info/METADATA +487 -0
- mathema-0.6.0.dist-info/RECORD +100 -0
- mathema-0.6.0.dist-info/WHEEL +5 -0
- mathema-0.6.0.dist-info/entry_points.txt +2 -0
- mathema-0.6.0.dist-info/licenses/LICENSE.md +108 -0
- mathema-0.6.0.dist-info/licenses/LICENSING.md +67 -0
- mathema-0.6.0.dist-info/top_level.txt +1 -0
mathema/__init__.py
ADDED
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
# Copyright 2026 Tetrion Ltd
|
|
3
|
+
"""mathema: Claim-Driven Development, turning software intent into
|
|
4
|
+
verifiable evidence.
|
|
5
|
+
|
|
6
|
+
import mathema
|
|
7
|
+
r = mathema.check(my_function, claims=["f(-x) == -f(x)"])
|
|
8
|
+
print(r)
|
|
9
|
+
mathema.write_spec(my_function, claims=[...]) # also writes the record
|
|
10
|
+
|
|
11
|
+
State a claim, verify it against the real function, keep the record. Every
|
|
12
|
+
claim is adjudicated on one of two evidence routes: **derive** lifts the
|
|
13
|
+
function's body to a sympy expression and decides the claim algebraically
|
|
14
|
+
(`proven` is exact, not sampled, see `symbolic.py`), or **probe** calls the
|
|
15
|
+
real function on sampled/seeded inputs and checks the claim numerically
|
|
16
|
+
(`holds (n=...)`/`falsified`, see `probing.py`). A function that can't be
|
|
17
|
+
lifted (a loop, a non-scalar parameter, ...) still gets probe-route
|
|
18
|
+
coverage; derive is strictly stronger evidence where it applies, not a
|
|
19
|
+
replacement for probe. Every claim binds to the function's identity hashes
|
|
20
|
+
so a later change is caught, not silently inherited.
|
|
21
|
+
|
|
22
|
+
A bare string claim (as above) takes the `best` route: a symbolic proof
|
|
23
|
+
is attempted first, and a claim the proof cannot decide falls through to
|
|
24
|
+
probing, so the returned entry's `route` names the mechanism that settled
|
|
25
|
+
it. To pin the route, build the claim with `route="derive"` or
|
|
26
|
+
`route="probe"` and pass it in the same `claims=` list; `mathema.check()`
|
|
27
|
+
accepts a pre-built claim object exactly as it accepts a string, so
|
|
28
|
+
nothing else about the call changes:
|
|
29
|
+
|
|
30
|
+
r = mathema.check(my_function,
|
|
31
|
+
claims=[mathema.claims.claim("f(-x) == -f(x)", route="derive")])
|
|
32
|
+
|
|
33
|
+
A `proven` verdict routed `derive` means the proof decided it, never
|
|
34
|
+
sampling. When the proof cannot decide, the claim still falls through to
|
|
35
|
+
probing, and `Probe.meta["mathema.derive_status"]` on the returned entry
|
|
36
|
+
("unliftable" vs. "undecided") records why the proof stopped;
|
|
37
|
+
see authoring.md's own claim-grammar section for the full grammar
|
|
38
|
+
(`d(...)`/`lim(...)`/`integrate(...)`/`Sum(...)`, domain quantifiers,
|
|
39
|
+
`raises(...)`) `route="derive"` understands. `mathema.claims` is this same
|
|
40
|
+
`claim()`/`check_conjectures()` pair exposed directly, for calling the
|
|
41
|
+
derive route standalone without `check()`'s own built-in-law probing:
|
|
42
|
+
|
|
43
|
+
results = mathema.claims.check_conjectures(
|
|
44
|
+
my_function, [mathema.claims.claim("f(-x) == -f(x)", route="derive")])
|
|
45
|
+
|
|
46
|
+
This package implements the Claim-Driven Development spec: see the sibling
|
|
47
|
+
`claim-driven-development` repo's `v0.2.0/` for the workflow definition and
|
|
48
|
+
the record schema this package reads and writes (`SPEC_VERSION` below
|
|
49
|
+
states which version).
|
|
50
|
+
"""
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
from dataclasses import dataclass, field
|
|
54
|
+
|
|
55
|
+
from .analysis import Facts, SourceUnavailable, _parse_notes, analyze_source
|
|
56
|
+
# aliased on import: the bare name `claims` is already the conjecture-module
|
|
57
|
+
# alias below (`mathema.claims.check(...)`, load-bearing in README/tests), so
|
|
58
|
+
# the claims()-decorator is exposed as `claims_decorator` at this level,
|
|
59
|
+
# still directly importable as `from mathema.authoring import claims`.
|
|
60
|
+
from .authoring import (DomainError, claims as claims_decorator,
|
|
61
|
+
declared_from_function, enforce_dimensions, enforce_domain,
|
|
62
|
+
enforce_structure,
|
|
63
|
+
materialize_declared, parse_docstring_claims,
|
|
64
|
+
reject_missing, resolve_declared, retrieve)
|
|
65
|
+
from .claim_families import _register_builtin_claim_families
|
|
66
|
+
from .conjecture import Conjecture, check_conjectures, claim
|
|
67
|
+
from .diagnostics import critical_points
|
|
68
|
+
from .compiled import CompiledForm, compile_form, numeric_check
|
|
69
|
+
from .forms import (Form, SubstitutedForm, Substitution, closed_forms,
|
|
70
|
+
executable_forms, register_substitution,
|
|
71
|
+
substituted_forms)
|
|
72
|
+
from .identity import form_hash
|
|
73
|
+
from .grammar import get_unicode_output, set_unicode_output
|
|
74
|
+
from .probing import Probe, probe
|
|
75
|
+
from .reason_codes import Category, ClaimReasonCode, ReasonCode, issue
|
|
76
|
+
from .spec import SPEC_VERSION
|
|
77
|
+
from .targets import Target, TargetError, resolve, resolve_function
|
|
78
|
+
from .verify import GateReport, VerifyResult, gate, verify_project
|
|
79
|
+
from .suggest import suggest_claims
|
|
80
|
+
# exposed for direct, notebook-style exploration of what a function
|
|
81
|
+
# lifts to; `mathema.lift_symbolic`, not bare `lift`, since `lift` is
|
|
82
|
+
# too generic a name to claim at this module's own top level. See
|
|
83
|
+
# docs/authoring.md's "Exploring a function symbolically" section.
|
|
84
|
+
from .symbolic import lift as lift_symbolic
|
|
85
|
+
from .symbolic._dot import _register_dot_family
|
|
86
|
+
from .symbolic._fold import _register_fold_family
|
|
87
|
+
from .symbolic._sum import _register_sum_family
|
|
88
|
+
|
|
89
|
+
from . import conjecture as claims
|
|
90
|
+
from . import spec as registry
|
|
91
|
+
|
|
92
|
+
import sys as _sys
|
|
93
|
+
for _alias, _mod in (("claims", claims), ("registry", registry)):
|
|
94
|
+
_sys.modules[f"{__name__}.{_alias}"] = _mod
|
|
95
|
+
|
|
96
|
+
# The built-in claim families register here, explicitly, once per
|
|
97
|
+
# process, not as an import side effect of the modules that define
|
|
98
|
+
# them. Any `mathema.*` import runs this package __init__ first, so the
|
|
99
|
+
# registry is always populated before check_conjectures() or try_prove()
|
|
100
|
+
# can look a family up.
|
|
101
|
+
_register_builtin_claim_families()
|
|
102
|
+
_register_dot_family()
|
|
103
|
+
_register_fold_family()
|
|
104
|
+
_register_sum_family()
|
|
105
|
+
|
|
106
|
+
__version__ = "0.6.0"
|
|
107
|
+
__all__ = ["claim", "check", "write_spec", "retrieve", "analyze",
|
|
108
|
+
"status", "track_claims",
|
|
109
|
+
"tagged", "Record", "claims", "registry", "SPEC_VERSION",
|
|
110
|
+
"__version__", "claims_decorator", "declared_from_function",
|
|
111
|
+
"resolve_declared", "materialize_declared",
|
|
112
|
+
"parse_docstring_claims", "enforce_domain", "enforce_dimensions", "enforce_structure", "reject_missing",
|
|
113
|
+
"DomainError", "docstring_report", "DocstringReport", "lift_symbolic",
|
|
114
|
+
"critical_points", "suggest_claims", "Conjecture",
|
|
115
|
+
"issue", "ReasonCode", "Category", "ClaimReasonCode",
|
|
116
|
+
"get_unicode_output", "set_unicode_output",
|
|
117
|
+
"Form", "SubstitutedForm", "Substitution", "closed_forms",
|
|
118
|
+
"substituted_forms", "register_substitution",
|
|
119
|
+
"resolve", "resolve_function", "Target", "TargetError",
|
|
120
|
+
"gate", "verify_project", "GateReport", "VerifyResult",
|
|
121
|
+
"executable_forms", "CompiledForm", "compile_form",
|
|
122
|
+
"numeric_check", "form_hash"]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class Record:
|
|
127
|
+
"""The result of checking a function's claims: the facts read off it,
|
|
128
|
+
and one Probe per claim adjudicated (built-in laws plus any claims
|
|
129
|
+
passed in). See the claim-driven-development repo's
|
|
130
|
+
v0.2.0/record-schema.md for the YAML shape this turns into via
|
|
131
|
+
to_spec()/save_spec()."""
|
|
132
|
+
facts: Facts
|
|
133
|
+
probes: list[Probe]
|
|
134
|
+
spec_path: str | None = None
|
|
135
|
+
# `lifted` holds the derive route's own symbolic-lift result
|
|
136
|
+
# (symbolic.Lifted) when the body lifts to a closed form, None
|
|
137
|
+
# otherwise; every reader treats None as a valid value. `concepts`
|
|
138
|
+
# is reserved for a future understanding/Mathemata layer and is
|
|
139
|
+
# always empty.
|
|
140
|
+
lifted: object | None = field(default=None, repr=False)
|
|
141
|
+
concepts: list = field(default_factory=list, repr=False)
|
|
142
|
+
dependencies: list = field(default_factory=list, repr=False)
|
|
143
|
+
# one-deep callee records (inventory.function_dependencies): every
|
|
144
|
+
# sibling function/class/module this function references, with key,
|
|
145
|
+
# file, line, and (for functions) the form hash freshness checks
|
|
146
|
+
# compare against, written into the verified spec as its own
|
|
147
|
+
# section, annotated with per-dependency freshness at record time.
|
|
148
|
+
# namespaced extension data, the same role Probe.meta already plays
|
|
149
|
+
# for one claim; this is the function-level counterpart, for data
|
|
150
|
+
# that isn't a stable, always-computed field (diagnostics.
|
|
151
|
+
# diagnostic_report(), opted into per function, is the first real
|
|
152
|
+
# user: "mathema.diagnostic_report" -> its own dict). Never
|
|
153
|
+
# populated automatically by check()/write_spec(), a caller sets it
|
|
154
|
+
# explicitly when it wants the extra work done.
|
|
155
|
+
meta: dict = field(default_factory=dict, repr=False)
|
|
156
|
+
|
|
157
|
+
def __repr__(self) -> str:
|
|
158
|
+
from .analysis import tier_word
|
|
159
|
+
lines = [f"mathema.Record({self.facts.name}) · {tier_word(self.facts.tier)} "
|
|
160
|
+
f"· form {self.facts.form}"]
|
|
161
|
+
for p in self.probes:
|
|
162
|
+
mark = {"holds": "holds ", "falsified": "FALSIFY", "proven": "proven ",
|
|
163
|
+
"skipped": "skip ", "unknown": "unknown",
|
|
164
|
+
"invalidated": "INVALID"}.get(p.verdict.split(":", 1)[0], p.verdict)
|
|
165
|
+
if p.verdict == "proven":
|
|
166
|
+
# a proof has no trial count the way a probe does, the
|
|
167
|
+
# statement itself gets prettified into ordinary math
|
|
168
|
+
# notation, and the quantifier (who it holds for) stands
|
|
169
|
+
# in place of "(n=...)", on its own line since it's
|
|
170
|
+
# often longer than the statement it qualifies.
|
|
171
|
+
stmt = p.statement.replace("==", "=").replace("<=", "≤").replace(">=", "≥")
|
|
172
|
+
line = f" {mark} {p.name}: {stmt}"
|
|
173
|
+
if p.condition:
|
|
174
|
+
line += f"\n {p.condition}"
|
|
175
|
+
else:
|
|
176
|
+
line = f" {mark} {p.name}: {p.statement}"
|
|
177
|
+
if p.verdict == "holds" and p.n:
|
|
178
|
+
line += f" (n={p.n})"
|
|
179
|
+
if p.counterexample:
|
|
180
|
+
line += f"\n counterexample {p.counterexample}"
|
|
181
|
+
stratum = getattr(p, "stratum", None)
|
|
182
|
+
if stratum:
|
|
183
|
+
# which stratum the falsification indicts, one line:
|
|
184
|
+
# the verdict above is unchanged, this only classifies
|
|
185
|
+
parts = []
|
|
186
|
+
if stratum.get("mathematics"):
|
|
187
|
+
parts.append(f"mathematics {stratum['mathematics']}")
|
|
188
|
+
if stratum.get("cause"):
|
|
189
|
+
parts.append(stratum["cause"])
|
|
190
|
+
elif stratum.get("blame"):
|
|
191
|
+
parts.append(f"blame {stratum['blame']}")
|
|
192
|
+
if parts:
|
|
193
|
+
line += f"\n [{', '.join(parts)}]"
|
|
194
|
+
lines.append(line)
|
|
195
|
+
return "\n".join(lines)
|
|
196
|
+
|
|
197
|
+
def to_spec(self) -> dict:
|
|
198
|
+
from .spec import to_spec
|
|
199
|
+
return to_spec(self)
|
|
200
|
+
|
|
201
|
+
def save_spec(self, path: str) -> str:
|
|
202
|
+
"""Write the decoupled spec: intent, identity, claims, references,
|
|
203
|
+
and the reasoning chain, as standalone YAML bound to the identity
|
|
204
|
+
hashes."""
|
|
205
|
+
from .spec import save_spec
|
|
206
|
+
return save_spec(self, path)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def analyze(fn) -> Facts:
|
|
210
|
+
"""Machine-derived facts about a function: parameters, purity, guards,
|
|
211
|
+
structural shape. Pure `ast`, no probing, no claim verification.
|
|
212
|
+
|
|
213
|
+
Falls back to a documentation-only record (tier 0) for callables with no
|
|
214
|
+
retrievable Python source (builtins, C extensions): the docstring states
|
|
215
|
+
the intent, and probing still runs against the live callable."""
|
|
216
|
+
injected = getattr(fn, "__mathema_facts__", None)
|
|
217
|
+
if isinstance(injected, Facts):
|
|
218
|
+
# a resolver-built proxy carries the facts its frontend could
|
|
219
|
+
# honestly state (a namespaced form hash, real param kinds,
|
|
220
|
+
# tree=None); they are richer than the doc-only fallback the
|
|
221
|
+
# source-less path below would produce, so they win outright
|
|
222
|
+
return injected
|
|
223
|
+
try:
|
|
224
|
+
facts = analyze_source(fn)
|
|
225
|
+
except SourceUnavailable:
|
|
226
|
+
return _doc_only_facts(fn)
|
|
227
|
+
facts.tier = 3 if not facts.is_pure else 2
|
|
228
|
+
return facts
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _doc_only_facts(fn) -> Facts:
|
|
232
|
+
"""A documentation-only record (tier 0) for callables without Python
|
|
233
|
+
source. Intent is the author's documented claim: a real but weak
|
|
234
|
+
evidence class, and the only one available for compiled code."""
|
|
235
|
+
import hashlib
|
|
236
|
+
import inspect
|
|
237
|
+
|
|
238
|
+
from .intent import parse_doc
|
|
239
|
+
|
|
240
|
+
doc = inspect.getdoc(fn) or ""
|
|
241
|
+
parsed = parse_doc(doc)
|
|
242
|
+
name = getattr(fn, "__name__", "callable")
|
|
243
|
+
try:
|
|
244
|
+
sig = inspect.signature(fn)
|
|
245
|
+
params = [n for n, p in sig.parameters.items()
|
|
246
|
+
if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
|
|
247
|
+
and p.default is p.empty]
|
|
248
|
+
sig_str = str(sig)
|
|
249
|
+
except (ValueError, TypeError):
|
|
250
|
+
params, sig_str = [], "(…)"
|
|
251
|
+
for arity in (1, 2, 3):
|
|
252
|
+
try:
|
|
253
|
+
fn(*[0.5] * arity)
|
|
254
|
+
except TypeError:
|
|
255
|
+
continue
|
|
256
|
+
except Exception:
|
|
257
|
+
pass # domain error still confirms the arity
|
|
258
|
+
params = [f"x{i}" for i in range(arity)] if arity > 1 else ["x"]
|
|
259
|
+
sig_str = "(" + ", ".join(params) + ")"
|
|
260
|
+
break
|
|
261
|
+
return Facts(
|
|
262
|
+
name=name,
|
|
263
|
+
signature=sig_str,
|
|
264
|
+
params=params,
|
|
265
|
+
param_kinds={p: "unknown" for p in params},
|
|
266
|
+
returns_kind="unknown",
|
|
267
|
+
docstring=doc or None,
|
|
268
|
+
source="<no Python source; documentation-only record>",
|
|
269
|
+
effects=[],
|
|
270
|
+
# purity cannot be established without source: null, per the
|
|
271
|
+
# record schema (and the general rule: a tool never reports a
|
|
272
|
+
# stronger epistemic state than it has a basis for). Probes
|
|
273
|
+
# still check determinism empirically either way.
|
|
274
|
+
is_pure=None,
|
|
275
|
+
loops=[], comprehensions=[], recursion=False, branch_count=0,
|
|
276
|
+
call_groups={"math": [], "builtin": [], "external": []},
|
|
277
|
+
max_loop_depth=0, lines=0,
|
|
278
|
+
form="doc:" + hashlib.sha256((doc or name).encode()).hexdigest()[:8],
|
|
279
|
+
sigh=hashlib.sha256(sig_str.encode()).hexdigest()[:12],
|
|
280
|
+
tier=0, tree=None,
|
|
281
|
+
doc_intent=parsed.summary or None,
|
|
282
|
+
doc_refs=parsed.refs,
|
|
283
|
+
doc_hints=parsed.concept_hints,
|
|
284
|
+
doc_notes=_parse_notes(doc),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _expand_claim_keywords(claims: list, fn, facts, parent_domain: dict,
|
|
289
|
+
extensive: bool) -> list:
|
|
290
|
+
"""Intent:
|
|
291
|
+
Replace battery keywords inside a claims list with the hazard
|
|
292
|
+
claims they name: `defined` / `excluding` / `stable` /
|
|
293
|
+
`stateless` (and their long/spaced spellings) each expand to
|
|
294
|
+
the RELEVANT members, the same structural gates
|
|
295
|
+
suggest_claims applies decide relevance, so a keyword asks a
|
|
296
|
+
question, never forces an irrelevant claim. Everything else
|
|
297
|
+
(law strings, Conjectures) passes through untouched, and the
|
|
298
|
+
expanded claims carry source "user": a keyword is adoption,
|
|
299
|
+
so its claims gate like any hand-written one.
|
|
300
|
+
|
|
301
|
+
Notes:
|
|
302
|
+
A string that is neither a keyword nor parseable as a law,
|
|
303
|
+
but is closely spelled to a keyword, raises with the whole
|
|
304
|
+
keyword vocabulary and its meanings, a near-miss teaches
|
|
305
|
+
the battery rather than failing as a grammar error. Keyword
|
|
306
|
+
claims never carry a route: the safety members are examine
|
|
307
|
+
by construction.
|
|
308
|
+
"""
|
|
309
|
+
from dataclasses import replace as _replace
|
|
310
|
+
|
|
311
|
+
from .conjecture import InvalidConjecture, claim
|
|
312
|
+
from .families import close_keyword, expand_keyword, keyword_help
|
|
313
|
+
from .suggest import suggest_claims
|
|
314
|
+
|
|
315
|
+
out: list = []
|
|
316
|
+
suggested = None
|
|
317
|
+
for item in claims:
|
|
318
|
+
if isinstance(item, dict):
|
|
319
|
+
# the claims-file row shape, accepted at the call site the
|
|
320
|
+
# same way a claims.yaml row is: one shared construction
|
|
321
|
+
# (spec._declared_conjecture), so the two spellings can
|
|
322
|
+
# never parse differently. A row with no `source` of its
|
|
323
|
+
# own is a call-site claim, exactly like a law string here
|
|
324
|
+
from .spec import _declared_conjecture
|
|
325
|
+
row = dict(item)
|
|
326
|
+
row.setdefault("source", "user")
|
|
327
|
+
out.append(_declared_conjecture(row))
|
|
328
|
+
continue
|
|
329
|
+
if not isinstance(item, str):
|
|
330
|
+
out.append(item)
|
|
331
|
+
continue
|
|
332
|
+
member_names = expand_keyword(item)
|
|
333
|
+
if member_names is None:
|
|
334
|
+
try:
|
|
335
|
+
claim(item)
|
|
336
|
+
except InvalidConjecture:
|
|
337
|
+
near = close_keyword(item)
|
|
338
|
+
if near is not None:
|
|
339
|
+
raise InvalidConjecture(
|
|
340
|
+
f"unknown claim {item!r}; did you mean the "
|
|
341
|
+
f"battery keyword {near!r}? keywords: "
|
|
342
|
+
f"{keyword_help()}") from None
|
|
343
|
+
raise
|
|
344
|
+
out.append(item)
|
|
345
|
+
continue
|
|
346
|
+
if suggested is None:
|
|
347
|
+
suggested = suggest_claims(fn, facts=facts,
|
|
348
|
+
extensive=extensive)
|
|
349
|
+
for cj in suggested:
|
|
350
|
+
if cj.name.split("[", 1)[0] in member_names:
|
|
351
|
+
out.append(_replace(cj, source="user"))
|
|
352
|
+
if "excluded_outside_domain" in member_names:
|
|
353
|
+
# never battery-suggested: generated here per parameter
|
|
354
|
+
# the function-level parent domain bounds (there is no
|
|
355
|
+
# outside otherwise); a claim's own quantifier is not a
|
|
356
|
+
# function-level domain and generates nothing here
|
|
357
|
+
for p in sorted(parent_domain):
|
|
358
|
+
if p in facts.params:
|
|
359
|
+
out.append(claim(f"excluded_outside_domain({p})"))
|
|
360
|
+
return out
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _refuse_name_collisions(entries: list) -> list:
|
|
364
|
+
"""Intent:
|
|
365
|
+
The claims a check adjudicates, keyed by name: an exact
|
|
366
|
+
duplicate (same name, statement and domain) is one claim, and
|
|
367
|
+
two distinct claims under one name are refused.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
InvalidConjecture: when two distinct claims take the same name
|
|
371
|
+
(four domain variants of one law all auto-name `f_mi_ge_0`,
|
|
372
|
+
say), since a name keys the claim's pins, locks and
|
|
373
|
+
verified row and cannot stand for two claims.
|
|
374
|
+
"""
|
|
375
|
+
import json
|
|
376
|
+
|
|
377
|
+
from .conjecture import InvalidConjecture
|
|
378
|
+
|
|
379
|
+
def _stmt(c):
|
|
380
|
+
return c.get("statement") or c.get("law")
|
|
381
|
+
|
|
382
|
+
def _identity(c):
|
|
383
|
+
# the full identity, statement and domain: a domain lives in its
|
|
384
|
+
# own field, not the (domain-stripped) statement
|
|
385
|
+
return (c.get("name"), _stmt(c),
|
|
386
|
+
json.dumps(c.get("domain") or {}, sort_keys=True,
|
|
387
|
+
default=str))
|
|
388
|
+
|
|
389
|
+
seen: dict = {}
|
|
390
|
+
for c in entries:
|
|
391
|
+
seen.setdefault(_identity(c), c)
|
|
392
|
+
by_name: dict = {}
|
|
393
|
+
for c in seen.values():
|
|
394
|
+
prior = by_name.setdefault(c.get("name"), c)
|
|
395
|
+
if prior is not c:
|
|
396
|
+
raise InvalidConjecture(
|
|
397
|
+
f"two claims take the name {c.get('name')!r} "
|
|
398
|
+
f"({_stmt(prior)!r} and {_stmt(c)!r}); give each an "
|
|
399
|
+
f"explicit name (claim(..., name=...), or `name:` in a "
|
|
400
|
+
f"claims file)")
|
|
401
|
+
return list(seen.values())
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _domains_from_claims(claims) -> dict:
|
|
405
|
+
"""Intent:
|
|
406
|
+
The parameter domains declared by the `for` quantifiers of the
|
|
407
|
+
claims being checked, so the built-in battery samples inside the
|
|
408
|
+
region their author actually declared.
|
|
409
|
+
|
|
410
|
+
Notes:
|
|
411
|
+
A claim that will not parse is ignored here and reported by the
|
|
412
|
+
ordinary path instead, so this never changes which errors a
|
|
413
|
+
caller sees. Where two claims bound the same parameter, the
|
|
414
|
+
first stands: the battery only needs one legal region to
|
|
415
|
+
synthesise a call in, and each claim is still adjudicated
|
|
416
|
+
against its own domain regardless. The built-in battery is the
|
|
417
|
+
only reader: these regions never become a function-level domain
|
|
418
|
+
for any claim.
|
|
419
|
+
"""
|
|
420
|
+
from .conjecture import claim as _claim
|
|
421
|
+
|
|
422
|
+
out: dict = {}
|
|
423
|
+
for c in claims or ():
|
|
424
|
+
try:
|
|
425
|
+
parsed = _claim(c) if isinstance(c, str) else c
|
|
426
|
+
except Exception:
|
|
427
|
+
continue
|
|
428
|
+
for name, bound in (getattr(parsed, "domain", None) or {}).items():
|
|
429
|
+
out.setdefault(name, bound)
|
|
430
|
+
return out
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def check(fn, claims: list | None = None, domain: dict | None = None,
|
|
434
|
+
trials: int | None = None,
|
|
435
|
+
trials_scale: float = 1.0, extensive: bool = False,
|
|
436
|
+
declared: dict | None = None,
|
|
437
|
+
known_premises: dict | None = None) -> Record:
|
|
438
|
+
"""Verify a function's claims, each adjudicated against the real
|
|
439
|
+
function.
|
|
440
|
+
|
|
441
|
+
mathema.check(ema) # + suggested claims
|
|
442
|
+
mathema.check(ema, claims=["f(x, 1.0) == x[-1]"]) # add a claim
|
|
443
|
+
mathema.check(ema, claims=[]) # declared surfaces only
|
|
444
|
+
mathema.check(ema, domain={"alpha": (0, 1)})
|
|
445
|
+
mathema.check(ema, declared=mathema.retrieve(ema)) # + file claims
|
|
446
|
+
|
|
447
|
+
check is IO-free: it reads only what travels with the function
|
|
448
|
+
object (decorator, docstring, type markers) and never touches the
|
|
449
|
+
filesystem. The file-declared layer, the highest-precedence
|
|
450
|
+
authoring surface, therefore reaches it only through `declared=`,
|
|
451
|
+
a retrieved entry from `mathema.retrieve(fn, root)`. Call-site
|
|
452
|
+
`claims=` still wins per claim name over everything retrieved.
|
|
453
|
+
`write_spec()`, the CLI, and the MCP tools do this join for you;
|
|
454
|
+
a bare `check(fn)` adjudicates the function-attached surfaces only.
|
|
455
|
+
|
|
456
|
+
`claims` omitted (`None`) defaults to `suggest_claims(fn)`'s
|
|
457
|
+
own proposals, monotonicity/affine-ness/convexity, symmetry,
|
|
458
|
+
idempotence, commutativity/associativity, raises(...) guards, each
|
|
459
|
+
adjudicated at the best evidence level actually available (a proof
|
|
460
|
+
when the function lifts, probing otherwise, see suggest_claims()'s
|
|
461
|
+
own docstring). `claims=[]` (an explicit empty list, not merely
|
|
462
|
+
omitted) means declared surfaces only: no suggested claims are
|
|
463
|
+
added, while the built-in structural probes and the function's own
|
|
464
|
+
decorator/docstring/type-marker claims still run, and so does a
|
|
465
|
+
`declared=` entry when one is passed. Any other explicit `claims=[...]` is
|
|
466
|
+
checked as given, unioned with whatever the function's own decorator
|
|
467
|
+
or docstring declares.
|
|
468
|
+
|
|
469
|
+
`domain` declares parameter limits: algebraic probes sample inside
|
|
470
|
+
the declared domain. Whether the code REJECTS out-of-domain input
|
|
471
|
+
is its own declared claim, `excluded_outside_domain(p)`, state it
|
|
472
|
+
explicitly, opt in with the `excluding` keyword, or get it
|
|
473
|
+
auto-declared by @enforce_domain (the decorator that makes it
|
|
474
|
+
true). There is no adjudication mode: a declared exclusion that
|
|
475
|
+
trials find unenforced is falsified with the accepted value as
|
|
476
|
+
witness; an undeclared one reports nothing. See
|
|
477
|
+
claim-driven-development's v0.2.0/claim-anatomy.md, "Domain:
|
|
478
|
+
declared vs enforced".
|
|
479
|
+
|
|
480
|
+
`known_premises` is caller-supplied context for `assuming <name>
|
|
481
|
+
holds` references that resolve nowhere in the batch: a mapping of
|
|
482
|
+
claim name to a human line (usually from
|
|
483
|
+
`compendium.external_premises`) that the missing-prerequisite note
|
|
484
|
+
then includes. Data only; it never satisfies a premise, and
|
|
485
|
+
check() itself stays IO-free.
|
|
486
|
+
|
|
487
|
+
`extensive` reaches every route this call touches: `probe()`'s own
|
|
488
|
+
critical-point sampling hints and `domain_safe[...]` probe, and a
|
|
489
|
+
`route="derive"` claim's case-split fallback. Default `False`
|
|
490
|
+
everywhere; real, opt-in cost when set.
|
|
491
|
+
|
|
492
|
+
A parameter or return type hinted with a mathema type marker
|
|
493
|
+
(`Annotated[float, Probability]`, `Annotated[list, Shape("m", "n")]`,
|
|
494
|
+
see types.py) contributes its own claims automatically: a domain
|
|
495
|
+
marker merges into `domain` (an explicitly passed bound for the same
|
|
496
|
+
parameter wins), and a `Shape` marker adds a structural probe. Purely
|
|
497
|
+
additive, a function with no markers behaves exactly as before.
|
|
498
|
+
|
|
499
|
+
Domains are per claim. The signature markers and `domain=` together
|
|
500
|
+
are the function-level parent domain; each claim is adjudicated over
|
|
501
|
+
that parent with its own `for` bindings overriding it per parameter,
|
|
502
|
+
and one claim's bindings never reach another claim. A claim's record
|
|
503
|
+
states its own bindings in `domain`/`condition` and whatever the
|
|
504
|
+
parent supplied in `meta["mathema.parent_domain"]`.
|
|
505
|
+
|
|
506
|
+
A `@claims_decorator(...)`-tagged function, or a docstring `Claims:`
|
|
507
|
+
block (see authoring.py), also contributes its claims automatically,
|
|
508
|
+
same as the type markers above; `claims=` passed here is unioned
|
|
509
|
+
with those, winning per claim name on a collision (an explicit call-
|
|
510
|
+
site claim is the most deliberate of the three; see
|
|
511
|
+
authoring.declared_from_function's own decorator-over-docstring rule
|
|
512
|
+
for the same reasoning one layer in).
|
|
513
|
+
"""
|
|
514
|
+
from .probing import _RISK, _SPECIALS
|
|
515
|
+
from .spec import declare, entry_claims
|
|
516
|
+
from .types import _TYPE_PROBE_TRIALS, domain_from_signature, type_probes
|
|
517
|
+
|
|
518
|
+
facts = analyze(fn)
|
|
519
|
+
# the function-level parent domain: signature markers, then an
|
|
520
|
+
# explicit domain= winning per parameter. Each claim is adjudicated
|
|
521
|
+
# over this parent with its own `for` bindings overriding it per
|
|
522
|
+
# parameter; one claim's bindings never reach another claim.
|
|
523
|
+
parent_domain = {**domain_from_signature(fn), **(domain or {})}
|
|
524
|
+
# the built-in battery only needs somewhere the function can be
|
|
525
|
+
# called: a parameter the parent leaves open is synthesised inside
|
|
526
|
+
# a region some claim declared for it, so the battery never reports
|
|
527
|
+
# a raise outside every region the author stated
|
|
528
|
+
battery_domain = {**_domains_from_claims(claims), **parent_domain}
|
|
529
|
+
|
|
530
|
+
from .inventory import function_dependencies
|
|
531
|
+
deps = function_dependencies(fn, facts)
|
|
532
|
+
|
|
533
|
+
def _stamp_surface(ps, surface):
|
|
534
|
+
# the authoring-surface provenance output rows fold into their
|
|
535
|
+
# `source` field (records.row_source); claim-route probes carry
|
|
536
|
+
# theirs from the conjecture instead
|
|
537
|
+
for p in ps:
|
|
538
|
+
meta = dict(p.meta or {})
|
|
539
|
+
meta.setdefault("mathema.surface", surface)
|
|
540
|
+
p.meta = meta
|
|
541
|
+
return ps
|
|
542
|
+
|
|
543
|
+
probes = _stamp_surface(
|
|
544
|
+
probe(fn, facts, domain=battery_domain or None,
|
|
545
|
+
trials=trials, trials_scale=trials_scale, extensive=extensive),
|
|
546
|
+
"builtin")
|
|
547
|
+
|
|
548
|
+
type_trials = trials or _TYPE_PROBE_TRIALS
|
|
549
|
+
scale = min(1.0, trials_scale)
|
|
550
|
+
if scale < 1.0:
|
|
551
|
+
type_trials = max(_RISK.min_trials_floor_when_scaled, len(_SPECIALS),
|
|
552
|
+
round(type_trials * scale))
|
|
553
|
+
probes = probes + _stamp_surface(type_probes(fn, trials=type_trials),
|
|
554
|
+
"types")
|
|
555
|
+
|
|
556
|
+
# whether this list is the caller's or mathema's own matters for
|
|
557
|
+
# precedence below: a call-site claim is the MOST deliberate
|
|
558
|
+
# surface, a suggestion the least
|
|
559
|
+
suggested = claims is None
|
|
560
|
+
if suggested:
|
|
561
|
+
claims = suggest_claims(fn, facts=facts, extensive=extensive)
|
|
562
|
+
claims = _expand_claim_keywords(claims, fn, facts, parent_domain,
|
|
563
|
+
extensive)
|
|
564
|
+
built = [claim(c) if isinstance(c, str) else c for c in claims]
|
|
565
|
+
explicit = []
|
|
566
|
+
for cj in built:
|
|
567
|
+
entry = declare(cj)
|
|
568
|
+
# declare()'s serialized form keeps only importable dotted
|
|
569
|
+
# refs; the caller's actual callables ride beside it under an
|
|
570
|
+
# in-memory-only key (never written to any store), re-attached
|
|
571
|
+
# when the merged entry is rebuilt into claims
|
|
572
|
+
live = {k: v for k, v in (getattr(cj, "funcs", None) or {}).items()
|
|
573
|
+
if callable(v)}
|
|
574
|
+
if live:
|
|
575
|
+
entry["__live_funcs__"] = live
|
|
576
|
+
explicit.append(entry)
|
|
577
|
+
# distinct claims that name alike (four domain variants of one law)
|
|
578
|
+
# cannot share the name-keyed merge below
|
|
579
|
+
explicit = _refuse_name_collisions(explicit)
|
|
580
|
+
from .spec import merge_entries
|
|
581
|
+
# a retrieved entry already carries the function surfaces merged
|
|
582
|
+
# with the file store at the documented precedence; without one,
|
|
583
|
+
# the function's own surfaces stand in (read off the object, so
|
|
584
|
+
# check() stays IO-free)
|
|
585
|
+
authored = (dict(declared) if declared is not None
|
|
586
|
+
else {"claims": declared_from_function(fn)})
|
|
587
|
+
if suggested:
|
|
588
|
+
# a suggestion must never outrank a same-named authored claim.
|
|
589
|
+
# It used to: the suggestion won the merge, so a human's
|
|
590
|
+
# declared claim came back source "suggested", gates false,
|
|
591
|
+
# and its verdict silently stopped counting toward the gate.
|
|
592
|
+
merged_entry = merge_entries({"claims": explicit}, authored,
|
|
593
|
+
on_conflict="silent")
|
|
594
|
+
else:
|
|
595
|
+
# a call-site claim overlays, winning per name: documented
|
|
596
|
+
# precedence, the caller is expressing intent now
|
|
597
|
+
merged_entry = merge_entries(authored, {"claims": explicit},
|
|
598
|
+
on_conflict="silent")
|
|
599
|
+
all_claims = entry_claims(merged_entry)
|
|
600
|
+
if all_claims:
|
|
601
|
+
probes = probes + check_conjectures(fn, all_claims, domain=parent_domain or None,
|
|
602
|
+
trials=trials, trials_scale=trials_scale,
|
|
603
|
+
facts=facts, extensive=extensive,
|
|
604
|
+
known_premises=known_premises,
|
|
605
|
+
float_companions=True)
|
|
606
|
+
from .concepts import Concept, concepts_for, flat_union
|
|
607
|
+
sources = concepts_for(facts, probes)
|
|
608
|
+
dismissed = set(((declared or {}).get("meta") or {}).get(
|
|
609
|
+
"mathema.concepts_dismissed") or [])
|
|
610
|
+
if dismissed:
|
|
611
|
+
# a curated dismissal silences the suggestion, permanently;
|
|
612
|
+
# keyword hints are inferences and never re-offer a tag a
|
|
613
|
+
# person has already said no to
|
|
614
|
+
sources = {k: [c for c in v if c not in dismissed]
|
|
615
|
+
for k, v in sources.items()}
|
|
616
|
+
# keyword hints are inferences, not assertions: they ride the
|
|
617
|
+
# provenance split, clearly labeled, but never the interop union
|
|
618
|
+
# or the record's own concepts field
|
|
619
|
+
asserted = {k: v for k, v in sources.items()
|
|
620
|
+
if k in ("declared", "mechanism") and v}
|
|
621
|
+
concept_objs = [Concept(name, src)
|
|
622
|
+
for src, names in asserted.items() for name in names]
|
|
623
|
+
meta = {}
|
|
624
|
+
if asserted or sources.get("keyword"):
|
|
625
|
+
# the spec's own interop shape (a flat list under
|
|
626
|
+
# meta.concepts) plus the un-flattened provenance beside it
|
|
627
|
+
meta = {"mathema.concept_sources": {k: v for k, v in sources.items()
|
|
628
|
+
if v}}
|
|
629
|
+
if asserted:
|
|
630
|
+
meta["concepts"] = flat_union(asserted)
|
|
631
|
+
# the derive route's closed form, kept on the record so the math
|
|
632
|
+
# section can state it (a proof's subject, rendered and exact).
|
|
633
|
+
# Guarded: an unliftable body simply leaves the field None, which
|
|
634
|
+
# every reader already treats as a valid value.
|
|
635
|
+
try:
|
|
636
|
+
lifted = lift_symbolic(fn, facts)
|
|
637
|
+
except Exception:
|
|
638
|
+
lifted = None
|
|
639
|
+
return Record(facts=facts, probes=probes, dependencies=deps,
|
|
640
|
+
concepts=concept_objs, meta=meta, lifted=lifted)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def write_spec(fn, claims: list | None = None, root: str = ".",
|
|
644
|
+
key: str | None = None, **kwargs) -> Record:
|
|
645
|
+
"""The one-call IO workflow: retrieve the declared layer from the
|
|
646
|
+
project store, check the function's claims against it, write the
|
|
647
|
+
record into the store (.mathema/verified/), and return the Record.
|
|
648
|
+
|
|
649
|
+
mathema.write_spec(my_fn, claims=[...])
|
|
650
|
+
mathema.write_spec(my_fn, domain={"a": (0, 1)}, strict=True)
|
|
651
|
+
|
|
652
|
+
This is `retrieve` + `check` + `spec.record` composed; `check`
|
|
653
|
+
itself stays IO-free.
|
|
654
|
+
"""
|
|
655
|
+
from .authoring import retrieve
|
|
656
|
+
from .spec import declare, merge_entries, record
|
|
657
|
+
|
|
658
|
+
declared = retrieve(fn, root)
|
|
659
|
+
from .compendium import external_premises as _external_premises
|
|
660
|
+
from .compendium import install as _install_compendium
|
|
661
|
+
_install_compendium(root)
|
|
662
|
+
kwargs.setdefault("known_premises", _external_premises(root))
|
|
663
|
+
rec = check(fn, claims=claims, declared=declared, **kwargs)
|
|
664
|
+
if key is None:
|
|
665
|
+
from .authoring import _fn_key
|
|
666
|
+
key = _fn_key(fn)
|
|
667
|
+
# The written claims_fingerprint must reflect everything check() just
|
|
668
|
+
# evaluated, the retrieved file-declared claims and the function's
|
|
669
|
+
# own decorator/docstring claims, not only the explicit `claims=`
|
|
670
|
+
# argument here. A fingerprint computed from a subset of what was
|
|
671
|
+
# actually checked would make `mathema verify` see a false "form
|
|
672
|
+
# changed" on the very next run (the claim set it merges independently
|
|
673
|
+
# wouldn't match this record's stamped fingerprint).
|
|
674
|
+
explicit = [declare(claim(c) if isinstance(c, str) else c) for c in (claims or [])]
|
|
675
|
+
merged = merge_entries(dict(declared), {"claims": explicit},
|
|
676
|
+
on_conflict="silent")
|
|
677
|
+
rec.spec_path = record(rec, key=key, root=root, claims=merged["claims"])
|
|
678
|
+
return rec
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def load_ipython_extension(ip):
|
|
682
|
+
"""`%load_ext mathema` then:
|
|
683
|
+
|
|
684
|
+
%%mathema
|
|
685
|
+
def my_fn(x: list, alpha: float) -> float:
|
|
686
|
+
...
|
|
687
|
+
|
|
688
|
+
runs the cell, then for every function it defines: writes its spec
|
|
689
|
+
(check + record to .mathema/verified/) and displays the result inline.
|
|
690
|
+
"""
|
|
691
|
+
import ast as _ast
|
|
692
|
+
|
|
693
|
+
from IPython.display import display
|
|
694
|
+
|
|
695
|
+
def mathema_cell(line, cell):
|
|
696
|
+
ip.run_cell(cell)
|
|
697
|
+
for node in _ast.parse(cell).body:
|
|
698
|
+
if isinstance(node, _ast.FunctionDef):
|
|
699
|
+
fn = ip.user_ns.get(node.name)
|
|
700
|
+
if callable(fn):
|
|
701
|
+
display(write_spec(fn))
|
|
702
|
+
|
|
703
|
+
ip.register_magic_function(mathema_cell, magic_kind="cell", magic_name="mathema")
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
_REGISTRY: dict = {}
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def track_claims(fn):
|
|
710
|
+
"""Optional bare tag: track this function's claims over time.
|
|
711
|
+
|
|
712
|
+
Entirely optional. Without it, everything still works: check, write_spec,
|
|
713
|
+
claims.check, and the CLI operate on any function. What an untracked
|
|
714
|
+
function loses is visibility in mathema.status(), the fresh-versus-stale
|
|
715
|
+
sweep, because status can only report on functions it knows exist. Tag
|
|
716
|
+
the functions whose claims you want watched; leave the rest alone.
|
|
717
|
+
|
|
718
|
+
Mechanics, precisely:
|
|
719
|
+
- runs once, at decoration time; the function object is returned
|
|
720
|
+
unchanged (no wrapper, zero call overhead, stack traces untouched)
|
|
721
|
+
- stamps ``fn.__mathema__ = {"key": ...}`` so tools and readers can see
|
|
722
|
+
the tag by introspection
|
|
723
|
+
- registers the function under its dotted key (module.qualname) in a
|
|
724
|
+
weak registry, so `mathema.tagged()` and `mathema.status()` can find
|
|
725
|
+
it; redefining the function in a notebook simply re-registers the key
|
|
726
|
+
The tag carries no spec content: intent and claims live in the sidecar
|
|
727
|
+
spec store (see `mathema.spec`), keeping code untouched by design.
|
|
728
|
+
"""
|
|
729
|
+
import weakref
|
|
730
|
+
|
|
731
|
+
from .authoring import _fn_key
|
|
732
|
+
key = _fn_key(fn)
|
|
733
|
+
fn.__mathema__ = {"key": key}
|
|
734
|
+
_REGISTRY[key] = weakref.ref(fn)
|
|
735
|
+
return fn
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def tagged() -> dict:
|
|
739
|
+
"""Currently-alive @track_claims-tagged functions, keyed by dotted name."""
|
|
740
|
+
out = {}
|
|
741
|
+
for key, ref in list(_REGISTRY.items()):
|
|
742
|
+
fn = ref()
|
|
743
|
+
if fn is None:
|
|
744
|
+
del _REGISTRY[key]
|
|
745
|
+
else:
|
|
746
|
+
out[key] = fn
|
|
747
|
+
return out
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def status(root: str = ".") -> str:
|
|
751
|
+
"""One line per tagged function: does a verified record exist, and is
|
|
752
|
+
it fresh (its recorded form hash matches the code as it is now)?
|
|
753
|
+
Freshness reads only the verified layer (.mathema/verified/), a declared
|
|
754
|
+
claims file never carries an identity hash, so it can't answer this."""
|
|
755
|
+
from .spec import load_verified
|
|
756
|
+
|
|
757
|
+
verified = load_verified(root)
|
|
758
|
+
lines = []
|
|
759
|
+
for key, fn in sorted(tagged().items()):
|
|
760
|
+
try:
|
|
761
|
+
current = analyze_source(fn).form
|
|
762
|
+
except SourceUnavailable:
|
|
763
|
+
current = None
|
|
764
|
+
rec = verified.get(key) or verified.get(fn.__name__)
|
|
765
|
+
if rec is None:
|
|
766
|
+
lines.append(f" ∅ {key}: no verified record")
|
|
767
|
+
continue
|
|
768
|
+
stored = (rec["entry"].get("identity") or {}).get("form")
|
|
769
|
+
state = ("fresh" if stored and stored == current
|
|
770
|
+
else "STALE (code changed since spec)" if stored else "no hash")
|
|
771
|
+
lines.append(f" {'✓' if state == 'fresh' else '✗'} {key}: {state} "
|
|
772
|
+
f"[{rec['source']}]")
|
|
773
|
+
return "mathema status\n" + ("\n".join(lines) if lines else
|
|
774
|
+
" (no @track_claims-tagged functions alive)")
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
@dataclass
|
|
778
|
+
class DocstringReport:
|
|
779
|
+
"""The result of `docstring_report()`: the same loose best-practice
|
|
780
|
+
checklist `mathema audit`'s `docs` column already scores
|
|
781
|
+
(`inventory.docstring_quality()`), returned as a single object so it
|
|
782
|
+
can be inspected or printed on its own for one function, e.g. in a
|
|
783
|
+
notebook, without running the full `audit` CLI over a package."""
|
|
784
|
+
name: str
|
|
785
|
+
quality: dict
|
|
786
|
+
|
|
787
|
+
def __repr__(self) -> str:
|
|
788
|
+
from .inventory import docs_checklist
|
|
789
|
+
|
|
790
|
+
q = self.quality
|
|
791
|
+
header = (f"mathema.DocstringReport({self.name}) · "
|
|
792
|
+
f"{q['score']}/{q['applicable']} criteria met")
|
|
793
|
+
return "\n".join([header] + [f" {line}" for line in docs_checklist(q)])
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def docstring_report(fn) -> DocstringReport:
|
|
797
|
+
"""Run the same loose docstring best-practice checklist `mathema
|
|
798
|
+
audit`'s `docs` column scores, against just this one function,
|
|
799
|
+
interactive/notebook-friendly, no package import or CLI needed.
|
|
800
|
+
|
|
801
|
+
mathema.docstring_report(my_fn)
|
|
802
|
+
"""
|
|
803
|
+
from .inventory import docstring_quality
|
|
804
|
+
|
|
805
|
+
return DocstringReport(name=getattr(fn, "__name__", "callable"),
|
|
806
|
+
quality=docstring_quality(fn))
|