Capability IR v1 — normative specification
This specification defines the internal source-analysis contract, independently of
Apizr's package version. Its empirical input is the
legacy behavior contract, recorded at
a386f507000852ab19e8239f6cbc8b4b3b8d249a (246 tests, 65.95% branch-aware coverage).
Legacy ambiguities are evidence for this design, not compatibility requirements for
this new domain. The existing generation pipeline remains unchanged.
Python / Notebook
|
+------------+------------+
| |
Legacy pipeline Capability Analyzer
| |
CodeAnalyzr JSON Capability IR v1
| |
FastAPI / Docker FUTURE
/ | \
REST MCP Runtime
Only the right-hand analyzer and IR exist in this change. A capability is a lexically observed module-level function contract, not a promise that importing, binding, invoking or serializing it will succeed. Classes, nested functions, repository resolution, generators consuming the IR, effect inference, runtime execution, sandboxing, plugins, MCP, AI and attestation implementations are outside v1's scope. Generator definitions are represented, but never invoked.
Document, identity and source
The required schema identifier is apizr.capability/v1. A document contains one
source, capabilities and diagnostics. Models are frozen, typed, and reject unknown
fields and inconsistent identities. Collections are tuples. The canonicalizer
sorts capabilities by ID and diagnostics by source line, code and message;
signatures, decorators and overloads retain source order. Duplicate IDs are
invalid even when constructing a document directly.
IDs are python:<module>:<qualified-symbol>. Module names are explicit dotted
Python identifiers (NFKC normalized, no keywords); symbols are AST-normalized
identifiers. V1 admits module-level functions only, so qualified symbol equals
name. IDs never depend on files, paths, whitespace or comments. Inspection of a
file requires the caller to supply its logical module name; no filename inference
or project import resolution occurs.
Source kind is python or notebook. Source locations contain logical module,
symbol, one-based definition line and inclusive end line. No absolute paths,
output paths, timestamps, hostnames, random IDs or working directories are emitted.
Python source digest is SHA-256 of exact original bytes, including comments,
encoding declarations, BOMs and line endings. Byte input is decoded using Python's
PEP 263 encoding detection. String input means its UTF-8 encoding, without further
normalization. Invalid encoding, syntax or logical module names raise ordinary
input exceptions; they do not produce partial IR documents.
Notebook inspection reads original bytes once, hashes them, and uses the existing non-executing NotebookTransformr exporter through a separate adapter outside the core package. A second digest hashes the exact UTF-8 Python text returned by that exporter, before formatting or analysis. Locations refer to that transformed text. Exporter/dependency versions can change transformation output; the second digest makes this observable. Non-code cells have no function contracts. Notebook magics remain rejected by the characterized adapter. No kernel is started.
Function and signature contracts
A capability includes ID, name, qualified name, kind function, source span,
optional docstring, execution form, signature, decorators, overload contracts,
availability, effects and evidence. Missing docstrings remain JSON null; no prose
is generated. Docstrings use Python AST's cleaned docstring convention.
Execution forms are sync, async, generator, async_generator. Yield detection
is confined to the function's own scope (not nested functions, lambdas or classes).
This is a syntactic observation, never a transport or runtime support guarantee.
Parameters retain Python binding order, name, kind (positional_only,
positional_or_keyword, keyword_only), required flag, optional declared type and
optional default expression. Null default-expression means no default; the
string None is a present default. Required must agree with default presence.
Defaults are normalized AST expressions and are never evaluated. Annotation and
default are independent: x: int = None differs from x: int | None = None.
Variadic implementations and variadic overload contracts are rejected with an
error rather than truncated to fixed signatures. Return annotation is separate,
and enforcement is always none; neither this analyzer nor the legacy runtime
adds enforcement based on this representation.
Every annotation retains declared: ast.unparse of its parsed expression.
Normalization discards lexical whitespace, comments, redundant parentheses and
quote style, while preserving literal values, actual forward-reference names,
operands, nested members and metadata expressions. No name resolution or alias
evaluation occurs. Forms classify syntax only: name, attribute, subscript,
union, forward_reference, literal, or unknown. Optional, Literal,
Annotated, Callable, collection types and arbitrary libraries' generics all
retain their complete subscript expression. A syntactic union is not proof that
an overloaded __or__ implements typing semantics. Unsupported structural forms
remain unknown with the complete declaration and an informational diagnostic.
The v1 canonical expression contract is pinned by the golden and cross-version
tests for supported Python syntax; new Python syntax is parsed by the running
interpreter and is not promised to parse on older interpreters.
Evidence, effects and availability
Evidence is reusable: declared, observed, inferred, unknown. Names,
locations and execution syntax are observed. Annotations/defaults/decorators are
declared. Future static heuristics must be inferred, not promoted to declared or
observed runtime facts. V1 has no AI-origin category.
Effects have explicit tri-state values true, false, unknown and evidence.
Filesystem read/write, network, environment, subprocess, state mutation, secrets
and external service access are all unknown with unknown evidence. Unknown is
not false. The analyzer performs no effect inference, even for an empty body.
The typed effect record is an extension point for future optional categories.
Availability is unconditional (observed direct module statement) or unknown.
Any control-flow scope (if, loops, try, with, match, etc.) makes it unknown
and produces a warning, even if True or if False: no reachability evaluation.
An ordinary decorator also makes availability unknown because it can replace the
binding; its complete expression is preserved without interpreting semantics.
An unconditional definition is still not a runtime guarantee: earlier top-level
exceptions, imports, subsequent rebinding, dynamic namespace operations or a
failing default can prevent or replace the binding. Binding/dataflow resolution is
explicitly deferred. All contracts are observations, not runnable entry points.
Duplicates, overloads and diagnostics
Two or more concrete definitions of one symbol are ambiguous: emit ERROR 001 and omit that symbol's capability. Do not select the first or last definition.
Recognize typing.overload and imported aliases only when a direct, preceding
module-level import uniquely binds the decorator root throughout module scope.
Recognize bare overload without such evidence as ambiguous, not as executable.
Rebindings/conditional imports make a matching overload marker ambiguous. Accept
an overload group only when all its declarations and its single implementation
are unconditional consecutive module statements, declarations precede the
implementation, their sync/async nature agrees, and the marker is the only stub
decorator. Preserve those signatures as declared contracts in source order. No
overload dispatch or signature compatibility solving is implemented. Stub-only,
conditional, reordered, interleaved or multiply decorated groups produce ERROR
004 and no capability. Unsupported variadics produce ERROR 002 and no capability.
Duplicate concrete implementations still produce ERROR 001 independently.
Diagnostics use stable enums for code and severity (info, warning, error),
a message and logical source span. They do not embed paths or source snippets.
| Code | Meaning | Severity |
|---|---|---|
| APIZR-CAP-001 | Duplicate concrete symbol | error |
| APIZR-CAP-002 | Variadic signature unsupported | error |
| APIZR-CAP-003 | Control-flow definition availability uncertain | warning |
| APIZR-CAP-004 | Overload association ambiguous | error |
| APIZR-CAP-005 | Decorator binding uncertain or lambda assignment unsupported | warning |
| APIZR-CAP-006 | Type structure unknown; declaration retained | info |
Methods and nested scopes are intentionally skipped without per-member noise. Historical issues about those features remain open. Lambda assignments are not function-definition contracts and receive 005 when directly assigned to a name.
Canonical bytes and digests
canonical_bytes(document) serializes the validated model in JSON mode, including
explicit nulls and defaults, with lexicographically sorted object keys, UTF-8
(ensure_ascii=False), compact separators , and :, no NaN, and exactly one
terminal LF. Collections follow the ordering above. No YAML representation is
canonical. Documents loaded from canonical JSON round-trip to identical bytes.
document_digest(document) returns a typed SHA-256 digest of those bytes, with
lowercase 64-character hex value. That digest is external, never embedded in its
own input. Source digests and document digests are separate concepts; formatting
changes can change both source digest and source locations without changing IDs.
Capability IR is an attestable artifact, not an attestation format.
Independent tools can hash or attest the artifact. There are no signatures, receipts, SLSA fields or dependencies on an attestation product.
API, version policy and limitations
from apizr.capabilities import (
canonical_bytes,
document_digest,
inspect_file,
inspect_source,
)
ir = inspect_source(b"def calculate(x: int) -> int: return x\n", module_name="pricing")
artifact = canonical_bytes(ir)
digest = document_digest(ir)
# inspect_file(Path('pricing.py'), module_name='pricing') reads exact file bytes.
Notebook entry point: apizr.capability_notebooks.inspect_notebook(path,
module_name=...). Core imports only stdlib and existing Pydantic; the notebook
adapter owns the optional exporter boundary. Inspection performs no user imports,
execution, network requests or subprocesses. File APIs read only the explicitly
supplied input. In-memory inspection does not access the filesystem. Static
inspection is not a resource sandbox: pathological input can exhaust parser
limits. Importing/running analyzed or generated code requires trusted input.
V1 readers must continue to understand v1 documents; meaning cannot silently
change. Breaking semantics require v2. Because v1 validation rejects extra fields,
additive optional fields require coordinated reader upgrades and a compatibility
review before emission; they are not automatically backward compatible. Package
releases do not rename the schema. The generated
JSON Schema and single golden document
are tested against the models/serializer. Compatibility CI retains the global 55% coverage floor and additionally requires
90% branch-aware coverage for apizr.capabilities.
The existing CLI stays unchanged; a
subcommand design and all IR-consuming generators are deferred.
Known limits include lexical binding/reachability, unsupported variadics and classes, no type/alias/runtime resolution, conservative overload association, no runtime argument/return enforcement, and syntax availability tied to the parser's Python version. These are explicit boundaries rather than inferred safety claims.
Problem tracking: #30 covers information loss for independent consumers; #31 tracks the deferred gap between lexical discovery and callable availability. The historical duplicate behavior in #28 remains open for the legacy pipeline.