tesserakit-app 0.3.1__tar.gz

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.
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ out/
8
+ .DS_Store
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: tesserakit-app
3
+ Version: 0.3.1
4
+ Summary: Tessera app: orchestrate the job packs over a project and build a self-contained HTML dashboard.
5
+ Author: Tessera
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Environment :: Console
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: rich>=13.7
12
+ Requires-Dist: tesserakit-core>=0.1.0
13
+ Requires-Dist: typer>=0.12
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8.0; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # tesserakit-app
19
+
20
+ The Tessera app: run the whole hub over a project and get one browsable dashboard.
21
+
22
+ `tessera-app` is the unifying surface over the job packs. It detects which packs apply to a project, runs them, and renders all their artifacts into a single self-contained HTML dashboard. No server, no build step, no extra dependencies.
23
+
24
+ It is a **CLI-only plugin**: it registers commands under `tessera.commands` and orchestrates JobPacks, but is not itself a JobPack. (This is the first real use of the deliberate split between the `tessera.commands` and `tessera.jobpacks` entry-point groups.)
25
+
26
+ ## Commands
27
+
28
+ ```bash
29
+ tessera detect --input . # show which packs apply, without running
30
+ tessera run --input . --output run # run applicable packs + build dashboard
31
+ tessera dashboard --input run # (re)build the HTML dashboard from a run
32
+ ```
33
+
34
+ ## What `run` does
35
+
36
+ 1. **Detects** applicable packs by inspecting the project:
37
+ - `.prompt.md` / `PROMPT.md` → prompts
38
+ - `SKILL.md` → skills
39
+ - `.recipe.md` / `RECIPE.md` → recipes
40
+ - `.curl` / curl `.sh` → api
41
+ - `corpus/` + a `queries.*` file → rag
42
+ - any `.csv` → evals (task `generic`)
43
+ - source files or a dependency manifest → repo
44
+ 2. **Runs** each applicable pack into `output/<pack>/`, continuing past any pack that fails.
45
+ 3. **Writes** `run_manifest.json` summarizing record/finding counts and artifacts.
46
+ 4. **Builds** `output/index.html`: a dashboard with headline cards and every pack's reports, rendered from Markdown to HTML in the browser-ready file.
47
+
48
+ ## Notes
49
+
50
+ - The orchestrator never raises on a single pack's failure; it records the error and moves on, so one bad input does not sink the whole run.
51
+ - The dashboard is fully self-contained (inline CSS, no JS, no external assets) so it can be committed, emailed, or served as a static file.
52
+ - For full control over a single pack (e.g. a specific eval task type or column overrides), use that pack's own command (`tessera evals compile ...`).
@@ -0,0 +1,35 @@
1
+ # tesserakit-app
2
+
3
+ The Tessera app: run the whole hub over a project and get one browsable dashboard.
4
+
5
+ `tessera-app` is the unifying surface over the job packs. It detects which packs apply to a project, runs them, and renders all their artifacts into a single self-contained HTML dashboard. No server, no build step, no extra dependencies.
6
+
7
+ It is a **CLI-only plugin**: it registers commands under `tessera.commands` and orchestrates JobPacks, but is not itself a JobPack. (This is the first real use of the deliberate split between the `tessera.commands` and `tessera.jobpacks` entry-point groups.)
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ tessera detect --input . # show which packs apply, without running
13
+ tessera run --input . --output run # run applicable packs + build dashboard
14
+ tessera dashboard --input run # (re)build the HTML dashboard from a run
15
+ ```
16
+
17
+ ## What `run` does
18
+
19
+ 1. **Detects** applicable packs by inspecting the project:
20
+ - `.prompt.md` / `PROMPT.md` → prompts
21
+ - `SKILL.md` → skills
22
+ - `.recipe.md` / `RECIPE.md` → recipes
23
+ - `.curl` / curl `.sh` → api
24
+ - `corpus/` + a `queries.*` file → rag
25
+ - any `.csv` → evals (task `generic`)
26
+ - source files or a dependency manifest → repo
27
+ 2. **Runs** each applicable pack into `output/<pack>/`, continuing past any pack that fails.
28
+ 3. **Writes** `run_manifest.json` summarizing record/finding counts and artifacts.
29
+ 4. **Builds** `output/index.html`: a dashboard with headline cards and every pack's reports, rendered from Markdown to HTML in the browser-ready file.
30
+
31
+ ## Notes
32
+
33
+ - The orchestrator never raises on a single pack's failure; it records the error and moves on, so one bad input does not sink the whole run.
34
+ - The dashboard is fully self-contained (inline CSS, no JS, no external assets) so it can be committed, emailed, or served as a static file.
35
+ - For full control over a single pack (e.g. a specific eval task type or column overrides), use that pack's own command (`tessera evals compile ...`).
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tesserakit-app"
7
+ version = "0.3.1"
8
+ description = "Tessera app: orchestrate the job packs over a project and build a self-contained HTML dashboard."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{ name = "Tessera" }]
12
+ dependencies = [
13
+ "tesserakit-core>=0.1.0",
14
+ "typer>=0.12",
15
+ "rich>=13.7",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "Programming Language :: Python :: 3",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["pytest>=8.0"]
26
+
27
+ [project.entry-points."tessera.commands"]
28
+ app = "tessera_app.cli:register"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/tessera_app"]
@@ -0,0 +1,3 @@
1
+ """Tessera app: orchestrator + dashboard over the job packs."""
2
+
3
+ __version__ = "0.3.1"
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from tessera_app.dashboard import build_dashboard
10
+ from tessera_app.detect import detect_packs
11
+ from tessera_app.orchestrator import run_project
12
+
13
+ console = Console()
14
+ app = typer.Typer(help="Run the whole Tessera hub over a project and build a dashboard.")
15
+
16
+
17
+ @app.command("detect")
18
+ def detect_cmd(
19
+ input: Path = typer.Option(Path("."), "--input", "-i", exists=True, help="Project directory."),
20
+ ) -> None:
21
+ """Show which packs apply to a project, without running them."""
22
+ detections = detect_packs(input)
23
+ table = Table(title="Applicable Packs")
24
+ table.add_column("Pack")
25
+ table.add_column("Why")
26
+ for d in detections:
27
+ table.add_row(d.pack, d.reason)
28
+ if not detections:
29
+ console.print("[yellow]No packs detected for this project.[/yellow]")
30
+ return
31
+ console.print(table)
32
+
33
+
34
+ @app.command("run")
35
+ def run_cmd(
36
+ input: Path = typer.Option(Path("."), "--input", "-i", exists=True, help="Project directory."),
37
+ output: Path = typer.Option(Path("tessera_run"), "--output", "-o", help="Output directory."),
38
+ only: str = typer.Option("", "--only", help="Comma-separated pack names to limit the run."),
39
+ dashboard: bool = typer.Option(True, "--dashboard/--no-dashboard", help="Build an HTML dashboard after the run."),
40
+ ) -> None:
41
+ """Detect applicable packs, run them, and (by default) build a dashboard."""
42
+ only_list = [s.strip() for s in only.split(",") if s.strip()] or None
43
+ results = run_project(input, output, only=only_list)
44
+
45
+ table = Table(title="Tessera Run")
46
+ table.add_column("Pack")
47
+ table.add_column("Status")
48
+ table.add_column("Records", justify="right")
49
+ table.add_column("Findings", justify="right")
50
+ table.add_column("Errors", justify="right")
51
+ for r in results:
52
+ status = "ok" if r.ok else "FAILED"
53
+ table.add_row(r.pack, status, str(r.record_count), str(r.finding_count), str(r.error_count))
54
+ console.print(table)
55
+
56
+ if not results:
57
+ console.print("[yellow]No packs were applicable.[/yellow]")
58
+ return
59
+
60
+ if dashboard:
61
+ html_path = build_dashboard(output)
62
+ console.print(f"[green]Dashboard:[/green] {html_path}")
63
+
64
+
65
+ @app.command("dashboard")
66
+ def dashboard_cmd(
67
+ input: Path = typer.Option(..., "--input", "-i", exists=True, help="A tessera run output directory (contains run_manifest.json)."),
68
+ output: Path = typer.Option(None, "--output", "-o", help="HTML path (default: <input>/index.html)."),
69
+ ) -> None:
70
+ """Build (or rebuild) the HTML dashboard from a run output directory."""
71
+ html_path = build_dashboard(input, output)
72
+ console.print(f"[green]Dashboard:[/green] {html_path}")
73
+
74
+
75
+ def register(root_app: typer.Typer) -> None:
76
+ # tessera-app contributes top-level commands rather than a subgroup.
77
+ root_app.command("run")(run_cmd)
78
+ root_app.command("detect")(detect_cmd)
79
+ root_app.command("dashboard")(dashboard_cmd)
@@ -0,0 +1,124 @@
1
+ """Build a self-contained HTML dashboard from a run output directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from tessera_app.markdown import render_markdown
10
+
11
+ _CSS = """
12
+ :root{--bg:#0f1115;--panel:#171a21;--ink:#e6e8eb;--muted:#9aa3af;--line:#262b35;--accent:#6ea8fe;--ok:#3fb950;--warn:#d29922;--err:#f85149}
13
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.55 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
14
+ header{padding:28px 32px;border-bottom:1px solid var(--line)}
15
+ h1{margin:0;font-size:22px}h2{margin:28px 0 10px;font-size:18px}h3{margin:18px 0 8px;font-size:15px;color:var(--muted)}
16
+ .sub{color:var(--muted);margin-top:6px}
17
+ .wrap{display:flex;gap:0;min-height:calc(100vh - 86px)}
18
+ nav{width:200px;flex:none;border-right:1px solid var(--line);padding:18px 0}
19
+ nav a{display:block;padding:8px 24px;color:var(--ink);text-decoration:none;border-left:2px solid transparent}
20
+ nav a:hover{background:var(--panel);border-left-color:var(--accent)}
21
+ main{flex:1;padding:24px 32px;max-width:980px}
22
+ .cards{display:flex;flex-wrap:wrap;gap:12px;margin:12px 0 4px}
23
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px;min-width:120px}
24
+ .card .k{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.04em}
25
+ .card .v{font-size:22px;font-weight:600;margin-top:4px}
26
+ .badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:12px;font-weight:600}
27
+ .b-ok{background:rgba(63,185,80,.15);color:var(--ok)}.b-err{background:rgba(248,81,73,.15);color:var(--err)}
28
+ section.pack{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:8px 20px 18px;margin:18px 0}
29
+ table{border-collapse:collapse;width:100%;margin:8px 0;font-size:13px}
30
+ th,td{border:1px solid var(--line);padding:6px 10px;text-align:left}th{background:#1d2230;color:var(--muted)}
31
+ code{background:#1d2230;padding:1px 5px;border-radius:4px;font-size:13px}
32
+ pre.code{background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:12px;overflow:auto;font-size:12.5px}
33
+ ul{margin:6px 0 6px 18px}details{margin:8px 0}summary{cursor:pointer;color:var(--accent)}
34
+ .report{border-top:1px solid var(--line);margin-top:14px;padding-top:6px}
35
+ footer{color:var(--muted);padding:24px 32px;border-top:1px solid var(--line);font-size:13px}
36
+ """
37
+
38
+ _REPORT_ORDER = [
39
+ "index.md", "validation_report.md", "coverage_report.md",
40
+ "data_quality_report.md", "dependencies_report.md",
41
+ "execution_plans.md", "redactions_report.md", "retrieval_targets.md",
42
+ ]
43
+
44
+
45
+ def build_dashboard(run_dir: Path, output_html: Path | None = None) -> Path:
46
+ """Render run_dir into a single self-contained index.html. Returns its path."""
47
+ run_dir = Path(run_dir)
48
+ manifest_path = run_dir / "run_manifest.json"
49
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.exists() else {"project": str(run_dir), "packs": []}
50
+ packs = manifest.get("packs", [])
51
+
52
+ nav = "".join(f"<a href='#{p['pack']}'>{html.escape(p['pack'])}</a>" for p in packs)
53
+
54
+ total_records = sum(p.get("record_count", 0) for p in packs)
55
+ total_findings = sum(p.get("finding_count", 0) for p in packs)
56
+ total_errors = sum(p.get("error_count", 0) for p in packs)
57
+
58
+ cards = _cards({
59
+ "Packs run": len([p for p in packs if p.get("ok")]),
60
+ "Records": total_records,
61
+ "Findings": total_findings,
62
+ "Errors": total_errors,
63
+ })
64
+
65
+ sections = "".join(_pack_section(run_dir, p) for p in packs)
66
+ if not packs:
67
+ sections = "<p class='sub'>No packs were applicable to this project.</p>"
68
+
69
+ doc = f"""<!doctype html>
70
+ <html lang="en"><head><meta charset="utf-8">
71
+ <meta name="viewport" content="width=device-width, initial-scale=1">
72
+ <title>Tessera Dashboard</title><style>{_CSS}</style></head>
73
+ <body>
74
+ <header><h1>Tessera Dashboard</h1>
75
+ <div class="sub">Project: <code>{html.escape(str(manifest.get('project','')))}</code></div>
76
+ {cards}</header>
77
+ <div class="wrap"><nav>{nav}</nav><main>{sections}</main></div>
78
+ <footer>Generated by tessera-app. Self-contained; no server required.</footer>
79
+ </body></html>"""
80
+
81
+ out = output_html or (run_dir / "index.html")
82
+ out.write_text(doc, encoding="utf-8")
83
+ return out
84
+
85
+
86
+ def _cards(metrics: dict[str, int]) -> str:
87
+ items = "".join(f"<div class='card'><div class='k'>{html.escape(k)}</div><div class='v'>{v}</div></div>"
88
+ for k, v in metrics.items())
89
+ return f"<div class='cards'>{items}</div>"
90
+
91
+
92
+ def _pack_section(run_dir: Path, p: dict) -> str:
93
+ name = p.get("pack", "")
94
+ status = ("<span class='badge b-ok'>ok</span>" if p.get("ok")
95
+ else f"<span class='badge b-err'>failed</span>")
96
+ head = (f"<section class='pack' id='{html.escape(name)}'>"
97
+ f"<h2>{html.escape(name)} {status}</h2>"
98
+ f"<div class='sub'>{html.escape(p.get('reason',''))}</div>")
99
+
100
+ if not p.get("ok"):
101
+ return head + f"<p class='b-err'>{html.escape(p.get('error',''))}</p></section>"
102
+
103
+ head += _cards({
104
+ "Records": p.get("record_count", 0),
105
+ "Findings": p.get("finding_count", 0),
106
+ "Errors": p.get("error_count", 0),
107
+ })
108
+
109
+ pack_dir = Path(p.get("output_dir", "")) if p.get("output_dir") else run_dir / name
110
+ reports = sorted(
111
+ (a for a in p.get("artifacts", []) if a.endswith(".md")),
112
+ key=lambda a: _REPORT_ORDER.index(a) if a in _REPORT_ORDER else 99,
113
+ )
114
+ body = ""
115
+ for fname in reports:
116
+ fpath = pack_dir / fname
117
+ if not fpath.exists():
118
+ continue
119
+ rendered = render_markdown(fpath.read_text(encoding="utf-8"))
120
+ open_attr = " open" if fname in ("index.md", "validation_report.md") else ""
121
+ body += (f"<details{open_attr}><summary>{html.escape(fname)}</summary>"
122
+ f"<div class='report'>{rendered}</div></details>")
123
+
124
+ return head + body + "</section>"
@@ -0,0 +1,157 @@
1
+ """Detect which job packs apply to a project directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ _IGNORE = {
10
+ ".git", ".venv", "venv", "node_modules", "__pycache__", ".pytest_cache",
11
+ "dist", "build", ".tox", "target", ".mypy_cache", ".ruff_cache",
12
+ }
13
+
14
+
15
+ @dataclass
16
+ class Detection:
17
+ pack: str
18
+ reason: str
19
+ input_path: Path
20
+ options: dict[str, Any] = field(default_factory=dict)
21
+
22
+
23
+ def _walk(root: Path):
24
+ for p in root.rglob("*"):
25
+ if any(part in _IGNORE for part in p.relative_to(root).parts):
26
+ continue
27
+ yield p
28
+
29
+
30
+ def detect_packs(project: Path) -> list[Detection]:
31
+ """Return the detections that apply to ``project`` (a directory)."""
32
+ project = project if project.is_dir() else project.parent
33
+ files = [p for p in _walk(project) if p.is_file()]
34
+ names = {p.name.lower() for p in files}
35
+ detections: list[Detection] = []
36
+
37
+ def any_suffix(*suffixes: str) -> bool:
38
+ return any(p.suffix.lower() in suffixes for p in files)
39
+
40
+ def any_named(predicate) -> bool:
41
+ return any(predicate(p) for p in files)
42
+
43
+ # prompts
44
+ if any_named(lambda p: p.name.endswith(".prompt.md") or p.name.lower() == "prompt.md"):
45
+ detections.append(Detection("prompts", "found .prompt.md / PROMPT.md files", project))
46
+
47
+ # skills
48
+ if "skill.md" in names:
49
+ detections.append(Detection("skills", "found SKILL.md files", project))
50
+
51
+ # recipes
52
+ if any_named(lambda p: p.name.endswith(".recipe.md") or p.name.lower() == "recipe.md"):
53
+ detections.append(Detection("recipes", "found .recipe.md / RECIPE.md files", project))
54
+
55
+ # api (curl files)
56
+ if any_suffix(".curl") or any_named(lambda p: p.suffix.lower() == ".sh" and "curl" in _safe_head(p)):
57
+ detections.append(Detection("api", "found curl/.sh files", project))
58
+
59
+ # rag (corpus/ + queries.*)
60
+ corpus = project / "corpus"
61
+ has_queries = any(p.name.lower() in ("queries.jsonl", "queries.yaml", "queries.yml") for p in files)
62
+ if corpus.is_dir() and has_queries:
63
+ detections.append(Detection("rag", "found corpus/ and a queries file", project))
64
+
65
+ # evals (first CSV)
66
+ csvs = sorted(p for p in files if p.suffix.lower() == ".csv")
67
+ if csvs:
68
+ detections.append(Detection("evals", f"found CSV: {csvs[0].name}", csvs[0], {"task_type": "generic"}))
69
+
70
+ # repo (a manifest or any source file => treat as a repository)
71
+ manifest_names = {"pyproject.toml", "package.json", "cargo.toml", "go.mod", "requirements.txt"}
72
+ source_suffixes = {".py", ".js", ".ts", ".go", ".rs", ".java", ".rb"}
73
+ if names & manifest_names or any_suffix(*source_suffixes):
74
+ detections.append(Detection("repo", "found source files / a dependency manifest", project))
75
+
76
+ # deps (a dependency manifest => audit pinning/duplicates)
77
+ if names & manifest_names or any_named(lambda p: p.name.lower().startswith("requirements") and p.name.lower().endswith(".txt")):
78
+ detections.append(Detection("deps", "found a dependency manifest", project))
79
+
80
+ # i18n (a locales/ or i18n/ directory of JSON files)
81
+ if (project / "locales").is_dir() or (project / "i18n").is_dir():
82
+ detections.append(Detection("i18n", "found a locales/ or i18n/ directory", project))
83
+
84
+ # config (any .env-style file present)
85
+ if any_named(lambda p: p.name.lower() == ".env" or p.name.lower().startswith(".env.") or p.name.lower().endswith(".env")):
86
+ detections.append(Detection("config", "found .env / .env.example files", project))
87
+
88
+ # schema (a *.schema.json or a json mentioning $schema/properties)
89
+ schema_file = next(
90
+ (p for p in files
91
+ if p.name.lower().endswith(".schema.json")
92
+ or (p.suffix.lower() == ".json" and '"$schema"' in _safe_head(p) and "openapi" not in _safe_head(p))),
93
+ None,
94
+ )
95
+ if schema_file is not None:
96
+ detections.append(Detection("schema", "found JSON Schema document(s)", project))
97
+
98
+ # openapi (a yaml/json spec mentioning openapi/swagger)
99
+ spec = next(
100
+ (p for p in files
101
+ if p.suffix.lower() in (".yaml", ".yml", ".json")
102
+ and ("openapi" in _safe_head(p) or "swagger" in _safe_head(p))),
103
+ None,
104
+ )
105
+ if spec is not None:
106
+ detections.append(Detection("openapi", f"found an OpenAPI/Swagger spec: {spec.name}", spec))
107
+
108
+ # docs (any Python source -> docstring coverage)
109
+ if any_suffix(".py"):
110
+ detections.append(Detection("docs", "found Python source for docstring coverage", project))
111
+
112
+ # links (any markdown files -> link check)
113
+ if any_suffix(".md", ".markdown"):
114
+ detections.append(Detection("links", "found markdown files", project))
115
+
116
+ # glossary (any source or docs -> vocabulary extraction)
117
+ if any_suffix(".py", ".js", ".ts", ".go", ".rs", ".java", ".rb", ".md"):
118
+ detections.append(Detection("glossary", "found source/docs for vocabulary analysis", project))
119
+
120
+ # tests (python test files present)
121
+ if any_named(lambda p: p.suffix == ".py" and (p.name.startswith("test_") or p.name.endswith("_test.py"))):
122
+ detections.append(Detection("tests", "found Python test files", project))
123
+
124
+ # sql (any .sql files)
125
+ if any_suffix(".sql"):
126
+ detections.append(Detection("sql", "found .sql files", project))
127
+
128
+ # dockerfile (any Dockerfile / *.dockerfile)
129
+ if any_named(lambda p: p.name.lower() == "dockerfile" or p.name.lower().startswith("dockerfile.") or p.name.lower().endswith(".dockerfile")):
130
+ detections.append(Detection("dockerfile", "found a Dockerfile", project))
131
+
132
+ # todo (any common source/doc files -> marker backlog)
133
+ if any_suffix(".py", ".js", ".ts", ".go", ".rs", ".java", ".rb", ".md", ".sql", ".sh"):
134
+ detections.append(Detection("todo", "found source/doc files to scan for markers", project))
135
+
136
+ # license (a LICENSE file or a manifest that may declare one)
137
+ if any_named(lambda p: p.name.lower().startswith(("license", "licence")) or p.name.lower() == "copying") or (names & manifest_names):
138
+ detections.append(Detection("license", "found a LICENSE file or a manifest", project))
139
+
140
+ # gha (GitHub Actions workflows)
141
+ if (project / ".github" / "workflows").is_dir():
142
+ detections.append(Detection("gha", "found .github/workflows", project))
143
+
144
+ # changelog (a git repo, or a commits.jsonl)
145
+ if (project / ".git").exists():
146
+ detections.append(Detection("changelog", "found a git repository", project))
147
+ elif any(p.name.lower() == "commits.jsonl" for p in files):
148
+ detections.append(Detection("changelog", "found commits.jsonl", project))
149
+
150
+ return detections
151
+
152
+
153
+ def _safe_head(path: Path, n: int = 400) -> str:
154
+ try:
155
+ return path.read_text(encoding="utf-8")[:n]
156
+ except (OSError, UnicodeDecodeError):
157
+ return ""
@@ -0,0 +1,91 @@
1
+ """A small, dependency-free Markdown-to-HTML renderer.
2
+
3
+ Handles the subset Tessera reports use: ATX headings, pipe tables, bullet
4
+ lists, fenced code blocks, bold, and inline code. Not a general Markdown
5
+ implementation; it is tuned to the artifacts this hub produces.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import html
11
+ import re
12
+
13
+ _BOLD = re.compile(r"\*\*(.+?)\*\*")
14
+ _CODE = re.compile(r"`([^`]+)`")
15
+
16
+
17
+ def _inline(text: str) -> str:
18
+ text = html.escape(text)
19
+ text = _BOLD.sub(r"<strong>\1</strong>", text)
20
+ text = _CODE.sub(r"<code>\1</code>", text)
21
+ return text
22
+
23
+
24
+ def _is_table_sep(line: str) -> bool:
25
+ s = line.strip()
26
+ return bool(s) and set(s) <= set("|:- ") and "-" in s
27
+
28
+
29
+ def render_markdown(md: str) -> str:
30
+ lines = md.splitlines()
31
+ out: list[str] = []
32
+ i = 0
33
+ n = len(lines)
34
+
35
+ while i < n:
36
+ line = lines[i]
37
+ stripped = line.strip()
38
+
39
+ # fenced code
40
+ if stripped.startswith("```"):
41
+ i += 1
42
+ code: list[str] = []
43
+ while i < n and not lines[i].strip().startswith("```"):
44
+ code.append(html.escape(lines[i]))
45
+ i += 1
46
+ i += 1 # skip closing fence
47
+ out.append("<pre class='code'>" + "\n".join(code) + "</pre>")
48
+ continue
49
+
50
+ # headings
51
+ m = re.match(r"^(#{1,6})\s+(.*)$", line)
52
+ if m:
53
+ level = len(m.group(1))
54
+ out.append(f"<h{level}>{_inline(m.group(2).strip())}</h{level}>")
55
+ i += 1
56
+ continue
57
+
58
+ # tables: a header row followed by a separator row
59
+ if stripped.startswith("|") and i + 1 < n and _is_table_sep(lines[i + 1]):
60
+ header = [c.strip() for c in stripped.strip("|").split("|")]
61
+ i += 2
62
+ rows: list[list[str]] = []
63
+ while i < n and lines[i].strip().startswith("|"):
64
+ rows.append([c.strip() for c in lines[i].strip().strip("|").split("|")])
65
+ i += 1
66
+ thead = "".join(f"<th>{_inline(h)}</th>" for h in header)
67
+ body = ""
68
+ for r in rows:
69
+ body += "<tr>" + "".join(f"<td>{_inline(c)}</td>" for c in r) + "</tr>"
70
+ out.append(f"<table><thead><tr>{thead}</tr></thead><tbody>{body}</tbody></table>")
71
+ continue
72
+
73
+ # bullet list
74
+ if re.match(r"^\s*[-*]\s+", line):
75
+ items: list[str] = []
76
+ while i < n and re.match(r"^\s*[-*]\s+", lines[i]):
77
+ items.append("<li>" + _inline(re.sub(r"^\s*[-*]\s+", "", lines[i])) + "</li>")
78
+ i += 1
79
+ out.append("<ul>" + "".join(items) + "</ul>")
80
+ continue
81
+
82
+ # blank line
83
+ if not stripped:
84
+ i += 1
85
+ continue
86
+
87
+ # paragraph
88
+ out.append(f"<p>{_inline(stripped)}</p>")
89
+ i += 1
90
+
91
+ return "\n".join(out)
@@ -0,0 +1,87 @@
1
+ """Run the applicable job packs over a project and summarize the run."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from tessera_core.models import RunContext
10
+ from tessera_core.plugins import load_jobpacks
11
+ from tessera_core.workspace import write_json
12
+
13
+ from tessera_app.detect import Detection, detect_packs
14
+
15
+
16
+ @dataclass
17
+ class PackResult:
18
+ pack: str
19
+ reason: str
20
+ ok: bool
21
+ record_count: int = 0
22
+ finding_count: int = 0
23
+ error_count: int = 0
24
+ artifacts: list[str] = field(default_factory=list)
25
+ output_dir: str = ""
26
+ error: str = ""
27
+
28
+
29
+ def run_project(project: Path, output_dir: Path, only: list[str] | None = None) -> list[PackResult]:
30
+ """Detect applicable packs, run each into output_dir/<pack>/, and summarize."""
31
+ detections = detect_packs(project)
32
+ if only:
33
+ detections = [d for d in detections if d.pack in only]
34
+
35
+ packs = load_jobpacks()
36
+ results: list[PackResult] = []
37
+ output_dir.mkdir(parents=True, exist_ok=True)
38
+
39
+ for det in detections:
40
+ pack = packs.get(det.pack)
41
+ if pack is None:
42
+ results.append(PackResult(det.pack, det.reason, ok=False, error="pack not installed"))
43
+ continue
44
+ pack_out = output_dir / det.pack
45
+ ctx = RunContext(job_name=det.pack, output_dir=pack_out)
46
+ try:
47
+ artifacts = pack.run(input_path=det.input_path, ctx=ctx, options=dict(det.options))
48
+ findings = ctx.metadata.get("findings", []) or []
49
+ errors = sum(1 for f in findings if getattr(f, "severity", "") == "error")
50
+ results.append(
51
+ PackResult(
52
+ pack=det.pack,
53
+ reason=det.reason,
54
+ ok=True,
55
+ record_count=ctx.metadata.get("record_count", 0),
56
+ finding_count=ctx.metadata.get("finding_count", len(findings)),
57
+ error_count=errors,
58
+ artifacts=[a.name for a in artifacts],
59
+ output_dir=str(pack_out),
60
+ )
61
+ )
62
+ except Exception as exc: # keep the run going; record the failure
63
+ results.append(PackResult(det.pack, det.reason, ok=False, error=str(exc), output_dir=str(pack_out)))
64
+
65
+ _write_manifest(project, output_dir, results)
66
+ return results
67
+
68
+
69
+ def _write_manifest(project: Path, output_dir: Path, results: list[PackResult]) -> None:
70
+ manifest: dict[str, Any] = {
71
+ "project": str(project),
72
+ "packs": [
73
+ {
74
+ "pack": r.pack,
75
+ "reason": r.reason,
76
+ "ok": r.ok,
77
+ "record_count": r.record_count,
78
+ "finding_count": r.finding_count,
79
+ "error_count": r.error_count,
80
+ "artifacts": r.artifacts,
81
+ "output_dir": r.output_dir,
82
+ "error": r.error,
83
+ }
84
+ for r in results
85
+ ],
86
+ }
87
+ write_json(output_dir / "run_manifest.json", manifest)
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from tessera_app.dashboard import build_dashboard
9
+ from tessera_app.detect import detect_packs
10
+ from tessera_app.markdown import render_markdown
11
+ from tessera_app.orchestrator import run_project
12
+
13
+ # The app orchestrates the other packs; skip cleanly if they aren't installed.
14
+ pytest.importorskip("tessera_repo.pack", reason="job packs not installed")
15
+ pytest.importorskip("tessera_prompts.pack", reason="job packs not installed")
16
+
17
+
18
+ def _make_project(root: Path) -> Path:
19
+ """A small project that triggers several packs at once."""
20
+ root.mkdir(parents=True, exist_ok=True)
21
+ (root / "README.md").write_text("# demo\n", encoding="utf-8")
22
+ (root / "pyproject.toml").write_text(
23
+ '[project]\nname = "demo"\nversion = "0.3.1"\ndependencies = ["rich"]\n', encoding="utf-8"
24
+ )
25
+ (root / "src").mkdir()
26
+ (root / "src" / "main.py").write_text("def f():\n return 1\n", encoding="utf-8")
27
+ # a prompt
28
+ (root / "hello.prompt.md").write_text(
29
+ "---\nname: hello\ndescription: Say hello to the user by name.\nversion: 1.0.0\n"
30
+ "variables:\n - name: who\n---\nHi {{who}}.\n",
31
+ encoding="utf-8",
32
+ )
33
+ # a CSV for evals
34
+ (root / "data.csv").write_text(
35
+ "question,answer\nWhat is 2+2?,4\nCapital of France?,Paris\n", encoding="utf-8"
36
+ )
37
+ return root
38
+
39
+
40
+ # ---------- markdown renderer ----------
41
+
42
+
43
+ def test_markdown_headings_tables_lists_code():
44
+ md = "# Title\n\n| A | B |\n|---|---|\n| 1 | 2 |\n\n- one\n- two\n\n```\ncode\n```\n\n**bold** and `c`."
45
+ html = render_markdown(md)
46
+ assert "<h1>Title</h1>" in html
47
+ assert "<table>" in html and "<th>A</th>" in html and "<td>1</td>" in html
48
+ assert "<ul><li>one</li><li>two</li></ul>" in html
49
+ assert "<pre class='code'>code</pre>" in html
50
+ assert "<strong>bold</strong>" in html and "<code>c</code>" in html
51
+
52
+
53
+ def test_markdown_escapes_html():
54
+ assert "&lt;script&gt;" in render_markdown("a <script> tag")
55
+
56
+
57
+ # ---------- detection ----------
58
+
59
+
60
+ def test_detect_multiple_packs(tmp_path: Path):
61
+ project = _make_project(tmp_path)
62
+ names = {d.pack for d in detect_packs(project)}
63
+ assert {"prompts", "evals", "repo"} <= names
64
+
65
+
66
+ # ---------- orchestrator ----------
67
+
68
+
69
+ def test_run_project_runs_applicable_and_writes_manifest(tmp_path: Path):
70
+ project = _make_project(tmp_path / "proj")
71
+ out = tmp_path / "run"
72
+ results = run_project(project, out)
73
+
74
+ packs_run = {r.pack for r in results if r.ok}
75
+ assert {"prompts", "evals", "repo"} <= packs_run
76
+
77
+ manifest = json.loads((out / "run_manifest.json").read_text())
78
+ assert manifest["packs"]
79
+ # each ok pack wrote its own subdir with artifacts
80
+ for r in results:
81
+ if r.ok:
82
+ assert (out / r.pack).is_dir()
83
+
84
+
85
+ def test_run_then_dashboard_is_self_contained(tmp_path: Path):
86
+ project = _make_project(tmp_path / "proj")
87
+ out = tmp_path / "run"
88
+ run_project(project, out)
89
+ html_path = build_dashboard(out)
90
+
91
+ assert html_path.exists()
92
+ doc = html_path.read_text(encoding="utf-8")
93
+ assert "<title>Tessera Dashboard</title>" in doc
94
+ assert "Tessera Dashboard" in doc
95
+ # references the packs that ran
96
+ assert "repo" in doc and "prompts" in doc
97
+ # self-contained: no external script/style links
98
+ assert "http://" not in doc and "https://" not in doc
99
+ assert "<script" not in doc
100
+
101
+
102
+ def test_only_filter_limits_run(tmp_path: Path):
103
+ project = _make_project(tmp_path / "proj")
104
+ out = tmp_path / "run"
105
+ results = run_project(project, out, only=["repo"])
106
+ assert {r.pack for r in results} == {"repo"}