an.capabilities#
Capabilities: what an asset, an engine or the environment affords, and the one matcher.
ADR 0002 (capability-based applicability, with a universal default) and the core
study §2.13 (three subjects). This is the LOWER layer of the pair: it knows
capabilities, analysers, requirements and substitution records, and nothing
about vocabulary entries, methods or aspects — an.semantic imports it,
never the reverse, and neither imports an.ir at module level.
A capability is a named, possibly parametrised fact (
limbs.legs,face.mouthwith chartrhubarb9,env.latex). Names use one dotted grammar, are registered with a description and a remedy (what would add it, and the command where one exists), and are persisted identifiers: the asset library’s facets and the substitution records store them. A capability belongs to one subject:asset,engineorenvironment.An analyser derives a subject’s capabilities — a profile,
{capability: params}— from what is there (a descriptor and its art, a renderer’s implemented members, the tools onPATH), never from a list typed beside it (ADR 0002 decision 2). It is versioned: a stored snapshot made by an older analyser is recomputed (current_affordances()). Declared facts used instead of deriving are listed under theoverridesparam, so the derivation reports which overrides it used.A requirement is a predicate over a profile, spelled as a string. The whole grammar (consult on P7, §3):
cap(afforded),cap:key(keyamong the capability’skeys),cap>=N(itscountparam — else its number ofkeys— is at leastN) anda|b(any of). No callables: a requirement must name its remedy, render into the generated docs and the MCP surface, and diff when a method’s version bumps.missing()is THE matcher: the requirement terms a profile (or aSubjectstriple) does not meet, in the order asked.why_not,applicableandresolve(inan.semantic) and the library’sfind(…, near=True)are all calls to it.A
Substitutionrecords that a method other than the requested one was used (generalising the compiled document’sasset_resolution).policychoices are information;missingandnoopare warnings that--strict-assetsmakes fatal (FATAL_REASONS).
>>> profile = {"swap.view": {"keys": ["front", "side"]}, "limbs.legs": {}}
>>> matches(profile, "swap.view:side"), matches(profile, "swap.view:back"), matches(profile, "limbs.legs")
(True, False, True)
>>> missing(profile, ["limbs.legs", "face.mouth|face.jaw", "swap.view>=3"])
['face.mouth|face.jaw', 'swap.view>=3']
Module Attributes
The three subjects a capability can belong to (core study §2.13). |
|
Separates a capability from one of its keys in a term ( |
|
The params entry listing the discrete values a capability affords. |
|
The params entry listing the declared facts the derivation used instead of deriving. |
|
|
|
Registered capabilities, by name. |
|
Registered analysers, by kind. |
|
The reasons |
Functions
|
|
|
|
|
The art an asset folder holds, as an analyser reads it: |
|
The registered capability names, sorted; filtered by |
|
|
|
The stored snapshot when its analyser version is current, else a fresh derivation. |
|
Whether |
|
The requirement terms |
|
A |
|
Register an analyser. |
|
Register a capability (or a |
|
What would add the capability a requirement term asks for. |
|
The subject whose profile a requirement term is matched against. |
Classes
|
The derivation of one kind's profile, versioned. |
|
A registered capability: its name, what it means, and how to add it. |
|
One requirement term: a capability, optionally a key or a count, or a disjunction. |
|
The three profiles a requirement can be matched against. |
|
One recorded substitution: the method asked for, the one used, and why. |
Exceptions
A capability, analyser or requirement is malformed, or collides with one registered. |
- an.capabilities.ANALYSERS: dict[str, Analyser] = {'engine': Analyser(kind='engine', version='3', subject='engine', declares=()), 'environment': Analyser(kind='environment', version='2', subject='environment', declares=())}#
Registered analysers, by kind.
- class an.capabilities.Analyser(kind, version, derive, subject='asset', declares=(), overrides=None)[source]#
Bases:
objectThe derivation of one kind’s profile, versioned.
kindis an asset kind (character) or a subject that has one analyser (engine,environment). Bumpversionwhenever the output can change for the same input: snapshots made under the old one are then recomputed on read. The version is NOT a compile input (consult §5): the derived profile is, so touching an analyser without changing its output re-renders nothing.- declares: tuple[str, ...] = ()#
The document’s declared facts the derivation honours instead of deriving (
rest_view,face_overlay) or reads as a request (gait): reported bydescribe_asset(ADR 0002 decision 2).
- overrides: Callable[[Any, Mapping[str, Any]], Iterable[str]] | None = None#
(doc, art) -> declared field namesthe derivation USED as overrides that do not show on an afforded capability’s params: a declared fact that REMOVES a capability (a character’soccluded, an#381). Those on an afforded capability are already in itsoverridesparam.None: none beyond those. Reporting only: it changes no profile, so adding it does not bumpversion.
- an.capabilities.CAPABILITIES: dict[str, Capability] = {'engine.compile': Capability(name='engine.compile', description='the engine implements the optional `compile` member', remedy='use an engine that implements `compile`', subject='engine', command=None, version='1'), 'engine.measure_duration': Capability(name='engine.measure_duration', description='the engine implements the optional `measure_duration` member', remedy='use an engine that implements `measure_duration`', subject='engine', command=None, version='1'), 'engine.paint_order': Capability(name='engine.paint_order', description="the engine paints parts in an order that is not their tree's (keys: `container`, reorder one container's items; `global`, one paint list across containers)", remedy="render the shot with an engine that declares that paint order (the stage does), or give the rig a draw order its tree already paints (`nesting: flat`, or each chain's parts together in draw order)", subject='engine', command=None, version='1'), 'engine.preview': Capability(name='engine.preview', description='the engine implements the optional `preview` member', remedy='use an engine that implements `preview`', subject='engine', command=None, version='1'), 'engine.render': Capability(name='engine.render', description='the engine renders shots (keys: the Shot.renderer values it claims)', remedy="register a renderer that claims the shot's `renderer` (an.adapters.register_renderer)", subject='engine', command=None, version='1'), 'engine.render_frames': Capability(name='engine.render_frames', description='the engine implements the optional `render_frames` member', remedy='use an engine that implements `render_frames`', subject='engine', command=None, version='1'), 'engine.seek': Capability(name='engine.seek', description='the engine implements the optional `seek` member', remedy='use an engine that implements `seek`', subject='engine', command=None, version='1'), 'env.browser': Capability(name='env.browser', description='Playwright with a Chromium build, which the stage engine renders in', remedy="pip install 'an[cutout]' && playwright install chromium", subject='environment', command=None, version='1'), 'env.ffmpeg': Capability(name='env.ffmpeg', description='`ffmpeg` is on PATH', remedy='install ffmpeg (`brew install ffmpeg` on macOS, `apt install ffmpeg` on Debian)', subject='environment', command=None, version='1'), 'env.key.anthropic': Capability(name='env.key.anthropic', description='the ANTHROPIC_API_KEY environment variable is set (its value is never read)', remedy='set ANTHROPIC_API_KEY (needed by `an iterate` and the vision verifier)', subject='environment', command=None, version='1'), 'env.key.elevenlabs': Capability(name='env.key.elevenlabs', description='the ELEVEN_API_KEY environment variable is set (its value is never read)', remedy='set ELEVEN_API_KEY (needed by the ElevenLabs voices)', subject='environment', command=None, version='1'), 'env.latex': Capability(name='env.latex', description='`latex` is on PATH', remedy='install a TeX distribution (MacTeX / TeX Live) so `latex` is on PATH', subject='environment', command=None, version='1'), 'env.manim': Capability(name='env.manim', description='the manim, manimkit Python package(s) are importable', remedy="pip install 'an[manim]' (Manim Community Edition and manimkit; on Linux first `apt install libcairo2-dev libpango1.0-dev`)", subject='environment', command=None, version='1'), 'env.node': Capability(name='env.node', description='`node` is on PATH', remedy='install Node.js (`brew install node`)', subject='environment', command=None, version='1'), 'env.rhubarb': Capability(name='env.rhubarb', description='`rhubarb` is on PATH', remedy='install Rhubarb Lip Sync (`brew install rhubarb-lipsync`)', subject='environment', command=None, version='1'), 'rig.hierarchy': Capability(name='rig.hierarchy', description="the rig nests its parts in chains (`nesting: bones`): keys = the chain roots, count = the deepest chain's length in parts", remedy="declare `nesting: bones` on the rig document and parent each part's bone to the bone it hangs from (an elbow's to the shoulder's)", subject='asset', command=None, version='1'), 'space.framing2d': Capability(name='space.framing2d', description='the engine lowers moves through the framing2d view space: a 2D framing of a flat picture: position, zoom (log), roll (angle)', remedy='render with an engine that lowers the framing2d view space', subject='engine', command=None, version='1'), 'space.orbit3d': Capability(name='space.orbit3d', description='the engine lowers moves through the orbit3d view space: an orbit camera around a 3D target: azimuth and elevation (angles), distance (log)', remedy='render with an engine that lowers the orbit3d view space', subject='engine', command=None, version='1')}#
Registered capabilities, by name.
- class an.capabilities.Capability(name, description, remedy, subject='asset', command=None, version='1')[source]#
Bases:
objectA registered capability: its name, what it means, and how to add it.
commandis the CLI that adds it, when one exists (an character add-views);versionbumps when the meaning of the name changes (a persisted name is never redefined in place).
- exception an.capabilities.CapabilityError[source]#
Bases:
ValueErrorA capability, analyser or requirement is malformed, or collides with one registered.
- an.capabilities.FATAL_REASONS: frozenset[str] = frozenset({'missing', 'noop'})#
The reasons
--strict-assetsmakes fatal (a test pins this set).
- an.capabilities.KEYS_PARAM: str = 'keys'#
The params entry listing the discrete values a capability affords.
- an.capabilities.KEY_SEP: str = ':'#
Separates a capability from one of its keys in a term (
swap.view:side).
- an.capabilities.OVERRIDES_PARAM: str = 'overrides'#
The params entry listing the declared facts the derivation used instead of deriving.
- an.capabilities.Profile#
{capability: params}— what an analyser derives, and the matcher’s only input.
- class an.capabilities.Requirement(capability='', key=None, at_least=None, any_of=())[source]#
Bases:
objectOne requirement term: a capability, optionally a key or a count, or a disjunction.
>>> str(parse_requirement("swap.view:side")), str(parse_requirement("rig.slots>=2")) ('swap.view:side', 'rig.slots>=2') >>> [str(r) for r in parse_requirement("face.eyes|face.brows").any_of] ['face.eyes', 'face.brows']
- an.capabilities.SUBJECTS: tuple[str, ...] = ('asset', 'engine', 'environment')#
The three subjects a capability can belong to (core study §2.13).
- class an.capabilities.Subjects(asset=<factory>, engine=<factory>, environment=<factory>)[source]#
Bases:
objectThe three profiles a requirement can be matched against.
A bare profile passed where
Subjectsis expected is the asset’s.>>> Subjects.of({"limbs.legs": {}}).asset {'limbs.legs': {}}
- class an.capabilities.Substitution(aspect, entity, requested, chosen, reason, requested_version=None, chosen_version='', missing=(), remedies=<factory>)[source]#
Bases:
objectOne recorded substitution: the method asked for, the one used, and why.
It generalises the compiled document’s
asset_resolution(ADR 0002 decision 6): a requested method replaced by a default is said out loud, and fatal under--strict-assetsunless it was a policy choice.>>> s = Substitution("locomotion", "bob", requested="loco.hem_sway", chosen="loco.rock", ... reason="missing", missing=("limbs.legs",)) >>> s.fatal, s.sentence() (True, "bob: locomotion 'loco.hem_sway' does not apply (missing limbs.legs); used 'loco.rock'")
- an.capabilities.affordances(asset, art=None, *, kind='character')[source]#
affordances(asset)(ADR 0002 decision 2): whatassetaffords, derived.assetis the document (a mapping, or a pydantic model, dumped to JSON first);artthe files present,{relative path: ref}. The kind’s registered analyser decides; with none, nothing is afforded.
- an.capabilities.analyse(kind, doc, art=None)[source]#
(profile, analysers)of one subject: its capabilities and the analyser versions used.A kind with no registered analyser affords nothing derived and records no analyser — an honest empty answer, not a guess.
>>> analyse("no-such-kind", {}) ({}, {})
- an.capabilities.art_in_dir(directory, *, exclude=())[source]#
The art an asset folder holds, as an analyser reads it:
{relative path: True}.excludedrops the document files themselves (character.json).>>> import tempfile, pathlib >>> with tempfile.TemporaryDirectory() as d: ... _ = (pathlib.Path(d) / "parts").mkdir(); _ = (pathlib.Path(d) / "parts" / "head.svg").write_text("<svg/>", encoding="utf-8") ... sorted(art_in_dir(d)) ['parts/head.svg']
- an.capabilities.capability_names(*, subject=None, owner=None)[source]#
The registered capability names, sorted; filtered by
subject/owner.
- an.capabilities.capability_of(query)[source]#
(capability, key)of a query term (the library’sfindform).>>> capability_of("swap.view:side"), capability_of("limbs.legs") (('swap.view', 'side'), ('limbs.legs', None))
- an.capabilities.current_affordances(kind, doc, art, *, stored, stored_analysers)[source]#
The stored snapshot when its analyser version is current, else a fresh derivation.
Affordances are derived data, so recomputing them is always safe; trusting a snapshot made by an older analyser is what would make a facet lie.
- an.capabilities.matches(profile, term)[source]#
Whether
profile(or the subject oftermin aSubjects) meetsterm.- Return type:
- an.capabilities.missing(profile, requires)[source]#
The requirement terms
profiledoes not meet, spelled, in the order asked.THE matcher (ADR 0002 decision 4):
why_not,applicable,resolveand the library’sfind(…, near=True)all call it.>>> missing({}, ["env.ffmpeg"]), missing(Subjects(environment={"env.ffmpeg": {}}), ["env.ffmpeg"]) (['env.ffmpeg'], [])
- an.capabilities.parse_requirement(spec)[source]#
A
Requirementfrom its spelling (cap,cap:key,cap>=N,a|b).- Return type:
>>> parse_requirement("rig.slots>=2").at_least 2 >>> parse_requirement("limbs.legs(hips)") Traceback (most recent call last): ... an.capabilities.CapabilityError: requirement 'limbs.legs(hips)': ...
- an.capabilities.register_analyser(kind, *, version='', subject='asset', owner='an')[source]#
Register an analyser. Two forms.
register_analyser(Analyser(...), owner=...)registers the object and returns it (a genre’sanalysersfield goes this way). With akindstring it is a decorator:@register_analyser("character", version="0.1.0")registers the decorated derivation.- Return type:
- an.capabilities.register_capability(name, *, description='', remedy='', subject='asset', command=None, version='1', owner='an')[source]#
Register a capability (or a
Capability). Returns it.Re-registering the same definition is a no-op; a different definition under a name another owner holds raises, because capability names are persisted and two meanings for one name would make a stored facet lie.
- Return type:
>>> cap = register_capability("demo.thing", description="a thing", remedy="add one", owner="demo") >>> CAPABILITIES["demo.thing"] is cap True >>> _ = drop_owner("demo")
- an.capabilities.remedy_for(term)[source]#
What would add the capability a requirement term asks for.
- Return type:
>>> remedy_for("no.such.capability") 'no registered capability no.such.capability; see vocabulary() for the known names'
- an.capabilities.subject_of(term)[source]#
The subject whose profile a requirement term is matched against.
An unregistered capability is matched against the asset (and reported missing there):
an.semantic.check_registry()is what refuses it.- Return type:
str
>>> subject_of("env.latex") == subject_of("limbs.legs") False
Modules
The core's two non-asset subjects: the engine (a renderer) and the environment. |