Reasoning Modules — Reference

Version: alpha · API 1. This is the per-module lookup: the kinds each published module declares, the fields you write, the fields it derives for you, and the queries you can call. The Reasoning Modules Tutorial teaches how to use one; this page is what you consult once you are using it. This document as Markdown: dke-reasoning-modules-ref.md — the same text this page is rendered from, for readers and tools that would rather have the source than the page.

Module semantics in general — how import resolves, how namespacing works, what re-submitting does — are in the DKE Python Reference §9. This page covers what the individual libraries contain.


1. What a module declares

Four kinds of declaration, all of which become part of your store the moment you import it:

Declaration What it is
Classes the kinds of thing the domain has, and the fields they carry
Derived fields values the engine works out from the ones you wrote, and keeps current as those change — you never write one
Standing rules conclusions that hold exactly while their condition holds over your data, and withdraw themselves when it stops
Queries defs you call by name, which answer over whatever facts you have loaded

Names are scoped to the module: code.Function and finance.Function are different kinds and never read each other’s data.

Derived fields and rule conclusions carry a derived provenance rather than a source, so you can always tell a conclusion from something you were told.


2. code — reasoning about a codebase

code reasons about a codebase: import it, load facts about your own source, and ask.

Version 1.12.0. Download it at dke.langsyn.com/source/code/latest/code.dpy, which always serves the current release, or name the version itself at https://dke.langsyn.com/source/code/1.12.0/code.dpy. A versioned URL says exactly which bytes you read and serves them until the next release, which replaces the previous version rather than keeping it — so latest is the link that goes on lasting, and the copy you have already downloaded is yours either way. A version never changes without its content changing, so the version you hold answers whether you are current without your having to diff anything.

2.1 Kinds and the fields you write

Kind Fields you write
code.Function name, module, lines, branches, loops, deprecated, test, called, returns_ignored, language, callers, callees, explained
code.Class name, module, methods, language
code.Call caller, callee, site
code.UnresolvedCall caller, site, text
code.Rationale subject, marker, text, site
code.Import importer, imported, root
code.Dependency name, version, direct (absent when transitive), pinned
code.Counterpart name, left, right, relation, left_covers, right_covers

Every query that answers “which functions” returns subjects, never display names — the same identity code.Call.caller holds and callers_of(fn) takes. So an answer from one query feeds straight into another, and name stays what it is: an optional label for reading, not an identity.

Three fields decide whether the queries work at all:

  • code.Call.caller and code.Call.callee hold the subject you recorded a Function under, not its display name. The queries compare them directly, so display names make every function look uncalled, with nothing to tell you why.
  • code.Function.test names a test that reaches the function, and untested reads its absence. Write it only where a test really does reach, or the rule falls silent everywhere instead of where it should be.
  • code.Function.called says something in this codebase calls this function, and uncalled reads its absence — the same shape as test. Record it on the CALLEE, next to the Call you record for the same edge. That looks redundant and is not: a rule ranges over one kind, so “does any Call name me?” is a question nothing can conclude from. Recorded here, the absence is a fact about this function and uncalled is an ordinary rule. Recording the Call alone is not enough — write both, or every function you recorded comes back uncalled however many callers the store holds.

code.Function.callers and code.callees are degree — how many distinct functions call this one, and how many it calls. Distinct neighbours, not call sites: a caller that calls you three times is one dependency, not three, and counting sites would rank a function by how chatty its callers happen to be.

They are recorded rather than counted for you, for the same reason called is — and unlike called, write them for every function, zero included. Those above are read by their absence, so a value everywhere silences the rule. callers is read by comparison, and a function nothing calls has a fan-in of zero, which is a fact rather than a gap. Optional and absent-tolerant: record neither and everything else answers as before, with hub simply never concluding.

code.UnresolvedCall is the opposite record: a call you saw and could not name a target for. It asserts no edge, deliberately — a missing edge is a known gap, and a guessed one is a wrong answer no query could tell from a fact. Write caller as the subject of the enclosing function (as on Call), site as the location to look at, and text as the callee exactly as it appears in the source. site is what makes an entry worth having; a subject without one is skipped. This kind is optional and absent-tolerant — see §2.5 for what it buys you.

code.Rationale is a reason the author wrote down, attached to what it explains. Every other kind here is structure — something a reader could recompute from your source. This is the part that cannot be recomputed: why the structure is the way it is.

It is stated, never inferred, for the reason Counterpart is: intent is a claim only a reader can make, and an inferred one produces facts you cannot tell from the real ones. Write subject as the subject of the Function or Class it explains, marker as which convention it was, text as what the author wrote, and site as where to read it in full. text is what makes an entry worth having; a subject without one is skipped.

Which comments count is your producer’s decision, exactly as what counts as a branch is — and the same warning applies in the same direction. A convention wide enough to sweep ordinary comments turns every line in your tree into a fact and rationale_for into noise. Choose something an author had to type on purpose. Ours records WHY: and NOTE: at the start of a comment, uppercase, and nothing else.

code.Function.explained says a reason exists, and unexplained_critical reads its absence — the same shape as test and called. Write it only where a reason really was recorded, or the rule falls silent everywhere instead of firing.

language says what a fact is aboutpython, cpp, rust, whatever you call it. It is optional, and a store that never records it works exactly as before. Record it once your store holds more than one language: module is a path, so it hints at the language at best and no query can rely on it, and without language there is no way to ask about one half of a repository. It also tells two producers apart, which matters because branches is whatever your analysis counted, and two producers can both be right on different scales while complexity compares them against one threshold either way.

code.Counterpart records two functions that have to agree — an encoder and its decoder, a writer and its reader, two siblings that ought to guard their inputs alike. Nothing in a call graph says two functions are halves of one contract; that is a claim about intent, and only you know it. State the pair, name in relation what is being counted, and give the count each side handles. left_covers and right_covers count members of that one named set, which makes them comparable to each other and to nothing else.

2.2 Derived and maintained for you

Field Meaning
code.Function.complexity one path, plus one per branch and per loop — see §2.3
code.Function.hotspot complexity above 10
code.Function.oversized more than 200 lines
code.Function.untested has lines recorded, and no test was seen to reach it
code.Function.uncalled has lines recorded, and nothing was seen to call it
code.Function.hub more than 5 distinct functions call it
code.Function.critical_untested complexity above 10, and no test was seen to reach it
code.Function.unexplained_critical the same, and no reason was recorded either
code.Function.discards_result throws away at least one result it was handed
code.Class.sprawling more than 20 methods
code.Counterpart.gap / reverse_gap how far apart the two sides are, each direction as a positive number
code.Counterpart.left_stricter the left side handles more — the right accepts what the left refuses
code.Counterpart.right_stricter the same, the other way round

2.3 How to count branches and loops

hotspot fires at a complexity above 10. A fixed threshold over a number you compute yourself only means something if we agree on how it is computed, so here is the rule. Follow it and your numbers are comparable — to your own past numbers, to another producer’s, and to the threshold.

complexity is one, plus one for each decision the function makes:

Count one for Notes
each if (and each else if / elif) else counts nothing
each loop for, while, do, and each clause of a comprehension
each case arm that tests a catch-all default / case _ counts nothing
each exception handler one per except / catch clause
each short-circuit boolean and / or, && / \|\|
each conditional expression the a ? b : c / b if a else c form

else, default and a bare case _ are where control lands when none of the decisions above them fired, so they add nothing — three arms and a catch-all is three, not four.

Two scope rules decide what belongs to which function:

  • a named nested function is its own Function, with its own counts; its decisions are not charged to the function that contains it;
  • an anonymous inline function — a lambda, a closure, a block — counts toward the function containing it. It is part of that function’s text and cannot be called from anywhere else, so there is no other function for its decisions to belong to.

The list is closed. Anything not on it counts nothing, including constructs with a fair claim to being a decision. An assert is the clearest one: it is a conditional failure, and most complexity tools count it — but in some languages it is a macro or a compiled-out call that a reader working from source cannot reliably see, and a rule that some producers can follow and others cannot is the problem this rule exists to solve. A number everyone computes the same way is worth more than a number that is arguably more faithful.

If you want a different scale — a stricter threshold, a metric of your own — define your own module named code.

2.4 Queries

Call Answers
code.callers_of(fn) everything that calls fn, directly
code.callees_of(fn) everything fn calls, directly
code.impact_of(fn) what breaks if fn changes — callers, and theirs, to three hops
code.importers_of(module) every module importing module
code.uncalled() functions with no recorded caller
code.unused_dependencies() dependencies you declared that nothing imports
code.unpinned_dependencies() dependencies declared as a range, not an exact version
code.asymmetries() pairs whose two sides do not handle the same amount, and which way
code.discarded_results() functions that throw a result away, so a failure cannot reach the caller
code.critical() complicated and unreached — where a defect is likeliest to survive
code.unexplained() critical() with everything already accounted for taken out
code.rationale_for(fn) why fn is the way it is, in the author’s own words
code.functions_in(language) every function recorded as belonging to one language
code.hotspots_in(language) the complicated ones in one language
code.hubs() the most depended-on functions, most first — what breaks most if you touch it
code.dispatch_sites() every call whose target was not determined — where reading stopped

2.5 What the answers can and cannot mean

The queries are only as good as the data, and for a codebase that has consequences worth stating plainly.

A call graph derived from source is a lower bound. Python decides calls as it runs, so anything reading source sees the calls the source makes plain and no others — a call through a variable, a handler table or getattr is not visible. Every edge you record that way is real; edges are missing. That matters most for uncalled(): a function reached only dynamically appears there. Read it as candidates to look at, never a list to delete.

How loose that bound is, is measurable. Record an UnresolvedCall wherever your producer sees a call and cannot name the target, and dispatch_sites() lists them: the location, and the callee as written. Read it beside uncalled() — any of those sites could be the call that a function on that list is reached by. A codebase with two dispatch sites and one with two hundred deserve very different trust in the same uncalled() answer, and this is what tells them apart. Nothing on the list is a defect by itself: a plugin table, a callback and a decorator all work exactly this way. It is a measure of how much of your call graph is decidable by reading.

Recording none of them is fine. The kind is absent-tolerant — everything else answers as before, and dispatch_sites() returns nothing.

untested inherits that bound, since reaching a function is worked out over the same graph — and reaching is not executing, because a branch may never be taken. Read it the same way.

critical() cannot tell deliberate from overlooked, and unexplained() can. Two functions can be identical in every structural respect — same complexity, neither reached by a test — and be entirely different objects: one whose author wrote down why it is shaped that way and why it is not tested, and one that says nothing. critical() returns both, because everything it reads is structure. Record Rationale and explained and unexplained() is the same list with the accounted-for taken out. A short unexplained() under a long critical() is a good result: the risk is known and written down rather than absent. Record none of it and unexplained() is simply critical() again, which is where you were.

There is no dependency age, and that is the schema being honest rather than an omission. Python’s package metadata carries no release date, so an age could only come from asking an index — which means sending a dependency list to a third party. A field nothing can honestly fill is worse than no field, because it reads as a capability.

unused_dependencies() matches by name, so a distribution whose import name differs from its package name (Pillow is imported as PIL) will look unused.

A subject missing the field a query reads is skipped, not fatal. You are never obliged to fill every field of a kind, and one half-written subject does not take out the answer for the complete ones beside it — a Dependency with no name, a Counterpart with no relation, an UnresolvedCall with no site and a Rationale with no text are each passed over, and everything else answers as it would have. So a partial record costs you that record and nothing more.


3. How you get one, and what holds once you have it

You download the .dpy file and compile it into your own store, once. From then on import <name> resolves it there like any other module, and nothing in your store distinguishes it from one you wrote.

Modules are served from dke.langsyn.com, under /source/<name>/<version>/<name>.dpy, with /source/<name>/latest/ forwarding to the current release, which replaces the previous version at that path rather than joining it. They are MIT, they are plain text, and you can read one in a browser before deciding to compile it.

  1. You can read it before you run it. It is a file in the published language. Every class, rule and query this page documents is a line you can see, and the tables below are a lookup, not a substitute for the source.
  2. It is yours once compiled. forget_rule removes one of its rules, forget_module removes the whole module, and nothing puts either back — your store holds what you compiled into it.
  3. Editing it is expected. If a rule does not suit your data, change the line and compile again. What you get is then genuinely yours, and this page describes the version you started from rather than the one you are running.
  4. Its interface is enumerable in the store. list_modules reports it, info_module reports the scripts it defines and the classes it declares, list reports its scripts with their signatures, and list rules its standing rules by name. A module’s SOURCE is not stored — the store keeps the compiled form — so the file you downloaded is the copy to keep.
  5. The version tells you whether you are current. The version stated for each module below never changes without its content changing, so the version you hold answers the question directly.

A published module’s name is a default, not a reserved word: compile your own module named code and yours is what import code resolves to.


4. What it costs

Nothing beyond what you already pay. Reasoning modules are not a separate subscription: importing one, and everything it then does, is billed by the same per-operation meter as any other work you send. A module that reasons more costs more, in proportion, and one you never call costs nothing.


Companion documents: the Reasoning Modules Tutorial, and the DKE Python Reference for the language these modules are written in.