fuaran-ui 0.7.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.
- fuaran_ui/__init__.py +113 -0
- fuaran_ui/ai_tools/__init__.py +86 -0
- fuaran_ui/ai_tools/dispatch.py +99 -0
- fuaran_ui/ai_tools/introspect.py +190 -0
- fuaran_ui/ai_tools/surface.py +194 -0
- fuaran_ui/canonical.py +216 -0
- fuaran_ui/charts/__init__.py +3391 -0
- fuaran_ui/cli/__init__.py +31 -0
- fuaran_ui/cli/core.py +387 -0
- fuaran_ui/client/__init__.py +77 -0
- fuaran_ui/client/client.py +284 -0
- fuaran_ui/client/contract.py +199 -0
- fuaran_ui/client/session.py +66 -0
- fuaran_ui/client/wire.py +250 -0
- fuaran_ui/compute/__init__.py +42 -0
- fuaran_ui/compute/evaluate.py +658 -0
- fuaran_ui/conformance/__init__.py +7 -0
- fuaran_ui/conformance/bridge.py +84 -0
- fuaran_ui/conformance/decoder_fuzz.py +1046 -0
- fuaran_ui/conformance/fuzz_exchange.py +209 -0
- fuaran_ui/conformance/harness.py +111 -0
- fuaran_ui/conformance/host_capability.py +366 -0
- fuaran_ui/conformance/refusal_report.py +123 -0
- fuaran_ui/dag.py +277 -0
- fuaran_ui/dataframe/__init__.py +149 -0
- fuaran_ui/dataframe/codec.py +1104 -0
- fuaran_ui/dataframe/evaluate.py +940 -0
- fuaran_ui/dataframe/model.py +428 -0
- fuaran_ui/elicitation.py +559 -0
- fuaran_ui/envelope.py +166 -0
- fuaran_ui/function.py +294 -0
- fuaran_ui/layout_observer/__init__.py +70 -0
- fuaran_ui/layout_observer/flags.py +260 -0
- fuaran_ui/layout_observer/observer.py +248 -0
- fuaran_ui/limits.py +136 -0
- fuaran_ui/merge.py +676 -0
- fuaran_ui/model.py +89 -0
- fuaran_ui/op_stream/__init__.py +140 -0
- fuaran_ui/op_stream/hash_chain.py +230 -0
- fuaran_ui/op_stream/in_memory_sink.py +144 -0
- fuaran_ui/op_stream/replay.py +375 -0
- fuaran_ui/op_stream/types.py +280 -0
- fuaran_ui/ops/__init__.py +72 -0
- fuaran_ui/ops/apply.py +1053 -0
- fuaran_ui/ops/decode.py +236 -0
- fuaran_ui/ops/diff.py +305 -0
- fuaran_ui/ops/encode.py +11 -0
- fuaran_ui/ops/placement.py +504 -0
- fuaran_ui/render_fidelity.py +561 -0
- fuaran_ui/renderer/__init__.py +152 -0
- fuaran_ui/renderer/bindings.py +873 -0
- fuaran_ui/renderer/content/fuaran-reference.css +4072 -0
- fuaran_ui/renderer/document.py +925 -0
- fuaran_ui/renderer/egress.py +596 -0
- fuaran_ui/renderer/email.py +1437 -0
- fuaran_ui/renderer/html.py +67 -0
- fuaran_ui/renderer/markdown.py +1211 -0
- fuaran_ui/renderer/notebook.py +406 -0
- fuaran_ui/renderer/projection.py +176 -0
- fuaran_ui/renderer/render.py +2793 -0
- fuaran_ui/renderer/sanitize.py +591 -0
- fuaran_ui/renderer/seeds.py +159 -0
- fuaran_ui/renderer/theme.py +263 -0
- fuaran_ui/result.py +71 -0
- fuaran_ui/runtime/__init__.py +22 -0
- fuaran_ui/runtime/runtime.py +259 -0
- fuaran_ui/runtime/sample.py +57 -0
- fuaran_ui/schema/__init__.py +15 -0
- fuaran_ui/schema/decode.py +4406 -0
- fuaran_ui/schema/encode.py +11 -0
- fuaran_ui/schema/types.py +4007 -0
- fuaran_ui/shapeguard.py +315 -0
- fuaran_ui/style_observer/__init__.py +129 -0
- fuaran_ui/style_observer/color.py +91 -0
- fuaran_ui/style_observer/flags.py +326 -0
- fuaran_ui/style_observer/manifest_flags.py +108 -0
- fuaran_ui/style_observer/observer.py +334 -0
- fuaran_ui/teleport.py +263 -0
- fuaran_ui/theme_manifest/__init__.py +91 -0
- fuaran_ui/theme_manifest/decode.py +174 -0
- fuaran_ui/theme_manifest/manifest.py +184 -0
- fuaran_ui/theme_manifest/project.py +173 -0
- fuaran_ui/ui/__init__.py +1663 -0
- fuaran_ui/ui/capability.py +242 -0
- fuaran_ui/ui/compute.py +759 -0
- fuaran_ui/ui/controls.py +313 -0
- fuaran_ui/ui/quick.py +381 -0
- fuaran_ui/validator/__init__.py +7 -0
- fuaran_ui/validator/validate.py +780 -0
- fuaran_ui-0.7.0.dist-info/METADATA +1335 -0
- fuaran_ui-0.7.0.dist-info/RECORD +94 -0
- fuaran_ui-0.7.0.dist-info/WHEEL +4 -0
- fuaran_ui-0.7.0.dist-info/entry_points.txt +2 -0
- fuaran_ui-0.7.0.dist-info/licenses/LICENSE +215 -0
fuaran_ui/__init__.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""fuaran-ui — a headless Python host of the Fuaran UI wire format.
|
|
2
|
+
|
|
3
|
+
A dependency-light, idiomatic-Python reference implementation of the canonical
|
|
4
|
+
Fuaran UI wire format (``WIRE_FORMAT.md``): decode / encode for the ``Node`` tree
|
|
5
|
+
and the ``TreeOp`` algebra, plus a pre-emit validator. It is a *sibling*
|
|
6
|
+
reference implementation built to the language-neutral spec + conformance
|
|
7
|
+
corpus, not a transpile of any other host.
|
|
8
|
+
|
|
9
|
+
Canonical imports::
|
|
10
|
+
|
|
11
|
+
from fuaran_ui import decode_node, encode_node, decode_op, encode_op
|
|
12
|
+
from fuaran_ui import decode_dag_record, encode_dag_record # branching op-stream
|
|
13
|
+
from fuaran_ui import Ok, Err, DecodeError
|
|
14
|
+
from fuaran_ui.renderer import render_html # optional headless renderer
|
|
15
|
+
from fuaran_ui.client import FuaranClient, FuaranSession # generation-endpoint client
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from .dag import DagOpRecord, DagResultEnvelope, decode_dag_record, encode_dag_record
|
|
21
|
+
from .function import (
|
|
22
|
+
EXACT,
|
|
23
|
+
SUBSUMES,
|
|
24
|
+
ComposePath,
|
|
25
|
+
FunctionEntry,
|
|
26
|
+
FunctionRegistry,
|
|
27
|
+
NoPath,
|
|
28
|
+
SigEntry,
|
|
29
|
+
Signature,
|
|
30
|
+
SignatureQuery,
|
|
31
|
+
function_entry,
|
|
32
|
+
slot_hole,
|
|
33
|
+
value_hole,
|
|
34
|
+
)
|
|
35
|
+
from .merge import (
|
|
36
|
+
MergeAuthor,
|
|
37
|
+
MergeConflict,
|
|
38
|
+
MergeConflicts,
|
|
39
|
+
MergeOk,
|
|
40
|
+
MergeResult,
|
|
41
|
+
MergeSide,
|
|
42
|
+
Primary,
|
|
43
|
+
Secondary,
|
|
44
|
+
encode_envelope,
|
|
45
|
+
merge3,
|
|
46
|
+
merge3_way_lenient,
|
|
47
|
+
merge3_way_with_author,
|
|
48
|
+
merge_3way,
|
|
49
|
+
)
|
|
50
|
+
from .model import Arr, Node, Obj, from_json
|
|
51
|
+
from .ops import decode_op, diff, encode_op
|
|
52
|
+
from .result import (
|
|
53
|
+
CODES,
|
|
54
|
+
DecodeError,
|
|
55
|
+
DecodeResult,
|
|
56
|
+
Err,
|
|
57
|
+
Ok,
|
|
58
|
+
)
|
|
59
|
+
from .schema import decode_node, encode_node
|
|
60
|
+
from .validator import Finding, validate_node
|
|
61
|
+
|
|
62
|
+
#: Distribution version. Kept in step with ``pyproject.toml``; the packaging-contract
|
|
63
|
+
#: test in ``tests/`` fails if the two disagree, so this is a pin rather than a copy.
|
|
64
|
+
__version__ = "0.7.0"
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"decode_node",
|
|
68
|
+
"encode_node",
|
|
69
|
+
"decode_op",
|
|
70
|
+
"encode_op",
|
|
71
|
+
"decode_dag_record",
|
|
72
|
+
"encode_dag_record",
|
|
73
|
+
"DagOpRecord",
|
|
74
|
+
"DagResultEnvelope",
|
|
75
|
+
"merge_3way",
|
|
76
|
+
"merge3",
|
|
77
|
+
"merge3_way_with_author",
|
|
78
|
+
"merge3_way_lenient",
|
|
79
|
+
"MergeOk",
|
|
80
|
+
"MergeConflicts",
|
|
81
|
+
"MergeConflict",
|
|
82
|
+
"MergeSide",
|
|
83
|
+
"MergeResult",
|
|
84
|
+
"encode_envelope",
|
|
85
|
+
"MergeAuthor",
|
|
86
|
+
"Primary",
|
|
87
|
+
"Secondary",
|
|
88
|
+
"diff",
|
|
89
|
+
"FunctionRegistry",
|
|
90
|
+
"FunctionEntry",
|
|
91
|
+
"SigEntry",
|
|
92
|
+
"Signature",
|
|
93
|
+
"SignatureQuery",
|
|
94
|
+
"ComposePath",
|
|
95
|
+
"NoPath",
|
|
96
|
+
"value_hole",
|
|
97
|
+
"slot_hole",
|
|
98
|
+
"function_entry",
|
|
99
|
+
"SUBSUMES",
|
|
100
|
+
"EXACT",
|
|
101
|
+
"validate_node",
|
|
102
|
+
"Finding",
|
|
103
|
+
"Node",
|
|
104
|
+
"Obj",
|
|
105
|
+
"Arr",
|
|
106
|
+
"from_json",
|
|
107
|
+
"Ok",
|
|
108
|
+
"Err",
|
|
109
|
+
"DecodeError",
|
|
110
|
+
"DecodeResult",
|
|
111
|
+
"CODES",
|
|
112
|
+
"__version__",
|
|
113
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""``fuaran_ui.ai_tools`` — the AI-tools introspection surface.
|
|
2
|
+
|
|
3
|
+
The runtime introspection an orchestrator/agent needs to drive the Fuaran tree
|
|
4
|
+
natively from Python (the language AI agents are written in), over the Phase 234
|
|
5
|
+
headless codec and typed surface. Three parts, all provider-agnostic and
|
|
6
|
+
standard-library-only:
|
|
7
|
+
|
|
8
|
+
* :mod:`~fuaran_ui.ai_tools.surface` — the **emittable-surface catalog**: what an
|
|
9
|
+
agent may emit (recognised kinds / bindings / actions / text-sources), the
|
|
10
|
+
bounded **value-space** projections, provider-agnostic **tool/function schemas**
|
|
11
|
+
it registers, and :func:`~fuaran_ui.ai_tools.surface.validate_emission` to check
|
|
12
|
+
an emission against the typed surface.
|
|
13
|
+
* :mod:`~fuaran_ui.ai_tools.introspect` — read-only **tree introspection**: kind,
|
|
14
|
+
bound binding slots (with their canonical wire-form expression), and structure.
|
|
15
|
+
* :mod:`~fuaran_ui.ai_tools.dispatch` — the **default-deny-by-shape dispatch gate**
|
|
16
|
+
(FGP 3): an effect shape the host has not permitted is refused.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .dispatch import (
|
|
22
|
+
GATED_EFFECT_SHAPES,
|
|
23
|
+
INERT_SHAPES,
|
|
24
|
+
DispatchDecision,
|
|
25
|
+
DispatchGate,
|
|
26
|
+
is_gated_effect,
|
|
27
|
+
)
|
|
28
|
+
from .introspect import (
|
|
29
|
+
BindingSlot,
|
|
30
|
+
NodeIntrospection,
|
|
31
|
+
TreeIntrospection,
|
|
32
|
+
binding_expression,
|
|
33
|
+
binding_slots,
|
|
34
|
+
child_ids,
|
|
35
|
+
child_nodes,
|
|
36
|
+
find_node,
|
|
37
|
+
inspect_tree,
|
|
38
|
+
kind_name,
|
|
39
|
+
node_state,
|
|
40
|
+
walk_nodes,
|
|
41
|
+
)
|
|
42
|
+
from .surface import (
|
|
43
|
+
ValidationResult,
|
|
44
|
+
action_cases,
|
|
45
|
+
binding_cases,
|
|
46
|
+
describe_surface,
|
|
47
|
+
emit_tool_schema,
|
|
48
|
+
emittable_kinds,
|
|
49
|
+
text_source_cases,
|
|
50
|
+
tool_schemas,
|
|
51
|
+
validate_emission,
|
|
52
|
+
value_space,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
# surface
|
|
57
|
+
"emittable_kinds",
|
|
58
|
+
"binding_cases",
|
|
59
|
+
"action_cases",
|
|
60
|
+
"text_source_cases",
|
|
61
|
+
"value_space",
|
|
62
|
+
"describe_surface",
|
|
63
|
+
"validate_emission",
|
|
64
|
+
"ValidationResult",
|
|
65
|
+
"tool_schemas",
|
|
66
|
+
"emit_tool_schema",
|
|
67
|
+
# introspect
|
|
68
|
+
"kind_name",
|
|
69
|
+
"binding_expression",
|
|
70
|
+
"binding_slots",
|
|
71
|
+
"child_nodes",
|
|
72
|
+
"child_ids",
|
|
73
|
+
"walk_nodes",
|
|
74
|
+
"find_node",
|
|
75
|
+
"node_state",
|
|
76
|
+
"inspect_tree",
|
|
77
|
+
"BindingSlot",
|
|
78
|
+
"NodeIntrospection",
|
|
79
|
+
"TreeIntrospection",
|
|
80
|
+
# dispatch
|
|
81
|
+
"DispatchGate",
|
|
82
|
+
"DispatchDecision",
|
|
83
|
+
"is_gated_effect",
|
|
84
|
+
"GATED_EFFECT_SHAPES",
|
|
85
|
+
"INERT_SHAPES",
|
|
86
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Default-deny-by-shape dispatch gate (FGP 3).
|
|
2
|
+
|
|
3
|
+
An agent driving the tree proposes ``Action`` effects; the host decides whether
|
|
4
|
+
each may run. This is the policy gate the introspection/dispatch path consults
|
|
5
|
+
**by shape** — the action's wire discriminator (`$type`) — before any effectful
|
|
6
|
+
action is dispatched. It is **default-deny**: an effect shape the host has not
|
|
7
|
+
explicitly permitted is refused, so a new or unexpected effect can never fire by
|
|
8
|
+
omission (the same posture as the reference tier's runtime dispatch gate and the
|
|
9
|
+
capability seam's default-deny-by-shape validation).
|
|
10
|
+
|
|
11
|
+
The gate is a *policy* only — it never executes an action (a Python host executes
|
|
12
|
+
the permitted effect downstream). It classifies an action's shape and returns an
|
|
13
|
+
allow/deny decision with a reason.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
from ..model import Obj
|
|
21
|
+
from ..schema.decode import ACTION_CASES
|
|
22
|
+
|
|
23
|
+
# Effectful shapes that reach outside the pure state-update loop — they require
|
|
24
|
+
# explicit host permission (mirrors the reference runtime's gated set:
|
|
25
|
+
# call/navigate/ai-tool/read-file-body, plus the host-effect notify / clipboard /
|
|
26
|
+
# capability-invoke). An effect not in this set is still default-denied unless
|
|
27
|
+
# permitted; this set is what a host reasons about when granting.
|
|
28
|
+
GATED_EFFECT_SHAPES = frozenset(
|
|
29
|
+
{"Dispatch", "Navigate", "AiTool", "ReadFileBody", "Notify", "WriteToClipboard", "Invoke", "Call"}
|
|
30
|
+
)
|
|
31
|
+
# `Call` joined the set in 0.3.0. It was named FIRST in the comment above from the
|
|
32
|
+
# day this module was written ("call/navigate/ai-tool/read-file-body") and absent
|
|
33
|
+
# from the set itself — a transcription slip rather than a policy, and one nothing
|
|
34
|
+
# caught because `authorize` default-denies an unclassified shape anyway, so the
|
|
35
|
+
# gate's VERDICT was already right. What was wrong is what a host reasoning with
|
|
36
|
+
# `is_gated_effect` was told: that an HTTP call to a host endpoint is not an
|
|
37
|
+
# outward effect it must decide about. A permission model is only as good as the
|
|
38
|
+
# question it is asked, and that one was being asked wrong.
|
|
39
|
+
|
|
40
|
+
# Structural, side-effect-free composition — safe to permit broadly. ``Chain`` is
|
|
41
|
+
# a sequence (its members are gated individually); ``SetState`` mutates only the
|
|
42
|
+
# local MVU state.
|
|
43
|
+
INERT_SHAPES = frozenset({"Chain", "SetState"})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class DispatchDecision:
|
|
48
|
+
"""The gate's verdict for one action shape."""
|
|
49
|
+
|
|
50
|
+
shape: str
|
|
51
|
+
allowed: bool
|
|
52
|
+
reason: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_gated_effect(shape: str) -> bool:
|
|
56
|
+
"""Whether a shape is an outward/host effect that must be explicitly permitted."""
|
|
57
|
+
return shape in GATED_EFFECT_SHAPES
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class DispatchGate:
|
|
62
|
+
"""A default-deny-by-shape policy gate.
|
|
63
|
+
|
|
64
|
+
``DispatchGate()`` denies every action shape. A host opts shapes in explicitly
|
|
65
|
+
with :meth:`permitting`; :meth:`permissive_inert` grants only the side-effect-free
|
|
66
|
+
structural shapes (``Chain`` / ``SetState``), leaving every outward effect denied.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
allowed: frozenset[str] = field(default_factory=frozenset)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def deny_all(cls) -> DispatchGate:
|
|
73
|
+
return cls(frozenset())
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def permitting(cls, *shapes: str) -> DispatchGate:
|
|
77
|
+
return cls(frozenset(shapes))
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def permissive_inert(cls) -> DispatchGate:
|
|
81
|
+
"""Permit only the inert structural shapes; every gated effect stays denied."""
|
|
82
|
+
return cls(frozenset(INERT_SHAPES))
|
|
83
|
+
|
|
84
|
+
def with_permitted(self, *shapes: str) -> DispatchGate:
|
|
85
|
+
return DispatchGate(self.allowed | frozenset(shapes))
|
|
86
|
+
|
|
87
|
+
def authorize_shape(self, shape: str) -> DispatchDecision:
|
|
88
|
+
"""The verdict for a bare action-shape string (default-deny)."""
|
|
89
|
+
if shape not in ACTION_CASES:
|
|
90
|
+
return DispatchDecision(shape, False, f"unknown action shape {shape!r}")
|
|
91
|
+
if shape in self.allowed:
|
|
92
|
+
return DispatchDecision(shape, True, "explicitly permitted")
|
|
93
|
+
return DispatchDecision(shape, False, "default-deny by shape (not permitted)")
|
|
94
|
+
|
|
95
|
+
def authorize(self, action: Obj) -> DispatchDecision:
|
|
96
|
+
"""The verdict for a decoded ``Action`` object (reads its ``$type`` tag)."""
|
|
97
|
+
if not isinstance(action, Obj) or action.tag is None:
|
|
98
|
+
return DispatchDecision("<malformed>", False, "not a discriminated action object")
|
|
99
|
+
return self.authorize_shape(action.tag)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Tree introspection — walk a decoded ``Node`` tree and report each node's kind,
|
|
2
|
+
its bound binding slots (with the canonical wire-form expression), and its
|
|
3
|
+
structural children.
|
|
4
|
+
|
|
5
|
+
The Python analogue of the reference tier's read-only introspection surface: the
|
|
6
|
+
*static* view an agent uses to inspect a tree it (or the model) emitted — kind
|
|
7
|
+
discriminator, which slots are data-bound and how (`$state.<key>` / `$queries.<name>`
|
|
8
|
+
/ …), and the structural shape. Value resolution against a live host + geometry
|
|
9
|
+
probing are a renderer-side concern and are deliberately out of scope here (this
|
|
10
|
+
is the source-side static surface, matching the reference tier's split).
|
|
11
|
+
|
|
12
|
+
It walks the generic decoded model (``Node`` / ``Obj`` / ``Arr``) rather than a
|
|
13
|
+
per-kind table, so a new ``NodeKind`` needs no change here — a bound slot is any
|
|
14
|
+
field whose value is a ``Binding``-tagged object, discovered structurally.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
from ..model import Arr, Node, Obj
|
|
22
|
+
from ..model import Value as WireValue
|
|
23
|
+
from ..schema.decode import BINDING_CASES
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class BindingSlot:
|
|
28
|
+
"""One data-bound slot on a node: its dotted field path within the kind, the
|
|
29
|
+
``Binding`` case that produced it, and the canonical wire-form expression."""
|
|
30
|
+
|
|
31
|
+
slot: str
|
|
32
|
+
source: str
|
|
33
|
+
expression: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class NodeIntrospection:
|
|
38
|
+
"""The per-node introspection envelope."""
|
|
39
|
+
|
|
40
|
+
id: str
|
|
41
|
+
kind: str
|
|
42
|
+
bindings: tuple[BindingSlot, ...]
|
|
43
|
+
child_ids: tuple[str, ...]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class TreeIntrospection:
|
|
48
|
+
"""A recursive structural snapshot of a whole tree."""
|
|
49
|
+
|
|
50
|
+
id: str
|
|
51
|
+
kind: str
|
|
52
|
+
bindings: tuple[BindingSlot, ...]
|
|
53
|
+
child_ids: tuple[str, ...]
|
|
54
|
+
children: tuple[TreeIntrospection, ...]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def kind_name(node: Node) -> str:
|
|
58
|
+
"""The wire discriminator for a node's kind (the ``$type`` tag) — e.g. ``Box``,
|
|
59
|
+
``Metric``, ``DataGrid``."""
|
|
60
|
+
return node.kind.tag or "<untagged>"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def binding_expression(binding: Obj) -> tuple[str, str]:
|
|
64
|
+
"""Classify a ``Binding``-tagged object into ``(source, expression)`` — the
|
|
65
|
+
canonical wire-form accessor the reference tier reports (`$state.<key>`, …)."""
|
|
66
|
+
tag = binding.tag or ""
|
|
67
|
+
fields = binding.fields
|
|
68
|
+
if tag == "Static":
|
|
69
|
+
return ("Static", "$static")
|
|
70
|
+
if tag == "Query":
|
|
71
|
+
return ("Query", f"$queries.{fields.get('name', '')}")
|
|
72
|
+
if tag == "Filter":
|
|
73
|
+
return ("Filter", f"$filters.{fields.get('name', '')}")
|
|
74
|
+
if tag == "Selection":
|
|
75
|
+
return ("Selection", f"$selection.{fields.get('nodeId', fields.get('sourceId', ''))}")
|
|
76
|
+
if tag == "State":
|
|
77
|
+
return ("State", f"$state.{fields.get('key', '')}")
|
|
78
|
+
if tag == "I18n":
|
|
79
|
+
return ("I18n", f"$i18n.{fields.get('key', '')}")
|
|
80
|
+
if tag == "Local":
|
|
81
|
+
return ("Computed", "$local")
|
|
82
|
+
if tag == "Format":
|
|
83
|
+
return ("Computed", "$format")
|
|
84
|
+
if tag == "Transform":
|
|
85
|
+
return ("Computed", "$transform")
|
|
86
|
+
if tag == "Invoke":
|
|
87
|
+
return ("Computed", "$invoke")
|
|
88
|
+
# Computed + any future value-computing case.
|
|
89
|
+
return ("Computed", "$computed")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _is_binding(value: WireValue) -> bool:
|
|
93
|
+
return isinstance(value, Obj) and value.tag in BINDING_CASES
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def binding_slots(node: Node) -> tuple[BindingSlot, ...]:
|
|
97
|
+
"""Every data-bound slot in the node's kind, in document order, with a dotted
|
|
98
|
+
field path (e.g. ``source``, ``spec.activeIndex``). Recurses through record
|
|
99
|
+
objects + arrays but **not** into a nested ``Node`` (those belong to the child)."""
|
|
100
|
+
slots: list[BindingSlot] = []
|
|
101
|
+
|
|
102
|
+
def walk(value: WireValue, path: str) -> None:
|
|
103
|
+
if isinstance(value, Node):
|
|
104
|
+
return # a child node's bindings are the child's, not this node's
|
|
105
|
+
if isinstance(value, Obj):
|
|
106
|
+
if _is_binding(value):
|
|
107
|
+
source, expr = binding_expression(value)
|
|
108
|
+
slots.append(BindingSlot(slot=path, source=source, expression=expr))
|
|
109
|
+
return
|
|
110
|
+
for name, field_value in value.fields.items():
|
|
111
|
+
walk(field_value, f"{path}.{name}" if path else name)
|
|
112
|
+
elif isinstance(value, Arr):
|
|
113
|
+
for i, item in enumerate(value.items):
|
|
114
|
+
walk(item, f"{path}[{i}]")
|
|
115
|
+
|
|
116
|
+
for name, field_value in node.kind.fields.items():
|
|
117
|
+
walk(field_value, name)
|
|
118
|
+
return tuple(slots)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def child_nodes(node: Node) -> tuple[Node, ...]:
|
|
122
|
+
"""The immediate structural child nodes — every ``Node`` reachable within the
|
|
123
|
+
kind without passing through another ``Node`` (so grandchildren are excluded)."""
|
|
124
|
+
children: list[Node] = []
|
|
125
|
+
|
|
126
|
+
def walk(value: WireValue) -> None:
|
|
127
|
+
if isinstance(value, Node):
|
|
128
|
+
children.append(value) # stop — its own children are its concern
|
|
129
|
+
elif isinstance(value, Obj):
|
|
130
|
+
for field_value in value.fields.values():
|
|
131
|
+
walk(field_value)
|
|
132
|
+
elif isinstance(value, Arr):
|
|
133
|
+
for item in value.items:
|
|
134
|
+
walk(item)
|
|
135
|
+
|
|
136
|
+
for field_value in node.kind.fields.values():
|
|
137
|
+
walk(field_value)
|
|
138
|
+
return tuple(children)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def child_ids(node: Node) -> tuple[str, ...]:
|
|
142
|
+
return tuple(c.id for c in child_nodes(node))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def walk_nodes(tree: Node) -> list[Node]:
|
|
146
|
+
"""Depth-first walk of every node in the tree, root first."""
|
|
147
|
+
acc: list[Node] = []
|
|
148
|
+
|
|
149
|
+
def visit(node: Node) -> None:
|
|
150
|
+
acc.append(node)
|
|
151
|
+
for child in child_nodes(node):
|
|
152
|
+
visit(child)
|
|
153
|
+
|
|
154
|
+
visit(tree)
|
|
155
|
+
return acc
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def find_node(tree: Node, node_id: str) -> Node | None:
|
|
159
|
+
"""The first node with ``node_id`` (depth-first), or ``None``."""
|
|
160
|
+
for node in walk_nodes(tree):
|
|
161
|
+
if node.id == node_id:
|
|
162
|
+
return node
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _introspect(node: Node) -> NodeIntrospection:
|
|
167
|
+
return NodeIntrospection(
|
|
168
|
+
id=node.id,
|
|
169
|
+
kind=kind_name(node),
|
|
170
|
+
bindings=binding_slots(node),
|
|
171
|
+
child_ids=child_ids(node),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def node_state(tree: Node, node_id: str) -> NodeIntrospection | None:
|
|
176
|
+
"""The introspection envelope for a single node by id, or ``None``."""
|
|
177
|
+
node = find_node(tree, node_id)
|
|
178
|
+
return None if node is None else _introspect(node)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def inspect_tree(tree: Node) -> TreeIntrospection:
|
|
182
|
+
"""A recursive structural snapshot of the whole tree."""
|
|
183
|
+
base = _introspect(tree)
|
|
184
|
+
return TreeIntrospection(
|
|
185
|
+
id=base.id,
|
|
186
|
+
kind=base.kind,
|
|
187
|
+
bindings=base.bindings,
|
|
188
|
+
child_ids=base.child_ids,
|
|
189
|
+
children=tuple(inspect_tree(c) for c in child_nodes(tree)),
|
|
190
|
+
)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""The emittable-surface catalog + provider-agnostic tool schemas.
|
|
2
|
+
|
|
3
|
+
So a Python agent can **discover what it may emit** (the recognised ``NodeKind`` /
|
|
4
|
+
``Binding`` / ``Action`` / ``TextSource`` cases and the bounded value-space
|
|
5
|
+
projections) and **validate its intent** against the typed surface, instead of
|
|
6
|
+
blind-emitting JSON. The catalog is derived from the codec's own recognised-case
|
|
7
|
+
sets + the bounded ``Literal`` enums, so it can never drift from what the decoder
|
|
8
|
+
accepts.
|
|
9
|
+
|
|
10
|
+
The tool schemas are **provider-agnostic** function/tool definitions (``name`` +
|
|
11
|
+
``description`` + JSON-Schema ``parameters``) an agent registers with any provider.
|
|
12
|
+
The primary constrained-emission tool takes the caller-supplied canonical wire
|
|
13
|
+
schema (``schema.json``) as its input schema, so the package stays corpus-free and
|
|
14
|
+
standard-library-only at import time.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import typing
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from ..result import Err
|
|
24
|
+
from ..schema import types as t
|
|
25
|
+
from ..schema.decode import ACTION_CASES, BINDING_CASES, KNOWN_KINDS, TEXT_SOURCE_CASES, decode_node
|
|
26
|
+
|
|
27
|
+
# The bounded value-space projections — each bare-string ``Literal`` enum an agent
|
|
28
|
+
# must pick within (WIRE_FORMAT §3.5). Derived from the typed surface via
|
|
29
|
+
# ``typing.get_args`` so the projection cannot drift from the codec.
|
|
30
|
+
_VALUE_SPACE_ENUMS: dict[str, Any] = {
|
|
31
|
+
"Tone": t.Tone,
|
|
32
|
+
"Weight": t.Weight,
|
|
33
|
+
"Emphasis": t.Emphasis,
|
|
34
|
+
"Orientation": t.Orientation,
|
|
35
|
+
"BadgeVariant": t.BadgeVariant,
|
|
36
|
+
"HeadingVariant": t.HeadingVariant,
|
|
37
|
+
"ButtonVariant": t.ButtonVariant,
|
|
38
|
+
"ChartKind": t.ChartKind,
|
|
39
|
+
"StyleRole": t.StyleRole,
|
|
40
|
+
"FontVoice": t.FontVoice,
|
|
41
|
+
"LiveRegion": t.LiveRegion,
|
|
42
|
+
"ImageVariant": t.ImageVariant,
|
|
43
|
+
# fuaran#1077 — the three presentation token vocabularies. Offered as value
|
|
44
|
+
# spaces precisely because they are CLOSED: an agent handed the projection
|
|
45
|
+
# cannot reach for a CSS ratio the decoder refuses.
|
|
46
|
+
"ImageFit": t.ImageFit,
|
|
47
|
+
"ImageAspect": t.ImageAspect,
|
|
48
|
+
"ImageLoading": t.ImageLoading,
|
|
49
|
+
"ScrollOrientation": t.ScrollOrientation,
|
|
50
|
+
"DateVariant": t.DateVariant,
|
|
51
|
+
"MathDisplay": t.MathDisplay,
|
|
52
|
+
# fuaran#867 — the two-case polarity enum. `Neutral` is reserved and is
|
|
53
|
+
# deliberately absent from the alias, so an agent offered this projection
|
|
54
|
+
# cannot pick a case the decoder refuses.
|
|
55
|
+
"TrendPolarity": t.TrendPolarity,
|
|
56
|
+
"BoxRole": t.BoxRole,
|
|
57
|
+
"FileReadEncoding": t.FileReadEncoding,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def emittable_kinds() -> list[str]:
|
|
62
|
+
"""The recognised ``NodeKind`` discriminators an agent may emit."""
|
|
63
|
+
return sorted(KNOWN_KINDS)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def binding_cases() -> list[str]:
|
|
67
|
+
"""The recognised ``Binding`` value-source discriminators."""
|
|
68
|
+
return sorted(BINDING_CASES)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def action_cases() -> list[str]:
|
|
72
|
+
"""The recognised ``Action`` effect discriminators."""
|
|
73
|
+
return sorted(ACTION_CASES)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def text_source_cases() -> list[str]:
|
|
77
|
+
"""The recognised ``TextSource`` discriminators."""
|
|
78
|
+
return sorted(TEXT_SOURCE_CASES)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def value_space() -> dict[str, list[str]]:
|
|
82
|
+
"""Each bounded ``Literal`` enum → its allowed values (the within-bounds
|
|
83
|
+
choices an agent picks from), derived from the typed surface."""
|
|
84
|
+
return {name: list(typing.get_args(alias)) for name, alias in _VALUE_SPACE_ENUMS.items()}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def describe_surface() -> dict[str, Any]:
|
|
88
|
+
"""A machine-readable snapshot of the whole emittable surface — the payload the
|
|
89
|
+
``list_surface`` tool returns to an agent."""
|
|
90
|
+
return {
|
|
91
|
+
"kinds": emittable_kinds(),
|
|
92
|
+
"bindings": binding_cases(),
|
|
93
|
+
"actions": action_cases(),
|
|
94
|
+
"textSources": text_source_cases(),
|
|
95
|
+
"valueSpace": value_space(),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ── Validation of an agent's emission against the typed surface ───────────────
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class ValidationResult:
|
|
104
|
+
"""The verdict for a candidate wire tree: ``ok`` plus the canonical decode
|
|
105
|
+
error (code / path / message) when it is rejected."""
|
|
106
|
+
|
|
107
|
+
ok: bool
|
|
108
|
+
code: str | None = None
|
|
109
|
+
path: str | None = None
|
|
110
|
+
message: str | None = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_emission(wire_json: str) -> ValidationResult:
|
|
114
|
+
"""Validate an agent's candidate ``Node`` wire JSON against the typed surface via
|
|
115
|
+
the Phase 234 codec — the canonical accept/reject decision + error location, so
|
|
116
|
+
the agent repairs intent instead of shipping malformed JSON downstream."""
|
|
117
|
+
result = decode_node(wire_json)
|
|
118
|
+
if isinstance(result, Err):
|
|
119
|
+
e = result.error
|
|
120
|
+
return ValidationResult(ok=False, code=e.code, path=e.path, message=e.message)
|
|
121
|
+
return ValidationResult(ok=True)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ── Provider-agnostic tool/function-call schemas ─────────────────────────────
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def emit_tool_schema(wire_schema: dict[str, Any]) -> dict[str, Any]:
|
|
128
|
+
"""The constrained-emission tool: emit a canonical Fuaran wire tree. Takes the
|
|
129
|
+
caller-supplied canonical wire schema (``wire-format-fixtures/schema.json``) as
|
|
130
|
+
its ``parameters`` so a provider constrains generation to schema-valid wire.
|
|
131
|
+
Passed in (not bundled) so this package stays corpus-free + stdlib-only."""
|
|
132
|
+
return {
|
|
133
|
+
"name": "fuaran_emit_tree",
|
|
134
|
+
"description": (
|
|
135
|
+
"Emit a Fuaran UI as a canonical wire-format JSON tree (a Node). The tree "
|
|
136
|
+
"renders on any conformant host. Emit only recognised kinds/bindings/actions "
|
|
137
|
+
"and pick bounded enum values within their value-space (call fuaran_list_surface)."
|
|
138
|
+
),
|
|
139
|
+
"parameters": wire_schema,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def tool_schemas() -> list[dict[str, Any]]:
|
|
144
|
+
"""The provider-agnostic discovery/validation tool definitions an agent registers.
|
|
145
|
+
(The constrained-emission tool is :func:`emit_tool_schema` — it needs the wire
|
|
146
|
+
schema passed in.) Each is a neutral ``{name, description, parameters}`` object
|
|
147
|
+
usable with any provider's function-calling API."""
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
"name": "fuaran_list_surface",
|
|
151
|
+
"description": (
|
|
152
|
+
"List the emittable Fuaran surface: recognised NodeKind / Binding / Action / "
|
|
153
|
+
"TextSource discriminators and the bounded value-space (enum -> allowed values). "
|
|
154
|
+
"Call before emitting to stay within the typed surface."
|
|
155
|
+
),
|
|
156
|
+
"parameters": {"type": "object", "properties": {}, "additionalProperties": False},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"name": "fuaran_validate_tree",
|
|
160
|
+
"description": (
|
|
161
|
+
"Validate a candidate Fuaran wire tree against the typed surface. Returns ok, or "
|
|
162
|
+
"the canonical decode error code + path so the emission can be repaired."
|
|
163
|
+
),
|
|
164
|
+
"parameters": {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"properties": {
|
|
167
|
+
"tree": {
|
|
168
|
+
"type": "string",
|
|
169
|
+
"description": "The candidate Node wire JSON to validate.",
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"required": ["tree"],
|
|
173
|
+
"additionalProperties": False,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "fuaran_inspect_tree",
|
|
178
|
+
"description": (
|
|
179
|
+
"Introspect an emitted Fuaran wire tree: report each node's id, kind, bound "
|
|
180
|
+
"binding slots (with their $state/$queries/... expression), and structural children."
|
|
181
|
+
),
|
|
182
|
+
"parameters": {
|
|
183
|
+
"type": "object",
|
|
184
|
+
"properties": {
|
|
185
|
+
"tree": {
|
|
186
|
+
"type": "string",
|
|
187
|
+
"description": "The Node wire JSON to introspect.",
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"required": ["tree"],
|
|
191
|
+
"additionalProperties": False,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
]
|