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 +22 -0
- uipilot/application/__init__.py +11 -0
- uipilot/application/service.py +220 -0
- uipilot/domain/__init__.py +17 -0
- uipilot/domain/compiler.py +520 -0
- uipilot/domain/errors.py +32 -0
- uipilot/domain/flows.py +58 -0
- uipilot/domain/graph.py +115 -0
- uipilot/domain/model.py +362 -0
- uipilot/domain/templating.py +101 -0
- uipilot/domain/usage.py +68 -0
- uipilot/domain/validation.py +324 -0
- uipilot/domain/verification.py +140 -0
- uipilot/infrastructure/__init__.py +14 -0
- uipilot/infrastructure/capabilities.py +103 -0
- uipilot/infrastructure/markdown_importer.py +171 -0
- uipilot/infrastructure/pack_loader.py +334 -0
- uipilot/infrastructure/scaffold.py +122 -0
- uipilot/presentation/__init__.py +11 -0
- uipilot/presentation/cli.py +570 -0
- uipilot/presentation/renderers.py +311 -0
- uipilot/templates/__init__.py +0 -0
- uipilot/templates/capabilities.py +18 -0
- uipilot/templates/data/app.app.yaml +15 -0
- uipilot/templates/data/flows.yaml +3 -0
- uipilot/templates/flowmap.config.yaml +14 -0
- uipilot/templates/skill.md +131 -0
- uipilot-0.1.0.dist-info/METADATA +227 -0
- uipilot-0.1.0.dist-info/RECORD +31 -0
- uipilot-0.1.0.dist-info/WHEEL +4 -0
- uipilot-0.1.0.dist-info/entry_points.txt +3 -0
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.
|