uipilot 0.1.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.
uipilot/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ # uipilot — app-agnostic UI-flow engine.
2
+ #
3
+ # Loads a per-app *pack* (structured YAML + a small config), walks the UI flow
4
+ # graph, substitutes params, and emits Playwright-MCP-executable scripts. The
5
+ # engine ships no domain vocabulary: everything project-specific (apps, elements,
6
+ # actions, flows, risk taxonomy, auth capabilities) lives in a pack.
7
+ #
8
+ # The code is split into four layers, with dependencies pointing strictly inward
9
+ # (presentation -> application -> {domain, infrastructure} -> domain):
10
+ #
11
+ # domain/ pure model + business rules (no I/O, no frameworks)
12
+ # infrastructure/ YAML loading, dynamic imports, file I/O
13
+ # application/ use-case orchestration
14
+ # presentation/ the `uipilot` CLI and output renderers
15
+ #
16
+ # There is intentionally no code here: this file only marks the directory as a
17
+ # package. Import concrete symbols from their owning module, e.g.
18
+ # from uipilot.infrastructure.pack_loader import load_pack
19
+ # from uipilot.application.service import open_pack
20
+ # from uipilot.domain.model import Action, Flow, Selector
21
+ #
22
+ # The distribution version is declared in pyproject.toml (single source).
@@ -0,0 +1,11 @@
1
+ # Application layer — use-case orchestration.
2
+ #
3
+ # Wires infrastructure (loading, importing, capability resolution) to domain
4
+ # services (compile, validate, verify, pathfind, usage) and returns domain
5
+ # objects or plain data — never rendered strings. Imported by presentation;
6
+ # imports domain + infrastructure.
7
+ #
8
+ # Components:
9
+ # service PackContext plus one function per use case: open_pack,
10
+ # compile_script, validate_pack, verify, route, uses,
11
+ # filter_actions/filter_elements, list_capabilities, import_markdown.
@@ -0,0 +1,220 @@
1
+ """Application layer — use-case orchestration.
2
+
3
+ Each function here is one use case the presentation layer invokes. This is the
4
+ only place that wires **infrastructure** (loading a pack, importing markdown,
5
+ resolving capabilities) to **domain** services (compile, validate, verify,
6
+ pathfind, usage). It returns domain objects / plain data — never rendered
7
+ strings, so any front-end (CLI now, an HTTP API later) can format them.
8
+
9
+ Dependency direction: application → {domain, infrastructure}. It is imported by
10
+ presentation and never imports it.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+ from uipilot.domain import compiler, graph, usage, validation, verification
21
+ from uipilot.domain.compiler import CompiledScript
22
+ from uipilot.domain.flows import expand_invocations
23
+ from uipilot.domain.model import Action, Element, Pack
24
+ from uipilot.domain.templating import RuntimeContext
25
+ from uipilot.domain.validation import ValidationReport
26
+ from uipilot.infrastructure.capabilities import CapabilityRegistry
27
+ from uipilot.infrastructure.markdown_importer import import_md, write_seed
28
+ from uipilot.infrastructure.pack_loader import load_pack
29
+ from uipilot.infrastructure.scaffold import init_project as _init_project
30
+
31
+
32
+ @dataclass
33
+ class PackContext:
34
+ """A loaded pack plus its per-run resolution context."""
35
+
36
+ pack: Pack
37
+ runtime: RuntimeContext
38
+
39
+
40
+ def open_pack(path: str | Path, env: Optional[dict] = None) -> PackContext:
41
+ """Load a pack and build its runtime context (env-bound token resolution)."""
42
+ pack = load_pack(path)
43
+ runtime = RuntimeContext(pack.config, env=dict(os.environ if env is None else env))
44
+ return PackContext(pack=pack, runtime=runtime)
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Queries
49
+ # ---------------------------------------------------------------------------
50
+
51
+
52
+ def filter_actions(pctx: PackContext, *, app: Optional[str] = None,
53
+ section: Optional[str] = None, risk: Optional[str] = None,
54
+ grep: Optional[str] = None, transport: Optional[str] = None) -> list[Action]:
55
+ pack = pctx.pack
56
+ out: list[Action] = []
57
+ for a in pack.actions.values():
58
+ if app and a.app != app:
59
+ continue
60
+ if risk and a.risk != risk:
61
+ continue
62
+ if transport and a.transport != transport:
63
+ continue
64
+ if grep and grep.lower() not in (a.id + " " + a.purpose).lower():
65
+ continue
66
+ if section:
67
+ secs = {pack.elements[e].section for e in a.elements if e in pack.elements}
68
+ if section not in secs:
69
+ continue
70
+ out.append(a)
71
+ return sorted(out, key=lambda a: a.id)
72
+
73
+
74
+ def filter_elements(pctx: PackContext, *, app: Optional[str] = None,
75
+ action: Optional[str] = None, section: Optional[str] = None,
76
+ grep: Optional[str] = None) -> list[Element]:
77
+ pack = pctx.pack
78
+ scope_ids: Optional[set] = None
79
+ if action:
80
+ act = pack.action(action)
81
+ if act is None:
82
+ raise KeyError(f"no action named '{action}'")
83
+ scope_ids = set(act.elements)
84
+ out: list[Element] = []
85
+ for e in pack.elements.values():
86
+ if app and e.app != app:
87
+ continue
88
+ if scope_ids is not None and e.id not in scope_ids:
89
+ continue
90
+ if section and e.section != section:
91
+ continue
92
+ if grep and grep.lower() not in (e.id + " " + (e.purpose or "")).lower():
93
+ continue
94
+ out.append(e)
95
+ return sorted(out, key=lambda e: e.id)
96
+
97
+
98
+ def route(pctx: PackContext, src: str, dst: str, max_depth: int = 25) -> dict:
99
+ """Compute a path and enrich it with risk/auth/param facts for consumers."""
100
+ pack = pctx.pack
101
+ result = graph.find_path(pack, src, dst, max_depth=max_depth)
102
+ if not result.found:
103
+ return {"found": False, "reason": result.reason}
104
+ on_path = [pack.actions[a] for a in result.path if a in pack.actions]
105
+ apps = {a.app for a in on_path}
106
+ return {
107
+ "found": True,
108
+ "length": result.length,
109
+ "path": result.path,
110
+ "crosses_app": len(apps) > 1,
111
+ "risk_max": pack.config.risk.max([a.risk for a in on_path]),
112
+ "requires_auth": sorted(apps),
113
+ "params_required": sorted({p.key for a in on_path for p in a.params}),
114
+ }
115
+
116
+
117
+ def uses(pctx: PackContext, ref: str) -> dict:
118
+ return usage.uses(pctx.pack, ref)
119
+
120
+
121
+ def flow_param_manifest(pctx: PackContext, name: str) -> list[dict]:
122
+ """Aggregate every param a flow needs (flow-level + each action's), deduped.
123
+
124
+ A cheap lookup that answers "what must I supply?" without compiling — so the
125
+ caller can gather values in one pass instead of the compile-read-recompile
126
+ dance. Secrets never echo a default; ``satisfied_by`` names a capability that
127
+ can mint the value so the agent need not ask a human.
128
+ """
129
+ pack = pctx.pack
130
+ flow = pack.flow(name)
131
+ if flow is None:
132
+ raise KeyError(f"no flow named '{name}'")
133
+ seen: set[str] = set()
134
+ manifest: list[dict] = []
135
+
136
+ def _add(param) -> None:
137
+ if param.key in seen:
138
+ return
139
+ seen.add(param.key)
140
+ entry = {
141
+ "key": param.key,
142
+ "type": param.type,
143
+ "required": param.required,
144
+ "secret": param.is_secret,
145
+ "default": None if param.is_secret else param.default,
146
+ }
147
+ if param.enum:
148
+ entry["enum"] = list(param.enum)
149
+ if param.satisfied_by:
150
+ entry["satisfied_by"] = param.satisfied_by
151
+ manifest.append(entry)
152
+
153
+ for param in flow.params:
154
+ _add(param)
155
+ for inv in expand_invocations(pack, name):
156
+ action = pack.action(inv.action_id)
157
+ if action is None:
158
+ continue
159
+ for param in action.params:
160
+ _add(param)
161
+ return manifest
162
+
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # Compilation / validation / verification
166
+ # ---------------------------------------------------------------------------
167
+
168
+
169
+ def compile_script(pctx: PackContext, *, flow: Optional[str] = None,
170
+ src: Optional[str] = None, dst: Optional[str] = None,
171
+ actions: Optional[list[str]] = None, **compile_kw) -> CompiledScript:
172
+ """Compile a flow, a path (src→dst), or an explicit action list."""
173
+ pack, rt = pctx.pack, pctx.runtime
174
+ if flow:
175
+ return compiler.compile_flow(pack, rt, flow, **compile_kw)
176
+ if src and dst:
177
+ return compiler.compile_path(pack, rt, src, dst, **compile_kw)
178
+ if actions:
179
+ return compiler.compile_actions(pack, rt, actions, **compile_kw)
180
+ raise ValueError("compile needs a flow, a path (src/dst), or actions")
181
+
182
+
183
+ def validate_pack(pctx: PackContext, app: Optional[str] = None) -> ValidationReport:
184
+ return validation.validate(pctx.pack, app=app)
185
+
186
+
187
+ def verify(pctx: PackContext, *, flow: Optional[str] = None, app: Optional[str] = None,
188
+ action: Optional[str] = None, drive: bool = False,
189
+ allow_gated: bool = False) -> dict:
190
+ return verification.verify_probe(pctx.pack, pctx.runtime, flow=flow, app=app,
191
+ action=action, drive=drive, allow_gated=allow_gated)
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # Capabilities / import
196
+ # ---------------------------------------------------------------------------
197
+
198
+
199
+ def list_capabilities(pctx: PackContext, check: bool = False) -> list[dict]:
200
+ reg = CapabilityRegistry(pctx.pack.config, pctx.pack.root)
201
+ checks = reg.check_all() if check else {}
202
+ return [{"key": key, "impl": reg.spec(key),
203
+ "error": checks.get(key) if check else None} for key in reg.keys]
204
+
205
+
206
+ def init_project(dest: str | Path, agents: list[str], force: bool = False) -> dict:
207
+ """Scaffold a pack skeleton + agent instruction files under ``dest``."""
208
+ return _init_project(dest, agents=agents, force=force)
209
+
210
+
211
+ def import_markdown(md_file: str | Path, out_dir: str | Path) -> dict:
212
+ result = import_md(md_file)
213
+ written = write_seed(result, out_dir)
214
+ return {
215
+ "structured": result.structured,
216
+ "apps": sorted(result.apps),
217
+ "flows": sorted(result.flows),
218
+ "notes": result.notes,
219
+ "written": [str(p) for p in written],
220
+ }
@@ -0,0 +1,17 @@
1
+ # Domain layer — pure model + business rules.
2
+ #
3
+ # No I/O, no frameworks. Depends on nothing outside the standard library; every
4
+ # other layer depends inward on this one.
5
+ #
6
+ # Components:
7
+ # model entities: App, Element, Action, Flow, Step, Param, Capture,
8
+ # Selector, Config, Pack (and the risk taxonomy).
9
+ # templating RuntimeContext + {{token}}/{{param}} resolution (env injected).
10
+ # flows subflow expansion — the single source of truth for inlining
11
+ # `use:` references, aliasing, and the one-level nesting cap.
12
+ # graph BFS pathfinding + reachability over UI-action next/prev edges.
13
+ # validation the static linter (E_/W_ codes): "is the map self-consistent?"
14
+ # compiler flow/path/actions -> CompiledScript (the payload build).
15
+ # verification read-only drift-probe builder: "does it still match the UI?"
16
+ # usage reverse index — change blast radius for an element/action/flow.
17
+ # errors the shared exception hierarchy.