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.
- tesserakit_app-0.3.1/.gitignore +8 -0
- tesserakit_app-0.3.1/PKG-INFO +52 -0
- tesserakit_app-0.3.1/README.md +35 -0
- tesserakit_app-0.3.1/pyproject.toml +31 -0
- tesserakit_app-0.3.1/src/tessera_app/__init__.py +3 -0
- tesserakit_app-0.3.1/src/tessera_app/cli.py +79 -0
- tesserakit_app-0.3.1/src/tessera_app/dashboard.py +124 -0
- tesserakit_app-0.3.1/src/tessera_app/detect.py +157 -0
- tesserakit_app-0.3.1/src/tessera_app/markdown.py +91 -0
- tesserakit_app-0.3.1/src/tessera_app/orchestrator.py +87 -0
- tesserakit_app-0.3.1/tests/test_app.py +106 -0
|
@@ -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,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 "<script>" 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"}
|