Spaces:
Sleeping
Sleeping
Flamehaven Initiative commited on
Commit ·
e9858b7
1
Parent(s): e3afeec
docs: sync hf packet surfaces to 5p and 7p
Browse files- CHANGELOG.md +3 -0
- README.md +16 -15
- docs/CLI_REFERENCE.md +4 -4
- docs/EXAMPLE_AUDITS.md +1 -1
- stem_ai/app.py +6 -6
- stem_ai/cli.py +6 -6
- stem_ai/render.py +232 -31
- stem_ai/render_html.py +44 -13
- stem_ai/render_html_components.py +13 -6
- stem_ai/render_html_styles.py +51 -8
- tests/test_scanner_regressions.py +47 -1
CHANGELOG.md
CHANGED
|
@@ -17,6 +17,9 @@ Rubric refinements and additions use patch versions (1.0.x).
|
|
| 17 |
- Tightened bio-detector scan boundaries by excluding `.manual_verify` from generated/non-runtime paths and replacing the broad trace-manifest `rglob("*")` walk with a pruned filesystem traversal that preserves existing detector semantics while lowering trace-scan overhead.
|
| 18 |
- Deferred the RDKit optional lane until at least one SMILES-like candidate is present, and cached the optional `rdkit.Chem` import so availability/import work is not repeated unnecessarily across candidate checks.
|
| 19 |
- Added a file-signature AST context cache for `detector_bio.py` so repeated scans of unchanged Python files in the same process avoid redundant `ast.parse/compile` work while keeping detector semantics unchanged.
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
---
|
| 22 |
|
|
|
|
| 17 |
- Tightened bio-detector scan boundaries by excluding `.manual_verify` from generated/non-runtime paths and replacing the broad trace-manifest `rglob("*")` walk with a pruned filesystem traversal that preserves existing detector semantics while lowering trace-scan overhead.
|
| 18 |
- Deferred the RDKit optional lane until at least one SMILES-like candidate is present, and cached the optional `rdkit.Chem` import so availability/import work is not repeated unnecessarily across candidate checks.
|
| 19 |
- Added a file-signature AST context cache for `detector_bio.py` so repeated scans of unchanged Python files in the same process avoid redundant `ast.parse/compile` work while keeping detector semantics unchanged.
|
| 20 |
+
- Changed the CLI default report depth from `--level 1` to `--level 3`, so the plain `stem <repo>` path now emits the full packet by default unless the user selects a shallower level.
|
| 21 |
+
- Reworked ReportLab PDF pagination so the brief, standard, and full packet tiers now honor their intended page counts, keeping the executive/regulatory summary on page 1 instead of allowing silent overflow into extra pages.
|
| 22 |
+
- Redefined detailed packet tiers so `--level 2` now emits a standard 5-page review packet and `--level 3` emits the full 7-page packet, restoring Stage 4 as a dedicated PDF page and preventing end-of-report overcrowding in the former 5-page Level 3 path.
|
| 23 |
|
| 24 |
---
|
| 25 |
|
README.md
CHANGED
|
@@ -57,7 +57,7 @@ pip install -e .[pdf]
|
|
| 57 |
# fastest path: scan a local repository
|
| 58 |
stem /path/to/bio-ai-repo
|
| 59 |
|
| 60 |
-
#
|
| 61 |
stem scan /path/to/bio-ai-repo --level 3 --format all --explain
|
| 62 |
```
|
| 63 |
|
|
@@ -175,20 +175,21 @@ Core modules: `stem_ai/scanner.py`, `stem_ai/render.py`, `stem_ai/cli.py`, `stem
|
|
| 175 |
## Output Artifacts
|
| 176 |
|
| 177 |
Each run writes to `--out DIR` (default: `stem_output/`).
|
|
|
|
| 178 |
|
| 179 |
| Level | Pages | Audience | Artifacts |
|
| 180 |
|-------|------:|---------|-----------|
|
| 181 |
-
| `--level 1` | 1 | Executive / triage | Score, tier, stage cards, code integrity summary |
|
| 182 |
-
| `--level 2` |
|
| 183 |
-
| `--level 3` |
|
| 184 |
|
| 185 |
```
|
| 186 |
<repo>_experiment_results.json # machine-readable score + full evidence object
|
| 187 |
<repo>_report.html # interactive 5-section HTML dashboard (v1.7.0+)
|
| 188 |
<repo>_report.md # human-readable audit report
|
| 189 |
<repo>_brief_1p.pdf # Level 1 executive dashboard
|
| 190 |
-
<repo>
|
| 191 |
-
<repo>
|
| 192 |
<repo>_explain.txt # --explain: file/line/snippet proof trace
|
| 193 |
```
|
| 194 |
|
|
@@ -199,12 +200,12 @@ Each run writes to `--out DIR` (default: `stem_output/`).
|
|
| 199 |
`--format html` generates a self-contained interactive dashboard (v1.7.0+). Single `.html` file — no network, no external dependencies.
|
| 200 |
|
| 201 |
<p align="center">
|
| 202 |
-
<img src="docs/assets/html_report_preview.png" alt="STEM BIO-AI interactive HTML dashboard" width="760">
|
| 203 |
</p>
|
| 204 |
|
| 205 |
**Example interactive HTML audit**
|
| 206 |
- Open in browser: <https://htmlpreview.github.io/?https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/examples/maziyarpanahi_openmed_report.html>
|
| 207 |
-
- Raw HTML artifact: [`docs/examples/maziyarpanahi_openmed_report.html`](docs/examples/maziyarpanahi_openmed_report.html)
|
| 208 |
|
| 209 |
**5 sections:** Executive Summary · Score Matrix · Code Integrity (expandable cards) · AIRI Risk Coverage (toggle) · Evidence Detail (filter chips)
|
| 210 |
|
|
@@ -219,25 +220,25 @@ Covered AIRI entries now also carry bounded `why mapped` reasoning derived from
|
|
| 219 |
## Report Preview
|
| 220 |
|
| 221 |
<p align="center">
|
| 222 |
-
<img src="docs/assets/report-preview/fieldbioinformatics/page-1.png" alt="STEM BIO-AI
|
| 223 |
</p>
|
| 224 |
|
| 225 |
-
**Sample PDF:** [Download the 5-page
|
| 226 |
|
| 227 |
<details>
|
| 228 |
-
<summary>View all 5 preview pages</summary>
|
| 229 |
|
| 230 |
| Page 1 | Page 2 |
|
| 231 |
|--------|--------|
|
| 232 |
-
| <img src="docs/assets/report-preview/fieldbioinformatics/page-1.png" alt="Page 1"> | <img src="docs/assets/report-preview/fieldbioinformatics/page-2.png" alt="Page 2"> |
|
| 233 |
|
| 234 |
| Page 3 | Page 4 |
|
| 235 |
|--------|--------|
|
| 236 |
-
| <img src="docs/assets/report-preview/fieldbioinformatics/page-3.png" alt="Page 3"> | <img src="docs/assets/report-preview/fieldbioinformatics/page-4.png" alt="Page 4"> |
|
| 237 |
|
| 238 |
| Page 5 |
|
| 239 |
|--------|
|
| 240 |
-
| <img src="docs/assets/report-preview/fieldbioinformatics/page-5.png" alt="Page 5"> |
|
| 241 |
|
| 242 |
</details>
|
| 243 |
|
|
@@ -390,7 +391,7 @@ Operational reference: [`docs/MICA_MEMORY.md`](docs/MICA_MEMORY.md)
|
|
| 390 |
Live demo: [huggingface.co/spaces/Flamehaven/stem-bio-ai](https://huggingface.co/spaces/Flamehaven/stem-bio-ai)
|
| 391 |
|
| 392 |
<p align="center">
|
| 393 |
-
<img src="docs/assets/HF-STEM-BIO_AI.png" alt="STEM BIO-AI Hugging Face Space" width="760">
|
| 394 |
</p>
|
| 395 |
|
| 396 |
The Space runs the same deterministic local scanner on public GitHub repositories. No provider API call is made.
|
|
|
|
| 57 |
# fastest path: scan a local repository
|
| 58 |
stem /path/to/bio-ai-repo
|
| 59 |
|
| 60 |
+
# 7-page full evidence packet with proof trace
|
| 61 |
stem scan /path/to/bio-ai-repo --level 3 --format all --explain
|
| 62 |
```
|
| 63 |
|
|
|
|
| 175 |
## Output Artifacts
|
| 176 |
|
| 177 |
Each run writes to `--out DIR` (default: `stem_output/`).
|
| 178 |
+
The plain `stem <repo>` and `stem scan <repo>` path now defaults to `--level 3`, which emits the full 7-page evidence packet unless you select a lower level explicitly.
|
| 179 |
|
| 180 |
| Level | Pages | Audience | Artifacts |
|
| 181 |
|-------|------:|---------|-----------|
|
| 182 |
+
| `--level 1` | 1 | Executive / triage (legacy) | Score, tier, stage cards, code integrity summary |
|
| 183 |
+
| `--level 2` | 5 | Standard audit review | Level 1 + Stage 1/2R/3/4 breakdown, AIRI summary, closeout page |
|
| 184 |
+
| `--level 3` | 7 | Full evidence packet | Level 2 + Stage 4 replication page, code integrity deep dive, remediation roadmap, metadata page |
|
| 185 |
|
| 186 |
```
|
| 187 |
<repo>_experiment_results.json # machine-readable score + full evidence object
|
| 188 |
<repo>_report.html # interactive 5-section HTML dashboard (v1.7.0+)
|
| 189 |
<repo>_report.md # human-readable audit report
|
| 190 |
<repo>_brief_1p.pdf # Level 1 executive dashboard
|
| 191 |
+
<repo>_detailed_5p.pdf # Level 2 standard review packet
|
| 192 |
+
<repo>_detailed_7p.pdf # Level 3 full review packet
|
| 193 |
<repo>_explain.txt # --explain: file/line/snippet proof trace
|
| 194 |
```
|
| 195 |
|
|
|
|
| 200 |
`--format html` generates a self-contained interactive dashboard (v1.7.0+). Single `.html` file — no network, no external dependencies.
|
| 201 |
|
| 202 |
<p align="center">
|
| 203 |
+
<img src="https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/assets/html_report_preview.png" alt="STEM BIO-AI interactive HTML dashboard" width="760">
|
| 204 |
</p>
|
| 205 |
|
| 206 |
**Example interactive HTML audit**
|
| 207 |
- Open in browser: <https://htmlpreview.github.io/?https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/examples/maziyarpanahi_openmed_report.html>
|
| 208 |
+
- Raw HTML artifact: [`docs/examples/maziyarpanahi_openmed_report.html`](https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/examples/maziyarpanahi_openmed_report.html)
|
| 209 |
|
| 210 |
**5 sections:** Executive Summary · Score Matrix · Code Integrity (expandable cards) · AIRI Risk Coverage (toggle) · Evidence Detail (filter chips)
|
| 211 |
|
|
|
|
| 220 |
## Report Preview
|
| 221 |
|
| 222 |
<p align="center">
|
| 223 |
+
<img src="https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/assets/report-preview/fieldbioinformatics/page-1.png" alt="STEM BIO-AI standard 5-page packet — page 1" width="760">
|
| 224 |
</p>
|
| 225 |
|
| 226 |
+
**Sample PDF:** [Download the 5-page standard packet preview](https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/assets/report-preview/fieldbioinformatics/artic-network_fieldbioinformatics_detailed_5p.pdf)
|
| 227 |
|
| 228 |
<details>
|
| 229 |
+
<summary>View all 5 standard-packet preview pages</summary>
|
| 230 |
|
| 231 |
| Page 1 | Page 2 |
|
| 232 |
|--------|--------|
|
| 233 |
+
| <img src="https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/assets/report-preview/fieldbioinformatics/page-1.png" alt="Page 1"> | <img src="https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/assets/report-preview/fieldbioinformatics/page-2.png" alt="Page 2"> |
|
| 234 |
|
| 235 |
| Page 3 | Page 4 |
|
| 236 |
|--------|--------|
|
| 237 |
+
| <img src="https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/assets/report-preview/fieldbioinformatics/page-3.png" alt="Page 3"> | <img src="https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/assets/report-preview/fieldbioinformatics/page-4.png" alt="Page 4"> |
|
| 238 |
|
| 239 |
| Page 5 |
|
| 240 |
|--------|
|
| 241 |
+
| <img src="https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/assets/report-preview/fieldbioinformatics/page-5.png" alt="Page 5"> |
|
| 242 |
|
| 243 |
</details>
|
| 244 |
|
|
|
|
| 391 |
Live demo: [huggingface.co/spaces/Flamehaven/stem-bio-ai](https://huggingface.co/spaces/Flamehaven/stem-bio-ai)
|
| 392 |
|
| 393 |
<p align="center">
|
| 394 |
+
<img src="https://raw.githubusercontent.com/flamehaven01/STEM-BIO-AI/main/docs/assets/HF-STEM-BIO_AI.png" alt="STEM BIO-AI Hugging Face Space" width="760">
|
| 395 |
</p>
|
| 396 |
|
| 397 |
The Space runs the same deterministic local scanner on public GitHub repositories. No provider API call is made.
|
docs/CLI_REFERENCE.md
CHANGED
|
@@ -53,7 +53,7 @@ These options work across `scan`, `gate`, and `advisory` workflows unless otherw
|
|
| 53 |
|
| 54 |
| Flag | Values | Default | Description |
|
| 55 |
|------|--------|---------|-------------|
|
| 56 |
-
| `--level` | `1`, `2`, `3` | `
|
| 57 |
| `--format` | `json`, `md`, `pdf`, `all` | command-specific | Artifact format to write |
|
| 58 |
| `--out`, `--output` | `DIR` | `stem_output` | Output directory |
|
| 59 |
| `--policy` | `NAME` | `default` | Named calibration profile to surface in the scan result |
|
|
@@ -243,9 +243,9 @@ Writes artifacts only. Equivalent to `--quiet`.
|
|
| 243 |
|---------|---------|
|
| 244 |
| `<repo>_experiment_results.json` | Machine-readable result object |
|
| 245 |
| `<repo>_report.md` | Human-readable Markdown report |
|
| 246 |
-
| `<repo>_brief_1p.pdf` | Level 1 brief report |
|
| 247 |
-
| `<repo>
|
| 248 |
-
| `<repo>
|
| 249 |
| `<repo>_explain.txt` | Proof trace from `--explain` |
|
| 250 |
|
| 251 |
---
|
|
|
|
| 53 |
|
| 54 |
| Flag | Values | Default | Description |
|
| 55 |
|------|--------|---------|-------------|
|
| 56 |
+
| `--level` | `1`, `2`, `3` | `3` | Report depth: 1=brief 1p (legacy), 2=standard review 5p, 3=full packet 7p |
|
| 57 |
| `--format` | `json`, `md`, `pdf`, `all` | command-specific | Artifact format to write |
|
| 58 |
| `--out`, `--output` | `DIR` | `stem_output` | Output directory |
|
| 59 |
| `--policy` | `NAME` | `default` | Named calibration profile to surface in the scan result |
|
|
|
|
| 243 |
|---------|---------|
|
| 244 |
| `<repo>_experiment_results.json` | Machine-readable result object |
|
| 245 |
| `<repo>_report.md` | Human-readable Markdown report |
|
| 246 |
+
| `<repo>_brief_1p.pdf` | Level 1 brief report (legacy quick brief) |
|
| 247 |
+
| `<repo>_detailed_5p.pdf` | Level 2 standard review packet |
|
| 248 |
+
| `<repo>_detailed_7p.pdf` | Level 3 full packet |
|
| 249 |
| `<repo>_explain.txt` | Proof trace from `--explain` |
|
| 250 |
|
| 251 |
---
|
docs/EXAMPLE_AUDITS.md
CHANGED
|
@@ -16,7 +16,7 @@ This project is easier to evaluate through outputs than through claims. This pag
|
|
| 16 |
- Report preview pages: [`docs/assets/report-preview/fieldbioinformatics/`](assets/report-preview/fieldbioinformatics)
|
| 17 |
- Sample PDF: [`artic-network_fieldbioinformatics_detailed_5p.pdf`](assets/report-preview/fieldbioinformatics/artic-network_fieldbioinformatics_detailed_5p.pdf)
|
| 18 |
|
| 19 |
-
Use this when you want to inspect the 5-page report shape, stage cards, rubric detail, and disclaimer boundary.
|
| 20 |
|
| 21 |
---
|
| 22 |
|
|
|
|
| 16 |
- Report preview pages: [`docs/assets/report-preview/fieldbioinformatics/`](assets/report-preview/fieldbioinformatics)
|
| 17 |
- Sample PDF: [`artic-network_fieldbioinformatics_detailed_5p.pdf`](assets/report-preview/fieldbioinformatics/artic-network_fieldbioinformatics_detailed_5p.pdf)
|
| 18 |
|
| 19 |
+
Use this when you want to inspect the standard 5-page report shape, stage cards, rubric detail, and disclaimer boundary.
|
| 20 |
|
| 21 |
---
|
| 22 |
|
stem_ai/app.py
CHANGED
|
@@ -335,7 +335,7 @@ with gr.Blocks(title=f"STEM BIO-AI — Evidence Scanner v{__version__}", **_bloc
|
|
| 335 |
<div style="{_CARD_STYLE}">
|
| 336 |
<p style="{_CARD_TITLE}">Local CLI</p>
|
| 337 |
<p style="{_CARD_BODY}">Run locally for downloadable artifacts:</p>
|
| 338 |
-
<code style="{_CODE_PILL}">stem <folder> --
|
| 339 |
</div>
|
| 340 |
</div>
|
| 341 |
"""
|
|
@@ -361,13 +361,13 @@ with gr.Blocks(title=f"STEM BIO-AI — Evidence Scanner v{__version__}", **_bloc
|
|
| 361 |
with gr.Column(scale=1):
|
| 362 |
level_input = gr.Radio(
|
| 363 |
choices=[
|
| 364 |
-
("Brief - 1 page", 1),
|
| 365 |
-
("Standard -
|
| 366 |
-
("Full -
|
| 367 |
],
|
| 368 |
-
value=
|
| 369 |
label="Report Level",
|
| 370 |
-
info="
|
| 371 |
)
|
| 372 |
with gr.Row():
|
| 373 |
run_button = gr.Button("Run live GitHub audit", variant="primary", scale=2)
|
|
|
|
| 335 |
<div style="{_CARD_STYLE}">
|
| 336 |
<p style="{_CARD_TITLE}">Local CLI</p>
|
| 337 |
<p style="{_CARD_BODY}">Run locally for downloadable artifacts:</p>
|
| 338 |
+
<code style="{_CODE_PILL}">stem <folder> --format all --explain</code>
|
| 339 |
</div>
|
| 340 |
</div>
|
| 341 |
"""
|
|
|
|
| 361 |
with gr.Column(scale=1):
|
| 362 |
level_input = gr.Radio(
|
| 363 |
choices=[
|
| 364 |
+
("Brief - 1 page (legacy)", 1),
|
| 365 |
+
("Standard - 5 pages", 2),
|
| 366 |
+
("Full - 7 pages", 3),
|
| 367 |
],
|
| 368 |
+
value=3,
|
| 369 |
label="Report Level",
|
| 370 |
+
info="Legacy brief, standard review packet, or full evidence packet.",
|
| 371 |
)
|
| 372 |
with gr.Row():
|
| 373 |
run_button = gr.Button("Run live GitHub audit", variant="primary", scale=2)
|
stem_ai/cli.py
CHANGED
|
@@ -14,8 +14,8 @@ from .scanner import audit_repository
|
|
| 14 |
|
| 15 |
_LEVEL_MAP = {
|
| 16 |
1: ("brief", 1),
|
| 17 |
-
2: ("detailed",
|
| 18 |
-
3: ("detailed",
|
| 19 |
}
|
| 20 |
_FORMAT_CHOICES = ["json", "md", "html", "pdf", "all"]
|
| 21 |
_SUMMARY_CHOICES = ["full", "compact", "off"]
|
|
@@ -203,12 +203,12 @@ def _add_shared_arguments(parser: argparse.ArgumentParser, *, default_format: st
|
|
| 203 |
"--level",
|
| 204 |
type=int,
|
| 205 |
choices=[1, 2, 3],
|
| 206 |
-
default=
|
| 207 |
help=(
|
| 208 |
"Report depth: "
|
| 209 |
-
"1=brief 1-page executive summary
|
| 210 |
-
"2=
|
| 211 |
-
"3=full
|
| 212 |
),
|
| 213 |
)
|
| 214 |
parser.add_argument(
|
|
|
|
| 14 |
|
| 15 |
_LEVEL_MAP = {
|
| 16 |
1: ("brief", 1),
|
| 17 |
+
2: ("detailed", 5),
|
| 18 |
+
3: ("detailed", 7),
|
| 19 |
}
|
| 20 |
_FORMAT_CHOICES = ["json", "md", "html", "pdf", "all"]
|
| 21 |
_SUMMARY_CHOICES = ["full", "compact", "off"]
|
|
|
|
| 203 |
"--level",
|
| 204 |
type=int,
|
| 205 |
choices=[1, 2, 3],
|
| 206 |
+
default=3,
|
| 207 |
help=(
|
| 208 |
"Report depth: "
|
| 209 |
+
"1=brief 1-page executive summary, "
|
| 210 |
+
"2=standard 5-page review packet, "
|
| 211 |
+
"3=full 7-page evidence packet (default)"
|
| 212 |
),
|
| 213 |
)
|
| 214 |
parser.add_argument(
|
stem_ai/render.py
CHANGED
|
@@ -17,6 +17,7 @@ try:
|
|
| 17 |
from reportlab.lib.units import mm
|
| 18 |
from reportlab.platypus import (
|
| 19 |
HRFlowable,
|
|
|
|
| 20 |
PageBreak,
|
| 21 |
Paragraph,
|
| 22 |
SimpleDocTemplate,
|
|
@@ -43,6 +44,8 @@ _DGRAY = "#4A5568"
|
|
| 43 |
_WHITE = "#FFFFFF"
|
| 44 |
|
| 45 |
_TIER_COLOR = {"T0": _RED, "T1": _RED, "T2": _ORANGE, "T3": _TEAL, "T4": _GREEN}
|
|
|
|
|
|
|
| 46 |
_BIO_DETECTOR_LABELS = {
|
| 47 |
"BIO_smiles_surface_integrity": "SMILES Surface Integrity",
|
| 48 |
"BIO_smiles_rdkit_validation": "SMILES RDKit Validation",
|
|
@@ -732,7 +735,7 @@ def _page1_executive(result: dict[str, Any], mode: str, pages: int) -> list[Any]
|
|
| 732 |
story.append(Spacer(1, 2 * mm))
|
| 733 |
story += _regulatory_basis_box(result)
|
| 734 |
story += _footer_block()
|
| 735 |
-
return story
|
| 736 |
|
| 737 |
|
| 738 |
def _header_block(result: dict[str, Any]) -> list[Any]:
|
|
@@ -1064,14 +1067,29 @@ def _footer_block() -> list[Any]:
|
|
| 1064 |
]
|
| 1065 |
|
| 1066 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1067 |
# ── Detail page dispatcher ────────────────────────────────────────────────────
|
| 1068 |
def _detail_pages(result: dict[str, Any], pages: int) -> list[Any]:
|
| 1069 |
story: list[Any] = []
|
| 1070 |
story += _page2_stage_analysis(result)
|
| 1071 |
story += _page3_stage3_analysis(result)
|
| 1072 |
if pages >= 5:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1073 |
story += _page4_integrity_deep(result)
|
| 1074 |
-
story +=
|
|
|
|
| 1075 |
return story
|
| 1076 |
|
| 1077 |
|
|
@@ -1146,7 +1164,7 @@ def _rubric_rows(items: list[tuple[str, str, str, str]], id_prefix: str = "R") -
|
|
| 1146 |
|
| 1147 |
# ── Page 2: Stage 1 + Stage 2R Analysis ──────────────────────────────────────
|
| 1148 |
def _page2_stage_analysis(result: dict[str, Any]) -> list[Any]:
|
| 1149 |
-
story: list[Any] = [
|
| 1150 |
score = result["score"]
|
| 1151 |
cls = result["classification"]
|
| 1152 |
s1 = score["stage_1_readme_intent"]
|
|
@@ -1311,12 +1329,12 @@ def _page2_stage_analysis(result: dict[str, Any]) -> list[Any]:
|
|
| 1311 |
))
|
| 1312 |
|
| 1313 |
story += _footer_block()
|
| 1314 |
-
return story
|
| 1315 |
|
| 1316 |
|
| 1317 |
# ── Page 3: Stage 3 Full Breakdown ───────────────────────────────────────────
|
| 1318 |
def _page3_stage3_analysis(result: dict[str, Any]) -> list[Any]:
|
| 1319 |
-
story: list[Any] = [
|
| 1320 |
score = result["score"]
|
| 1321 |
s3 = score["stage_3_code_bio"]
|
| 1322 |
rubric = result.get("stage_3_rubric", {})
|
|
@@ -1437,12 +1455,75 @@ def _page3_stage3_analysis(result: dict[str, Any]) -> list[Any]:
|
|
| 1437 |
))
|
| 1438 |
|
| 1439 |
story += _footer_block()
|
| 1440 |
-
return story
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1441 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1442 |
|
| 1443 |
-
|
|
|
|
| 1444 |
def _page4_integrity_deep(result: dict[str, Any]) -> list[Any]:
|
| 1445 |
-
story: list[Any] = [
|
| 1446 |
ci = result["code_integrity"]
|
| 1447 |
cls = result["classification"]
|
| 1448 |
hashes = result.get("file_hashes_sha256", {})
|
|
@@ -1608,15 +1689,87 @@ def _page4_integrity_deep(result: dict[str, Any]) -> list[Any]:
|
|
| 1608 |
story.append(hash_tbl)
|
| 1609 |
|
| 1610 |
story += _footer_block()
|
| 1611 |
-
return story
|
| 1612 |
|
| 1613 |
|
| 1614 |
-
# ── Page 5:
|
| 1615 |
-
def
|
| 1616 |
-
story: list[Any] = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1617 |
risks = result.get("notable_risks", [])
|
| 1618 |
positive = result.get("notable_positive_evidence", [])
|
| 1619 |
-
score = result["score"]
|
| 1620 |
airi = result.get("airi_risk_coverage", {})
|
| 1621 |
|
| 1622 |
# Priority Improvements
|
|
@@ -1733,8 +1886,14 @@ def _page5_method_remediation(result: dict[str, Any]) -> list[Any]:
|
|
| 1733 |
_style("MBSCOPE", 8, 11, _DGRAY),
|
| 1734 |
))
|
| 1735 |
|
| 1736 |
-
|
| 1737 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1738 |
story += _sec_hdr("Report Metadata", _NAVY)
|
| 1739 |
tgt = result["target"]
|
| 1740 |
meta_items = [
|
|
@@ -1772,7 +1931,7 @@ def _page5_method_remediation(result: dict[str, Any]) -> list[Any]:
|
|
| 1772 |
]))
|
| 1773 |
story.append(meta_tbl)
|
| 1774 |
story += _footer_block()
|
| 1775 |
-
return story
|
| 1776 |
|
| 1777 |
|
| 1778 |
# ── plain-text PDF fallback (no reportlab) ───────────────────────────────────
|
|
@@ -1830,31 +1989,73 @@ def render_pdf_pages(result: dict[str, Any], mode: str, pages: int) -> list[list
|
|
| 1830 |
f"- {k}: {v['score']} / {v['max']} {v['evidence']}"
|
| 1831 |
for k, v in result.get("stage_4_rubric", {}).items()
|
| 1832 |
]])
|
| 1833 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1834 |
if pages == 5:
|
| 1835 |
-
sets.append(
|
|
|
|
|
|
|
|
|
|
| 1836 |
*[f"- {k}: {v['status']} {v['evidence'][0]}" for k, v in result["code_integrity"].items()],
|
| 1837 |
-
*([f"- AST analysis scope: {ast_note}"] if ast_note else [])
|
| 1838 |
-
|
| 1839 |
"AIRI Coverage Summary",
|
| 1840 |
f"- Covered Risks: {airi.get('covered_count', 0)} / {airi.get('total_risks_in_detector_scope', 0)}",
|
| 1841 |
f"- Coverage Rate: {airi.get('coverage_rate', 0):.3f}",
|
| 1842 |
-
|
| 1843 |
-
|
| 1844 |
-
|
| 1845 |
-
|
| 1846 |
-
|
| 1847 |
-
|
| 1848 |
-
|
| 1849 |
-
|
| 1850 |
-
|
| 1851 |
-
|
|
|
|
| 1852 |
sets.append(_fit_page([
|
| 1853 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1854 |
"",
|
| 1855 |
"Method Boundary",
|
| 1856 |
result["method"],
|
| 1857 |
]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1858 |
return sets[:pages]
|
| 1859 |
|
| 1860 |
|
|
|
|
| 17 |
from reportlab.lib.units import mm
|
| 18 |
from reportlab.platypus import (
|
| 19 |
HRFlowable,
|
| 20 |
+
KeepInFrame,
|
| 21 |
PageBreak,
|
| 22 |
Paragraph,
|
| 23 |
SimpleDocTemplate,
|
|
|
|
| 44 |
_WHITE = "#FFFFFF"
|
| 45 |
|
| 46 |
_TIER_COLOR = {"T0": _RED, "T1": _RED, "T2": _ORANGE, "T3": _TEAL, "T4": _GREEN}
|
| 47 |
+
_PDF_CONTENT_WIDTH = A4[0] - (28 * mm)
|
| 48 |
+
_PDF_CONTENT_HEIGHT = A4[1] - (24 * mm)
|
| 49 |
_BIO_DETECTOR_LABELS = {
|
| 50 |
"BIO_smiles_surface_integrity": "SMILES Surface Integrity",
|
| 51 |
"BIO_smiles_rdkit_validation": "SMILES RDKit Validation",
|
|
|
|
| 735 |
story.append(Spacer(1, 2 * mm))
|
| 736 |
story += _regulatory_basis_box(result)
|
| 737 |
story += _footer_block()
|
| 738 |
+
return _single_page_story(story)
|
| 739 |
|
| 740 |
|
| 741 |
def _header_block(result: dict[str, Any]) -> list[Any]:
|
|
|
|
| 1067 |
]
|
| 1068 |
|
| 1069 |
|
| 1070 |
+
def _single_page_story(flowables: list[Any], *, break_before: bool = False) -> list[Any]:
|
| 1071 |
+
wrapped = KeepInFrame(
|
| 1072 |
+
_PDF_CONTENT_WIDTH,
|
| 1073 |
+
_PDF_CONTENT_HEIGHT,
|
| 1074 |
+
flowables,
|
| 1075 |
+
mode="shrink",
|
| 1076 |
+
)
|
| 1077 |
+
return ([PageBreak()] if break_before else []) + [wrapped]
|
| 1078 |
+
|
| 1079 |
+
|
| 1080 |
# ── Detail page dispatcher ────────────────────────────────────────────────────
|
| 1081 |
def _detail_pages(result: dict[str, Any], pages: int) -> list[Any]:
|
| 1082 |
story: list[Any] = []
|
| 1083 |
story += _page2_stage_analysis(result)
|
| 1084 |
story += _page3_stage3_analysis(result)
|
| 1085 |
if pages >= 5:
|
| 1086 |
+
story += _page4_stage4_replication(result)
|
| 1087 |
+
if pages == 5:
|
| 1088 |
+
story += _page5_compact_closure(result)
|
| 1089 |
+
elif pages >= 7:
|
| 1090 |
story += _page4_integrity_deep(result)
|
| 1091 |
+
story += _page6_method_airi(result)
|
| 1092 |
+
story += _page7_report_metadata(result)
|
| 1093 |
return story
|
| 1094 |
|
| 1095 |
|
|
|
|
| 1164 |
|
| 1165 |
# ── Page 2: Stage 1 + Stage 2R Analysis ──────────────────────────────────────
|
| 1166 |
def _page2_stage_analysis(result: dict[str, Any]) -> list[Any]:
|
| 1167 |
+
story: list[Any] = []
|
| 1168 |
score = result["score"]
|
| 1169 |
cls = result["classification"]
|
| 1170 |
s1 = score["stage_1_readme_intent"]
|
|
|
|
| 1329 |
))
|
| 1330 |
|
| 1331 |
story += _footer_block()
|
| 1332 |
+
return _single_page_story(story, break_before=True)
|
| 1333 |
|
| 1334 |
|
| 1335 |
# ── Page 3: Stage 3 Full Breakdown ───────────────────────────────────────────
|
| 1336 |
def _page3_stage3_analysis(result: dict[str, Any]) -> list[Any]:
|
| 1337 |
+
story: list[Any] = []
|
| 1338 |
score = result["score"]
|
| 1339 |
s3 = score["stage_3_code_bio"]
|
| 1340 |
rubric = result.get("stage_3_rubric", {})
|
|
|
|
| 1455 |
))
|
| 1456 |
|
| 1457 |
story += _footer_block()
|
| 1458 |
+
return _single_page_story(story, break_before=True)
|
| 1459 |
+
|
| 1460 |
+
|
| 1461 |
+
# ── Page 4: Stage 4 Replication Deep Dive (5p/7p) ────────────────────────────
|
| 1462 |
+
def _page4_stage4_replication(result: dict[str, Any]) -> list[Any]:
|
| 1463 |
+
story: list[Any] = []
|
| 1464 |
+
score = result["score"]
|
| 1465 |
+
stage4_score = result.get("replication_score", 0)
|
| 1466 |
+
stage4_tier = result.get("replication_tier", "R0")
|
| 1467 |
+
rubric = result.get("stage_4_rubric", {})
|
| 1468 |
+
|
| 1469 |
+
story += _sec_hdr("Stage 4 — Replication Evidence Lane | Separate lane", _GREEN)
|
| 1470 |
+
|
| 1471 |
+
label_map = {
|
| 1472 |
+
"S4_container_environment": "S4: Container / Runtime Environment",
|
| 1473 |
+
"S4_make_reproduce_target": "S4: Reproduce Target",
|
| 1474 |
+
"S4_environment_lock_evidence": "S4: Environment Lock Evidence",
|
| 1475 |
+
"S4_exact_dependency_pins_or_hashes": "S4: Exact Dependency Pins / Hashes",
|
| 1476 |
+
"S4_readme_reproducibility_section": "S4: Reproducibility Section",
|
| 1477 |
+
"S4_checksum_files": "S4: Checksums / Integrity Files",
|
| 1478 |
+
"S4_dataset_url": "S4: Dataset / Data Source URL",
|
| 1479 |
+
"S4_model_weight_url_or_checksum": "S4: Model Artifact URL / Checksum",
|
| 1480 |
+
"S4_citation_cff": "S4: CITATION.cff",
|
| 1481 |
+
"S4_license_restriction": "S4: License / Use Restriction",
|
| 1482 |
+
"S4_cli_entrypoint": "S4: CLI Entrypoint",
|
| 1483 |
+
"S4_seed_setting": "S4: Deterministic Seed Setting",
|
| 1484 |
+
"S4_runnable_examples": "S4: Runnable Examples",
|
| 1485 |
+
}
|
| 1486 |
|
| 1487 |
+
items: list[tuple[str, str, str, str]] = []
|
| 1488 |
+
for key, item in rubric.items():
|
| 1489 |
+
if not isinstance(item, dict) or "score" not in item or "max" not in item:
|
| 1490 |
+
continue
|
| 1491 |
+
sc = item.get("score", 0)
|
| 1492 |
+
mx = item.get("max", 0)
|
| 1493 |
+
color = _GREEN if sc == mx and mx else (_AMBER if sc > 0 else _RED)
|
| 1494 |
+
evidence = item.get("evidence", "")
|
| 1495 |
+
items.append((label_map.get(key, key), f"{sc} / {mx}", color, evidence))
|
| 1496 |
+
|
| 1497 |
+
chip = _mini_score("S4 Score", stage4_score, 100, _GREEN)
|
| 1498 |
+
tbl = _rubric_rows(items, "S4")
|
| 1499 |
+
combined = Table([[chip, tbl]], colWidths=[38 * mm, None])
|
| 1500 |
+
combined.setStyle(TableStyle([
|
| 1501 |
+
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
| 1502 |
+
("LEFTPADDING", (0, 0), (-1, -1), 2),
|
| 1503 |
+
("RIGHTPADDING", (0, 0), (-1, -1), 2),
|
| 1504 |
+
]))
|
| 1505 |
+
story.append(combined)
|
| 1506 |
+
story.append(Spacer(1, 2 * mm))
|
| 1507 |
+
story.append(Paragraph(
|
| 1508 |
+
f'<font color="{_DGRAY}" size="7.5"><i>Replication tier: {stage4_tier}. '
|
| 1509 |
+
'Stage 4 is reported separately and does not alter the formal score.</i></font>',
|
| 1510 |
+
_style("S4_NOTE", 7.5, 10, _DGRAY),
|
| 1511 |
+
))
|
| 1512 |
+
story.append(Spacer(1, 3 * mm))
|
| 1513 |
+
story.append(Paragraph(
|
| 1514 |
+
f'<font color="{_DGRAY}" size="8">'
|
| 1515 |
+
f'Final score remains <b>{score["final_score"]} / 100</b> ({_xt(score["formal_tier"])}) even when Stage 4 moves. '
|
| 1516 |
+
'This lane exists to show reproducibility and operational evidence posture separately from the formal repository score.'
|
| 1517 |
+
f'</font>',
|
| 1518 |
+
_style("S4_SCOPE", 8, 11, _DGRAY),
|
| 1519 |
+
))
|
| 1520 |
+
story += _footer_block()
|
| 1521 |
+
return _single_page_story(story, break_before=True)
|
| 1522 |
|
| 1523 |
+
|
| 1524 |
+
# ── Page 5: Code Integrity Deep Dive + Classification (7p only) ──────────────
|
| 1525 |
def _page4_integrity_deep(result: dict[str, Any]) -> list[Any]:
|
| 1526 |
+
story: list[Any] = []
|
| 1527 |
ci = result["code_integrity"]
|
| 1528 |
cls = result["classification"]
|
| 1529 |
hashes = result.get("file_hashes_sha256", {})
|
|
|
|
| 1689 |
story.append(hash_tbl)
|
| 1690 |
|
| 1691 |
story += _footer_block()
|
| 1692 |
+
return _single_page_story(story, break_before=True)
|
| 1693 |
|
| 1694 |
|
| 1695 |
+
# ── Page 5: Compact Closeout (5p standard packet only) ───────────────────────
|
| 1696 |
+
def _page5_compact_closure(result: dict[str, Any]) -> list[Any]:
|
| 1697 |
+
story: list[Any] = []
|
| 1698 |
+
score = result["score"]
|
| 1699 |
+
risks = result.get("notable_risks", [])
|
| 1700 |
+
airi = result.get("airi_risk_coverage", {})
|
| 1701 |
+
ci = result["code_integrity"]
|
| 1702 |
+
|
| 1703 |
+
story += _sec_hdr("Closeout Summary", _NAVY)
|
| 1704 |
+
|
| 1705 |
+
ci_rows: list[tuple[str, str, str, str]] = []
|
| 1706 |
+
short = {
|
| 1707 |
+
"C1_hardcoded_credentials": "C1: Hardcoded Credentials",
|
| 1708 |
+
"C2_dependency_pinning": "C2: Dependency Pinning",
|
| 1709 |
+
"C3_dead_or_deprecated_patient_adjacent_paths": "C3: Deprecated Patient Paths",
|
| 1710 |
+
"C4_exception_handling_clinical_adjacent_paths": "C4: Fail-Open Exceptions",
|
| 1711 |
+
"C5_compliance_boundary_integrity": "C5: Compliance Boundary Integrity",
|
| 1712 |
+
"C6_mock_auth_or_fail_open_boundary": "C6: Mock Auth / Fail-Open Boundary",
|
| 1713 |
+
}
|
| 1714 |
+
for key, cfg in ci.items():
|
| 1715 |
+
status = cfg["status"]
|
| 1716 |
+
color = _status_hex(status)
|
| 1717 |
+
evidence = cfg["evidence"][0] if cfg.get("evidence") else ""
|
| 1718 |
+
ci_rows.append((short.get(key, key), status, color, evidence))
|
| 1719 |
+
|
| 1720 |
+
story.append(_rubric_rows(ci_rows, "CC"))
|
| 1721 |
+
story.append(Spacer(1, 3 * mm))
|
| 1722 |
+
|
| 1723 |
+
if risks:
|
| 1724 |
+
story += _sec_hdr("Top Risks", _RED)
|
| 1725 |
+
for risk in risks[:4]:
|
| 1726 |
+
story.append(Paragraph(
|
| 1727 |
+
f'• <font color="{_DGRAY}" size="8">{_xt(risk)}</font>',
|
| 1728 |
+
_style(f"TR_{risk[:6]}", 8, 11, _DGRAY),
|
| 1729 |
+
))
|
| 1730 |
+
if airi:
|
| 1731 |
+
story.append(Spacer(1, 3 * mm))
|
| 1732 |
+
story += _sec_hdr("AIRI Coverage Summary", _TEAL)
|
| 1733 |
+
story.append(Paragraph(
|
| 1734 |
+
f'<font color="{_DGRAY}" size="8">'
|
| 1735 |
+
f'Covered Risks: <b>{airi.get("covered_count", 0)} / {airi.get("total_risks_in_detector_scope", 0)}</b> '
|
| 1736 |
+
f'| Coverage Rate: <b>{airi.get("coverage_rate", 0):.3f}</b></font>',
|
| 1737 |
+
_style("AIRI_COMPACT", 8, 11, _DGRAY),
|
| 1738 |
+
))
|
| 1739 |
+
for risk in airi.get("covered_risks", [])[:3]:
|
| 1740 |
+
reason = _airi_reason_summary(risk)
|
| 1741 |
+
story.append(Paragraph(
|
| 1742 |
+
f'<font color="{_DGRAY}" size="8">• <b>{_xt(str(risk.get("id", "—")))}</b> '
|
| 1743 |
+
f'{_xt(str(risk.get("title", "")))}'
|
| 1744 |
+
f'{f" — why: {_xt(reason)}" if reason else ""}</font>',
|
| 1745 |
+
_style(f"AIRC_{str(risk.get('id', 'risk'))[:8]}", 8, 10, _DGRAY),
|
| 1746 |
+
))
|
| 1747 |
+
if airi.get("known_gaps_in_bundle"):
|
| 1748 |
+
gap_preview = ", ".join(
|
| 1749 |
+
f"{g.get('id', '—')} {_xt(str(g.get('title', '')))}"
|
| 1750 |
+
for g in airi.get("known_gaps_in_bundle", [])[:2]
|
| 1751 |
+
)
|
| 1752 |
+
story.append(Paragraph(
|
| 1753 |
+
f'<font color="{_DGRAY}" size="8">Known gaps preview: {gap_preview}</font>',
|
| 1754 |
+
_style("AIRI_GAPC", 8, 10, _DGRAY),
|
| 1755 |
+
))
|
| 1756 |
+
|
| 1757 |
+
story.append(Spacer(1, 3 * mm))
|
| 1758 |
+
story += _sec_hdr("Method Boundary", _DGRAY)
|
| 1759 |
+
story.append(Paragraph(
|
| 1760 |
+
f'<font color="{_DGRAY}" size="8"><b>Final score:</b> {score["final_score"]} / 100 ({_xt(score["formal_tier"])})'
|
| 1761 |
+
f'<br/><b>Method:</b> {_xt(result.get("method", ""))}</font>',
|
| 1762 |
+
_style("MBCOMPACT", 8, 11, _DGRAY),
|
| 1763 |
+
))
|
| 1764 |
+
story += _footer_block()
|
| 1765 |
+
return _single_page_story(story, break_before=True)
|
| 1766 |
+
|
| 1767 |
+
|
| 1768 |
+
# ── Page 6: Priority Improvements + AIRI + Method (7p only) ─────────────────
|
| 1769 |
+
def _page6_method_airi(result: dict[str, Any]) -> list[Any]:
|
| 1770 |
+
story: list[Any] = []
|
| 1771 |
risks = result.get("notable_risks", [])
|
| 1772 |
positive = result.get("notable_positive_evidence", [])
|
|
|
|
| 1773 |
airi = result.get("airi_risk_coverage", {})
|
| 1774 |
|
| 1775 |
# Priority Improvements
|
|
|
|
| 1886 |
_style("MBSCOPE", 8, 11, _DGRAY),
|
| 1887 |
))
|
| 1888 |
|
| 1889 |
+
story += _footer_block()
|
| 1890 |
+
return _single_page_story(story, break_before=True)
|
| 1891 |
+
|
| 1892 |
+
|
| 1893 |
+
# ── Page 7: Report Metadata (7p only) ────────────────────────────────────────
|
| 1894 |
+
def _page7_report_metadata(result: dict[str, Any]) -> list[Any]:
|
| 1895 |
+
story: list[Any] = []
|
| 1896 |
+
score = result["score"]
|
| 1897 |
story += _sec_hdr("Report Metadata", _NAVY)
|
| 1898 |
tgt = result["target"]
|
| 1899 |
meta_items = [
|
|
|
|
| 1931 |
]))
|
| 1932 |
story.append(meta_tbl)
|
| 1933 |
story += _footer_block()
|
| 1934 |
+
return _single_page_story(story, break_before=True)
|
| 1935 |
|
| 1936 |
|
| 1937 |
# ── plain-text PDF fallback (no reportlab) ───────────────────────────────────
|
|
|
|
| 1989 |
f"- {k}: {v['score']} / {v['max']} {v['evidence']}"
|
| 1990 |
for k, v in result.get("stage_4_rubric", {}).items()
|
| 1991 |
]])
|
| 1992 |
+
p4 = _fit_page([
|
| 1993 |
+
"Stage 4 Replication Evidence",
|
| 1994 |
+
f"- Stage 4 Replication Score: {result.get('replication_score', 0)} / 100 ({result.get('replication_tier', 'R0')})",
|
| 1995 |
+
*[
|
| 1996 |
+
f"- {k}: {v['score']} / {v['max']} {v['evidence']}"
|
| 1997 |
+
for k, v in result.get("stage_4_rubric", {}).items()
|
| 1998 |
+
],
|
| 1999 |
+
])
|
| 2000 |
+
sets = [_fit_page(brief), p2, _fit_page([
|
| 2001 |
+
"Stage 3 Evidence",
|
| 2002 |
+
*[f"- {k}: {v['score']} / {v['max']} {v['evidence']}" for k, v in result["stage_3_rubric"].items()]
|
| 2003 |
+
])]
|
| 2004 |
if pages == 5:
|
| 2005 |
+
sets.append(p4)
|
| 2006 |
+
sets.append(_fit_page([
|
| 2007 |
+
"Closeout Summary",
|
| 2008 |
+
"Code Integrity",
|
| 2009 |
*[f"- {k}: {v['status']} {v['evidence'][0]}" for k, v in result["code_integrity"].items()],
|
| 2010 |
+
*([f"- AST analysis scope: {ast_note}"] if ast_note else []),
|
| 2011 |
+
"",
|
| 2012 |
"AIRI Coverage Summary",
|
| 2013 |
f"- Covered Risks: {airi.get('covered_count', 0)} / {airi.get('total_risks_in_detector_scope', 0)}",
|
| 2014 |
f"- Coverage Rate: {airi.get('coverage_rate', 0):.3f}",
|
| 2015 |
+
*[
|
| 2016 |
+
f"- {risk.get('id', '—')}: {risk.get('title', '')}"
|
| 2017 |
+
+ (f" | why: {_airi_reason_summary(risk)}" if _airi_reason_summary(risk) else "")
|
| 2018 |
+
for risk in airi.get("covered_risks", [])[:2]
|
| 2019 |
+
],
|
| 2020 |
+
"",
|
| 2021 |
+
"Method Boundary",
|
| 2022 |
+
result["method"],
|
| 2023 |
+
]))
|
| 2024 |
+
elif pages >= 7:
|
| 2025 |
+
sets.append(p4)
|
| 2026 |
sets.append(_fit_page([
|
| 2027 |
+
"Code Integrity",
|
| 2028 |
+
*[f"- {k}: {v['status']} {v['evidence'][0]}" for k, v in result["code_integrity"].items()],
|
| 2029 |
+
*([f"- AST analysis scope: {ast_note}"] if ast_note else []),
|
| 2030 |
+
]))
|
| 2031 |
+
sets.append(_fit_page([
|
| 2032 |
+
"Priority Improvement Roadmap",
|
| 2033 |
+
*[f"- {r}" for r in result.get("notable_risks", [])[:4]],
|
| 2034 |
+
"",
|
| 2035 |
+
"AIRI Coverage Summary",
|
| 2036 |
+
f"- Covered Risks: {airi.get('covered_count', 0)} / {airi.get('total_risks_in_detector_scope', 0)}",
|
| 2037 |
+
f"- Coverage Rate: {airi.get('coverage_rate', 0):.3f}",
|
| 2038 |
+
f"- Bundle Scope: {airi.get('airi_bundle_scope', 'unknown')}",
|
| 2039 |
+
*[
|
| 2040 |
+
f"- {risk.get('id', '—')}: {risk.get('title', '')}"
|
| 2041 |
+
+ (f" | why: {_airi_reason_summary(risk)}" if _airi_reason_summary(risk) else "")
|
| 2042 |
+
for risk in airi.get("covered_risks", [])[:3]
|
| 2043 |
+
],
|
| 2044 |
"",
|
| 2045 |
"Method Boundary",
|
| 2046 |
result["method"],
|
| 2047 |
]))
|
| 2048 |
+
sets.append(_fit_page([
|
| 2049 |
+
"Report Metadata",
|
| 2050 |
+
f"- Schema Version: {result.get('schema_version', '—')}",
|
| 2051 |
+
f"- STEM BIO-AI Version: {result.get('stem_ai_version', '—')}",
|
| 2052 |
+
f"- Generated (local date): {result.get('generated_at_local', '—')}",
|
| 2053 |
+
f"- Repository: {result['target']['name']}",
|
| 2054 |
+
f"- Branch: {result['target'].get('branch') or '—'}",
|
| 2055 |
+
f"- Commit (HEAD): {(result['target'].get('commit') or '—')[:40]}",
|
| 2056 |
+
f"- Files Scanned: {result['target'].get('file_count', '—')}",
|
| 2057 |
+
f"- Final Score / Tier: {score['final_score']} / 100 — {score['formal_tier']}",
|
| 2058 |
+
]))
|
| 2059 |
return sets[:pages]
|
| 2060 |
|
| 2061 |
|
stem_ai/render_html.py
CHANGED
|
@@ -35,7 +35,7 @@ def _calibration_effect_note(calibration: dict[str, Any]) -> str | None:
|
|
| 35 |
)
|
| 36 |
|
| 37 |
|
| 38 |
-
def _nav() -> str:
|
| 39 |
links = [
|
| 40 |
("#s1", "1. Summary"),
|
| 41 |
("#s2", "2. Decision Path"),
|
|
@@ -44,18 +44,39 @@ def _nav() -> str:
|
|
| 44 |
("#s5", "5. Evidence"),
|
| 45 |
]
|
| 46 |
items = "".join(f'<a href="{h}" class="nav-link">{l}</a>' for h, l in links)
|
| 47 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
def _hero(score: dict, final: int, tier: str, tc: str, target: str, date: str) -> str:
|
| 51 |
-
gauge = svg_gauge(final, tc)
|
| 52 |
use_scope = xt(str(score.get("use_scope", "")))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
return (
|
| 54 |
f'<header class="hero">'
|
| 55 |
-
f'<div class="hero-left">{gauge}</div>'
|
| 56 |
f'<div class="hero-right">'
|
| 57 |
f'<div class="eyebrow">STEM BIO-AI Local Audit | {date}</div>'
|
| 58 |
-
f'<h1>{
|
| 59 |
f'<div class="hero-meta">'
|
| 60 |
f'<span class="tier">{xt(tier)}</span>'
|
| 61 |
f'<span class="hero-chip">Deterministic local scan</span>'
|
|
@@ -350,7 +371,7 @@ def _section3(integrity: dict, cc: dict) -> str:
|
|
| 350 |
return (
|
| 351 |
f'<section id="s3">'
|
| 352 |
f'<h2 class="s-title">Code Integrity & Contract {hint}</h2>'
|
| 353 |
-
f'<div class="integrity-
|
| 354 |
f'<div class="panel">'
|
| 355 |
f'<div class="eyebrow">Warnings First</div>'
|
| 356 |
f'<h3 class="subhead">Mapped risk lanes that fired</h3>'
|
|
@@ -376,15 +397,24 @@ def _section4(airi: dict) -> str:
|
|
| 376 |
all_risks = airi.get("covered_risks", [])
|
| 377 |
donut = svg_donut(pct, _C["green"], 98)
|
| 378 |
|
| 379 |
-
|
|
|
|
| 380 |
for r in all_risks:
|
| 381 |
try:
|
| 382 |
d = int(str(r.get("subdomain_id", "0")).split(".")[0])
|
| 383 |
-
if d in
|
| 384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
except (ValueError, IndexError):
|
| 386 |
continue
|
| 387 |
-
domain_boxes =
|
|
|
|
| 388 |
|
| 389 |
c_rows = "".join(airi_row(r, "covered") for r in all_risks[:24])
|
| 390 |
g_rows = "".join(airi_row(g, "gap") for g in gaps)
|
|
@@ -394,7 +424,7 @@ def _section4(airi: dict) -> str:
|
|
| 394 |
f'<button class="toggle-btn" data-view="covered" onclick="airiToggle(\'covered\')">Covered ({covered_n})</button>'
|
| 395 |
f'<button class="toggle-btn" data-view="gaps" onclick="airiToggle(\'gaps\')">Gaps ({len(gaps)})</button>'
|
| 396 |
f'</div>'
|
| 397 |
-
f'<span class="muted-note">Click a domain to filter.</span>'
|
| 398 |
f'</div>'
|
| 399 |
)
|
| 400 |
table = (
|
|
@@ -448,7 +478,7 @@ def _section4(airi: dict) -> str:
|
|
| 448 |
return (
|
| 449 |
f'<section id="s4">'
|
| 450 |
f'<h2 class="s-title">MIT AI Risk Repository Coverage {hint}<span class="airi-tag">{src} | airisk.mit.edu</span></h2>'
|
| 451 |
-
f'<div class="airi-
|
| 452 |
f'<div class="panel">'
|
| 453 |
f'<div class="eyebrow">Feature Explainer</div>'
|
| 454 |
f'<h3 class="subhead">What this section is doing</h3>'
|
|
@@ -521,6 +551,7 @@ def render_html(result: dict[str, Any]) -> str:
|
|
| 521 |
tier = str(score["formal_tier"])
|
| 522 |
tc = tier_color(tier)
|
| 523 |
target = xt(str(result["target"]["name"]))
|
|
|
|
| 524 |
date = xt(str(result.get("generated_at_local", "")))
|
| 525 |
s1 = int(score.get("stage_1_readme_intent", 0))
|
| 526 |
s2 = int(score.get("stage_2_repo_local_consistency", 0))
|
|
@@ -529,7 +560,6 @@ def render_html(result: dict[str, Any]) -> str:
|
|
| 529 |
t0 = result.get("classification", {}).get("t0_hard_floor", False)
|
| 530 |
|
| 531 |
css = build_css(tc)
|
| 532 |
-
nav = _nav()
|
| 533 |
hero = _hero(score, final, tier, tc, target, date)
|
| 534 |
sec1 = _section1(result, final, tc, s1, s2, s3, s4, t0)
|
| 535 |
sec2 = _section2(result, s1, s2, s3, s4)
|
|
@@ -537,6 +567,7 @@ def render_html(result: dict[str, Any]) -> str:
|
|
| 537 |
sec4 = _section4(result.get("airi_risk_coverage", {}))
|
| 538 |
sec5 = _section5(result.get("evidence_ledger", []))
|
| 539 |
ver = xt(result.get("stem_ai_version", ""))
|
|
|
|
| 540 |
|
| 541 |
return f"""<!DOCTYPE html>
|
| 542 |
<html lang="en">
|
|
|
|
| 35 |
)
|
| 36 |
|
| 37 |
|
| 38 |
+
def _nav(ver: str) -> str:
|
| 39 |
links = [
|
| 40 |
("#s1", "1. Summary"),
|
| 41 |
("#s2", "2. Decision Path"),
|
|
|
|
| 44 |
("#s5", "5. Evidence"),
|
| 45 |
]
|
| 46 |
items = "".join(f'<a href="{h}" class="nav-link">{l}</a>' for h, l in links)
|
| 47 |
+
return (
|
| 48 |
+
f'<nav class="nav" aria-label="Report sections">'
|
| 49 |
+
f'<div class="nav-links">{items}</div>'
|
| 50 |
+
f'<div class="nav-brand"><strong>STEM-BIO-AI</strong><span>STEM BIO-AI Local CLI Scan | {ver}</span></div>'
|
| 51 |
+
f'</nav>'
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _hero_accent(tc: str) -> str:
|
| 56 |
+
if tc == _C["red"]:
|
| 57 |
+
return "#D65D6D"
|
| 58 |
+
if tc == _C["amber"]:
|
| 59 |
+
return "#D28F22"
|
| 60 |
+
if tc == _C["green"]:
|
| 61 |
+
return "#2A8A56"
|
| 62 |
+
return "#3B86BE"
|
| 63 |
|
| 64 |
|
| 65 |
def _hero(score: dict, final: int, tier: str, tc: str, target: str, date: str) -> str:
|
| 66 |
+
gauge = svg_gauge(final, _hero_accent(tc))
|
| 67 |
use_scope = xt(str(score.get("use_scope", "")))
|
| 68 |
+
remote = xt(str(score.get("_target_remote", "")))
|
| 69 |
+
title = (
|
| 70 |
+
f'<a href="{remote}" class="hero-link" target="_blank" rel="noopener noreferrer">{target}</a>'
|
| 71 |
+
if remote
|
| 72 |
+
else target
|
| 73 |
+
)
|
| 74 |
return (
|
| 75 |
f'<header class="hero">'
|
| 76 |
+
f'<div class="hero-left"><div class="hero-gauge-card">{gauge}</div></div>'
|
| 77 |
f'<div class="hero-right">'
|
| 78 |
f'<div class="eyebrow">STEM BIO-AI Local Audit | {date}</div>'
|
| 79 |
+
f'<h1>{title}</h1>'
|
| 80 |
f'<div class="hero-meta">'
|
| 81 |
f'<span class="tier">{xt(tier)}</span>'
|
| 82 |
f'<span class="hero-chip">Deterministic local scan</span>'
|
|
|
|
| 371 |
return (
|
| 372 |
f'<section id="s3">'
|
| 373 |
f'<h2 class="s-title">Code Integrity & Contract {hint}</h2>'
|
| 374 |
+
f'<div class="integrity-stack">'
|
| 375 |
f'<div class="panel">'
|
| 376 |
f'<div class="eyebrow">Warnings First</div>'
|
| 377 |
f'<h3 class="subhead">Mapped risk lanes that fired</h3>'
|
|
|
|
| 397 |
all_risks = airi.get("covered_risks", [])
|
| 398 |
donut = svg_donut(pct, _C["green"], 98)
|
| 399 |
|
| 400 |
+
covered_counts: dict[int, int] = {d: 0 for d in range(1, 8)}
|
| 401 |
+
gap_counts: dict[int, int] = {d: 0 for d in range(1, 8)}
|
| 402 |
for r in all_risks:
|
| 403 |
try:
|
| 404 |
d = int(str(r.get("subdomain_id", "0")).split(".")[0])
|
| 405 |
+
if d in covered_counts:
|
| 406 |
+
covered_counts[d] += 1
|
| 407 |
+
except (ValueError, IndexError):
|
| 408 |
+
continue
|
| 409 |
+
for g in gaps:
|
| 410 |
+
try:
|
| 411 |
+
d = int(str(g.get("subdomain_id", "0")).split(".")[0])
|
| 412 |
+
if d in gap_counts:
|
| 413 |
+
gap_counts[d] += 1
|
| 414 |
except (ValueError, IndexError):
|
| 415 |
continue
|
| 416 |
+
domain_boxes = domain_card(0, covered_n, len(gaps))
|
| 417 |
+
domain_boxes += "".join(domain_card(d, covered_counts[d], gap_counts[d]) for d in range(1, 8))
|
| 418 |
|
| 419 |
c_rows = "".join(airi_row(r, "covered") for r in all_risks[:24])
|
| 420 |
g_rows = "".join(airi_row(g, "gap") for g in gaps)
|
|
|
|
| 424 |
f'<button class="toggle-btn" data-view="covered" onclick="airiToggle(\'covered\')">Covered ({covered_n})</button>'
|
| 425 |
f'<button class="toggle-btn" data-view="gaps" onclick="airiToggle(\'gaps\')">Gaps ({len(gaps)})</button>'
|
| 426 |
f'</div>'
|
| 427 |
+
f'<span class="muted-note">Click a domain card to filter. Counts are shown as covered / gaps.</span>'
|
| 428 |
f'</div>'
|
| 429 |
)
|
| 430 |
table = (
|
|
|
|
| 478 |
return (
|
| 479 |
f'<section id="s4">'
|
| 480 |
f'<h2 class="s-title">MIT AI Risk Repository Coverage {hint}<span class="airi-tag">{src} | airisk.mit.edu</span></h2>'
|
| 481 |
+
f'<div class="airi-stack">'
|
| 482 |
f'<div class="panel">'
|
| 483 |
f'<div class="eyebrow">Feature Explainer</div>'
|
| 484 |
f'<h3 class="subhead">What this section is doing</h3>'
|
|
|
|
| 551 |
tier = str(score["formal_tier"])
|
| 552 |
tc = tier_color(tier)
|
| 553 |
target = xt(str(result["target"]["name"]))
|
| 554 |
+
score["_target_remote"] = str(result.get("target", {}).get("remote", "")).removesuffix(".git")
|
| 555 |
date = xt(str(result.get("generated_at_local", "")))
|
| 556 |
s1 = int(score.get("stage_1_readme_intent", 0))
|
| 557 |
s2 = int(score.get("stage_2_repo_local_consistency", 0))
|
|
|
|
| 560 |
t0 = result.get("classification", {}).get("t0_hard_floor", False)
|
| 561 |
|
| 562 |
css = build_css(tc)
|
|
|
|
| 563 |
hero = _hero(score, final, tier, tc, target, date)
|
| 564 |
sec1 = _section1(result, final, tc, s1, s2, s3, s4, t0)
|
| 565 |
sec2 = _section2(result, s1, s2, s3, s4)
|
|
|
|
| 567 |
sec4 = _section4(result.get("airi_risk_coverage", {}))
|
| 568 |
sec5 = _section5(result.get("evidence_ledger", []))
|
| 569 |
ver = xt(result.get("stem_ai_version", ""))
|
| 570 |
+
nav = _nav(ver)
|
| 571 |
|
| 572 |
return f"""<!DOCTYPE html>
|
| 573 |
<html lang="en">
|
stem_ai/render_html_components.py
CHANGED
|
@@ -158,16 +158,23 @@ def integrity_card(key: str, info: dict[str, Any]) -> str:
|
|
| 158 |
)
|
| 159 |
|
| 160 |
|
| 161 |
-
def domain_card(d_num: int,
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
return (
|
| 166 |
f'<div class="domain-card" data-domain="{d_num}"'
|
|
|
|
| 167 |
f' onclick="filterDomain({d_num})" style="{dim}">'
|
| 168 |
-
f'<span class="domain-num" style="background:{color}">{d_num}</span>'
|
| 169 |
f'<span class="domain-label">{name}</span>'
|
| 170 |
-
f'<span class="domain-cnt" style="color:{color}">{
|
| 171 |
f'</div>'
|
| 172 |
)
|
| 173 |
|
|
|
|
| 158 |
)
|
| 159 |
|
| 160 |
|
| 161 |
+
def domain_card(d_num: int, covered_count: int, gap_count: int = 0) -> str:
|
| 162 |
+
if d_num == 0:
|
| 163 |
+
color = _C["navy"]
|
| 164 |
+
name = "All Domains"
|
| 165 |
+
else:
|
| 166 |
+
color = _AIRI_DOMAIN_COLORS.get(d_num, _C["dgray"])
|
| 167 |
+
name = _AIRI_DOMAIN_NAMES.get(d_num, f"Domain {d_num}")
|
| 168 |
+
total = covered_count + gap_count
|
| 169 |
+
dim = "opacity:.55;cursor:pointer" if total == 0 else "cursor:pointer"
|
| 170 |
+
badge = f"{covered_count}/{gap_count}"
|
| 171 |
return (
|
| 172 |
f'<div class="domain-card" data-domain="{d_num}"'
|
| 173 |
+
f' data-covered="{covered_count}" data-gaps="{gap_count}"'
|
| 174 |
f' onclick="filterDomain({d_num})" style="{dim}">'
|
| 175 |
+
f'<span class="domain-num" style="background:{color}">{"All" if d_num == 0 else d_num}</span>'
|
| 176 |
f'<span class="domain-label">{name}</span>'
|
| 177 |
+
f'<span class="domain-cnt" style="color:{color}">{badge}</span>'
|
| 178 |
f'</div>'
|
| 179 |
)
|
| 180 |
|
stem_ai/render_html_styles.py
CHANGED
|
@@ -44,21 +44,27 @@ h1,h2,h3,.stage-name,.metric-value,.tier{{
|
|
| 44 |
position:sticky;top:0;z-index:60;
|
| 45 |
background:rgba(255,255,255,.86);
|
| 46 |
backdrop-filter:blur(14px);
|
| 47 |
-
display:flex;
|
| 48 |
border-bottom:1px solid rgba(13,31,60,.08);
|
| 49 |
box-shadow:0 8px 24px rgba(13,31,60,.06);
|
|
|
|
| 50 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
.nav-link{{
|
| 52 |
padding:14px 18px;color:{dg};font-size:12px;font-weight:700;
|
| 53 |
text-decoration:none;transition:color .2s,background .2s,border-color .2s;
|
| 54 |
border-bottom:3px solid transparent;white-space:nowrap;
|
| 55 |
}}
|
| 56 |
-
.nav-link:hover{{color:{n};background:rgba(13,31,60,.04)}}
|
| 57 |
.nav-link.active{{color:{n};border-bottom-color:{t}}}
|
| 58 |
.hero{{
|
| 59 |
background:
|
| 60 |
-
radial-gradient(circle at top right, rgba(255,255,255,.
|
| 61 |
-
|
|
|
|
| 62 |
color:#fff;
|
| 63 |
padding:42px 48px 46px;
|
| 64 |
display:grid;
|
|
@@ -69,8 +75,17 @@ h1,h2,h3,.stage-name,.metric-value,.tier{{
|
|
| 69 |
.hero-left{{display:flex;justify-content:center}}
|
| 70 |
.hero-right{{display:flex;flex-direction:column;gap:10px}}
|
| 71 |
.hero h1{{font-size:34px;font-weight:700;line-height:1.1;letter-spacing:-.02em}}
|
|
|
|
|
|
|
| 72 |
.eyebrow{{font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:rgba(13,31,60,.62)}}
|
| 73 |
-
.hero .eyebrow{{color:rgba(255,255,255,.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
.hero-meta{{display:flex;gap:10px;flex-wrap:wrap;align-items:center}}
|
| 75 |
.tier{{
|
| 76 |
display:inline-block;background:{tc};color:#fff;font-size:13px;font-weight:700;
|
|
@@ -93,7 +108,15 @@ section{{margin-bottom:34px;scroll-margin-top:72px}}
|
|
| 93 |
padding:24px;
|
| 94 |
box-shadow:var(--shadow-soft);
|
| 95 |
}}
|
| 96 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
.memo-card,.stage-card{{
|
| 98 |
background:{w};
|
| 99 |
border:1px solid rgba(13,31,60,.08);
|
|
@@ -203,7 +226,7 @@ section{{margin-bottom:34px;scroll-margin-top:72px}}
|
|
| 203 |
.risk-item:hover{{border-left-color:{r};box-shadow:0 2px 8px rgba(0,0,0,.08)}}
|
| 204 |
.domain-grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px;margin-bottom:18px}}
|
| 205 |
.domain-card{{display:flex;align-items:center;gap:10px;padding:11px 14px;background:{w};border:1.5px solid {mg};border-radius:12px;transition:border-color .15s,box-shadow .15s;user-select:none}}
|
| 206 |
-
.domain-card:hover{{border-color:{t};box-shadow:
|
| 207 |
.domain-card.domain-active{{border-color:var(--d-color,{t});box-shadow:0 0 0 3px color-mix(in srgb,var(--d-color,{t}) 18%,transparent)}}
|
| 208 |
.domain-num{{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;color:#fff;font-size:11px;font-weight:800;flex-shrink:0}}
|
| 209 |
.domain-label{{font-size:11px;color:{n};font-weight:700;flex:1;line-height:1.3}}
|
|
@@ -225,13 +248,14 @@ section{{margin-bottom:34px;scroll-margin-top:72px}}
|
|
| 225 |
.metric-grid{{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
| 226 |
.stage-deck{{grid-template-columns:1fr}}
|
| 227 |
.memo-card,.memo-card:nth-child(1),.memo-card:nth-child(2),.memo-card:nth-child(3),.memo-card:nth-child(4){{grid-column:span 12}}
|
| 228 |
-
.integrity-layout,.airi-layout{{grid-template-columns:1fr}}
|
| 229 |
.config-pattern{{grid-template-columns:1fr}}
|
| 230 |
.config-grid{{grid-template-columns:1fr}}
|
| 231 |
}}
|
| 232 |
@media(max-width:760px){{
|
| 233 |
.hero{{padding:28px 22px;grid-template-columns:1fr}}
|
| 234 |
.content{{padding:24px 14px 42px}}
|
|
|
|
|
|
|
| 235 |
.nav-link{{padding:12px 12px;font-size:11px}}
|
| 236 |
.metric-grid{{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
| 237 |
}}
|
|
@@ -254,12 +278,31 @@ function toggleCard(card) {
|
|
| 254 |
var _airiView = 'covered';
|
| 255 |
var _airiDomain = 0;
|
| 256 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
function _applyAiriFilter() {
|
| 258 |
document.querySelectorAll('.airi-covered,.airi-gaps').forEach(function(r) {
|
| 259 |
var viewOk = r.classList.contains('airi-' + _airiView);
|
| 260 |
var domainOk = (_airiDomain === 0 || r.dataset.domain == _airiDomain);
|
| 261 |
r.style.display = (viewOk && domainOk) ? '' : 'none';
|
| 262 |
});
|
|
|
|
| 263 |
}
|
| 264 |
|
| 265 |
function airiToggle(view) {
|
|
|
|
| 44 |
position:sticky;top:0;z-index:60;
|
| 45 |
background:rgba(255,255,255,.86);
|
| 46 |
backdrop-filter:blur(14px);
|
| 47 |
+
display:flex;justify-content:space-between;align-items:center;gap:16px;
|
| 48 |
border-bottom:1px solid rgba(13,31,60,.08);
|
| 49 |
box-shadow:0 8px 24px rgba(13,31,60,.06);
|
| 50 |
+
padding-right:16px;
|
| 51 |
}}
|
| 52 |
+
.nav-links{{display:flex;gap:4px;overflow-x:auto;flex:1;min-width:0}}
|
| 53 |
+
.nav-brand{{display:grid;justify-items:end;flex-shrink:0;white-space:nowrap;padding-left:12px;border-left:1px solid rgba(13,31,60,.08)}}
|
| 54 |
+
.nav-brand strong{{font-size:11px;letter-spacing:.08em;color:{n}}}
|
| 55 |
+
.nav-brand span{{font-size:10px;color:{dg};font-weight:700}}
|
| 56 |
.nav-link{{
|
| 57 |
padding:14px 18px;color:{dg};font-size:12px;font-weight:700;
|
| 58 |
text-decoration:none;transition:color .2s,background .2s,border-color .2s;
|
| 59 |
border-bottom:3px solid transparent;white-space:nowrap;
|
| 60 |
}}
|
| 61 |
+
.nav-link:hover{{color:{n};background:rgba(13,31,60,.04);transform:translateY(-1px)}}
|
| 62 |
.nav-link.active{{color:{n};border-bottom-color:{t}}}
|
| 63 |
.hero{{
|
| 64 |
background:
|
| 65 |
+
radial-gradient(circle at top right, rgba(255,255,255,.35), transparent 28%),
|
| 66 |
+
radial-gradient(circle at top left, rgba(255,255,255,.26), transparent 26%),
|
| 67 |
+
linear-gradient(135deg, #2B547B 0%, #3C6C96 46%, #6E9FBE 100%);
|
| 68 |
color:#fff;
|
| 69 |
padding:42px 48px 46px;
|
| 70 |
display:grid;
|
|
|
|
| 75 |
.hero-left{{display:flex;justify-content:center}}
|
| 76 |
.hero-right{{display:flex;flex-direction:column;gap:10px}}
|
| 77 |
.hero h1{{font-size:34px;font-weight:700;line-height:1.1;letter-spacing:-.02em}}
|
| 78 |
+
.hero-link{{color:#fff;text-decoration:none;border-bottom:1px solid transparent}}
|
| 79 |
+
.hero-link:hover{{border-bottom-color:rgba(255,255,255,.72)}}
|
| 80 |
.eyebrow{{font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:rgba(13,31,60,.62)}}
|
| 81 |
+
.hero .eyebrow{{color:rgba(255,255,255,.82)}}
|
| 82 |
+
.hero-gauge-card{{
|
| 83 |
+
background:rgba(255,255,255,.96);
|
| 84 |
+
border:1px solid rgba(255,255,255,.45);
|
| 85 |
+
border-radius:22px;
|
| 86 |
+
padding:18px 20px 14px;
|
| 87 |
+
box-shadow:0 18px 42px rgba(8,22,38,.18);
|
| 88 |
+
}}
|
| 89 |
.hero-meta{{display:flex;gap:10px;flex-wrap:wrap;align-items:center}}
|
| 90 |
.tier{{
|
| 91 |
display:inline-block;background:{tc};color:#fff;font-size:13px;font-weight:700;
|
|
|
|
| 108 |
padding:24px;
|
| 109 |
box-shadow:var(--shadow-soft);
|
| 110 |
}}
|
| 111 |
+
.panel:hover,
|
| 112 |
+
.metric-card:hover,
|
| 113 |
+
.memo-card:hover,
|
| 114 |
+
.stage-card:hover{{
|
| 115 |
+
box-shadow:var(--shadow-strong);
|
| 116 |
+
transform:translateY(-2px);
|
| 117 |
+
}}
|
| 118 |
+
.memo-grid{{display:grid;grid-template-columns:repeat(12,1fr);gap:18px}}
|
| 119 |
+
.integrity-stack,.airi-stack{{display:grid;grid-template-columns:1fr;gap:18px}}
|
| 120 |
.memo-card,.stage-card{{
|
| 121 |
background:{w};
|
| 122 |
border:1px solid rgba(13,31,60,.08);
|
|
|
|
| 226 |
.risk-item:hover{{border-left-color:{r};box-shadow:0 2px 8px rgba(0,0,0,.08)}}
|
| 227 |
.domain-grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px;margin-bottom:18px}}
|
| 228 |
.domain-card{{display:flex;align-items:center;gap:10px;padding:11px 14px;background:{w};border:1.5px solid {mg};border-radius:12px;transition:border-color .15s,box-shadow .15s;user-select:none}}
|
| 229 |
+
.domain-card:hover{{border-color:{t};box-shadow:var(--shadow-strong);transform:translateY(-2px)}}
|
| 230 |
.domain-card.domain-active{{border-color:var(--d-color,{t});box-shadow:0 0 0 3px color-mix(in srgb,var(--d-color,{t}) 18%,transparent)}}
|
| 231 |
.domain-num{{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;color:#fff;font-size:11px;font-weight:800;flex-shrink:0}}
|
| 232 |
.domain-label{{font-size:11px;color:{n};font-weight:700;flex:1;line-height:1.3}}
|
|
|
|
| 248 |
.metric-grid{{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
| 249 |
.stage-deck{{grid-template-columns:1fr}}
|
| 250 |
.memo-card,.memo-card:nth-child(1),.memo-card:nth-child(2),.memo-card:nth-child(3),.memo-card:nth-child(4){{grid-column:span 12}}
|
|
|
|
| 251 |
.config-pattern{{grid-template-columns:1fr}}
|
| 252 |
.config-grid{{grid-template-columns:1fr}}
|
| 253 |
}}
|
| 254 |
@media(max-width:760px){{
|
| 255 |
.hero{{padding:28px 22px;grid-template-columns:1fr}}
|
| 256 |
.content{{padding:24px 14px 42px}}
|
| 257 |
+
.nav{{padding-right:10px}}
|
| 258 |
+
.nav-brand{{display:none}}
|
| 259 |
.nav-link{{padding:12px 12px;font-size:11px}}
|
| 260 |
.metric-grid{{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
| 261 |
}}
|
|
|
|
| 278 |
var _airiView = 'covered';
|
| 279 |
var _airiDomain = 0;
|
| 280 |
|
| 281 |
+
function _updateAiriToggleLabels() {
|
| 282 |
+
var covered = 0;
|
| 283 |
+
var gaps = 0;
|
| 284 |
+
var card = document.querySelector('.domain-card[data-domain="' + _airiDomain + '"]');
|
| 285 |
+
if (card) {
|
| 286 |
+
covered = parseInt(card.dataset.covered || '0', 10);
|
| 287 |
+
gaps = parseInt(card.dataset.gaps || '0', 10);
|
| 288 |
+
} else {
|
| 289 |
+
var allCard = document.querySelector('.domain-card[data-domain="0"]');
|
| 290 |
+
covered = parseInt(allCard?.dataset.covered || '0', 10);
|
| 291 |
+
gaps = parseInt(allCard?.dataset.gaps || '0', 10);
|
| 292 |
+
}
|
| 293 |
+
document.querySelectorAll('.toggle-btn[data-view]').forEach(function(b) {
|
| 294 |
+
if (b.dataset.view === 'covered') b.textContent = 'Covered (' + covered + ')';
|
| 295 |
+
if (b.dataset.view === 'gaps') b.textContent = 'Gaps (' + gaps + ')';
|
| 296 |
+
});
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
function _applyAiriFilter() {
|
| 300 |
document.querySelectorAll('.airi-covered,.airi-gaps').forEach(function(r) {
|
| 301 |
var viewOk = r.classList.contains('airi-' + _airiView);
|
| 302 |
var domainOk = (_airiDomain === 0 || r.dataset.domain == _airiDomain);
|
| 303 |
r.style.display = (viewOk && domainOk) ? '' : 'none';
|
| 304 |
});
|
| 305 |
+
_updateAiriToggleLabels();
|
| 306 |
}
|
| 307 |
|
| 308 |
function airiToggle(view) {
|
tests/test_scanner_regressions.py
CHANGED
|
@@ -4,6 +4,8 @@ from pathlib import Path
|
|
| 4 |
import json
|
| 5 |
from datetime import date
|
| 6 |
|
|
|
|
|
|
|
| 7 |
import stem_ai.detectors as detectors
|
| 8 |
from stem_ai.advisory_contract import (
|
| 9 |
_fallback_citations,
|
|
@@ -53,7 +55,7 @@ from stem_ai.reasoning_model import (
|
|
| 53 |
)
|
| 54 |
from stem_ai.regulatory_traceability import build_regulatory_basis
|
| 55 |
from stem_ai.redaction import redact_object, redaction_policy, sanitize_artifact_text, secret_scan
|
| 56 |
-
from stem_ai.render import _explain_status_label, render_explain, render_markdown, render_pdf_pages
|
| 57 |
from stem_ai.render_html import render_html
|
| 58 |
from stem_ai.scanner import _score_bias, _score_changelog, _score_provenance, _score_stage_2r, audit_repository
|
| 59 |
|
|
@@ -2458,6 +2460,50 @@ def test_cli_scan_accepts_named_policy_and_surfaces_it_in_output(tmp_path: Path,
|
|
| 2458 |
assert result["calibration_profile"]["profile_name"] == "strict_clinical_adjacency"
|
| 2459 |
|
| 2460 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2461 |
def test_cli_rejects_invalid_policy_name(tmp_path: Path) -> None:
|
| 2462 |
repo = tmp_path / "repo"
|
| 2463 |
_write(repo / "README.md", "Bio repository for molecular analysis.\n")
|
|
|
|
| 4 |
import json
|
| 5 |
from datetime import date
|
| 6 |
|
| 7 |
+
import pytest
|
| 8 |
+
|
| 9 |
import stem_ai.detectors as detectors
|
| 10 |
from stem_ai.advisory_contract import (
|
| 11 |
_fallback_citations,
|
|
|
|
| 55 |
)
|
| 56 |
from stem_ai.regulatory_traceability import build_regulatory_basis
|
| 57 |
from stem_ai.redaction import redact_object, redaction_policy, sanitize_artifact_text, secret_scan
|
| 58 |
+
from stem_ai.render import _explain_status_label, render_explain, render_markdown, render_pdf_pages, write_outputs
|
| 59 |
from stem_ai.render_html import render_html
|
| 60 |
from stem_ai.scanner import _score_bias, _score_changelog, _score_provenance, _score_stage_2r, audit_repository
|
| 61 |
|
|
|
|
| 2460 |
assert result["calibration_profile"]["profile_name"] == "strict_clinical_adjacency"
|
| 2461 |
|
| 2462 |
|
| 2463 |
+
def test_cli_scan_defaults_to_level_3() -> None:
|
| 2464 |
+
from stem_ai.cli import _build_parser
|
| 2465 |
+
|
| 2466 |
+
parser = _build_parser()
|
| 2467 |
+
|
| 2468 |
+
args = parser.parse_args(["scan", "D:/tmp/example"])
|
| 2469 |
+
|
| 2470 |
+
assert args.level == 3
|
| 2471 |
+
|
| 2472 |
+
|
| 2473 |
+
def test_detailed_pdf_packets_render_expected_page_counts(tmp_path: Path) -> None:
|
| 2474 |
+
pypdf = pytest.importorskip("pypdf")
|
| 2475 |
+
|
| 2476 |
+
_write(
|
| 2477 |
+
tmp_path / "README.md",
|
| 2478 |
+
"Bioinformatics repository for clinical trial biomarker analysis and medical imaging.\n"
|
| 2479 |
+
"HIPAA-compliant architecture when self-hosted.\n"
|
| 2480 |
+
"World's most powerful open-source bio AI.\n"
|
| 2481 |
+
"PubMed, ClinicalTrials.gov, and DailyMed workflow support.\n",
|
| 2482 |
+
)
|
| 2483 |
+
_write(tmp_path / "package.json", '{\"name\":\"demo-bio\"}\n')
|
| 2484 |
+
_write(tmp_path / "pnpm-lock.yaml", "lockfileVersion: '9.0'\n")
|
| 2485 |
+
_write(tmp_path / "Dockerfile", "FROM python:3.11-slim\n")
|
| 2486 |
+
_write(
|
| 2487 |
+
tmp_path / "src" / "pipeline.py",
|
| 2488 |
+
"def self_host_mode():\n"
|
| 2489 |
+
" if True:\n"
|
| 2490 |
+
" return {'user': 'dev@localhost'}\n",
|
| 2491 |
+
)
|
| 2492 |
+
|
| 2493 |
+
result = audit_repository(tmp_path)
|
| 2494 |
+
out_dir = tmp_path / "out"
|
| 2495 |
+
created_5 = write_outputs(result, out_dir / "l2", mode="detailed", pages=5, fmt="pdf", explain=False)
|
| 2496 |
+
pdf_5 = next(path for path in created_5 if path.suffix == ".pdf")
|
| 2497 |
+
reader_5 = pypdf.PdfReader(str(pdf_5))
|
| 2498 |
+
|
| 2499 |
+
created_7 = write_outputs(result, out_dir / "l3", mode="detailed", pages=7, fmt="pdf", explain=False)
|
| 2500 |
+
pdf_7 = next(path for path in created_7 if path.suffix == ".pdf")
|
| 2501 |
+
reader_7 = pypdf.PdfReader(str(pdf_7))
|
| 2502 |
+
|
| 2503 |
+
assert len(reader_5.pages) == 5
|
| 2504 |
+
assert len(reader_7.pages) == 7
|
| 2505 |
+
|
| 2506 |
+
|
| 2507 |
def test_cli_rejects_invalid_policy_name(tmp_path: Path) -> None:
|
| 2508 |
repo = tmp_path / "repo"
|
| 2509 |
_write(repo / "README.md", "Bio repository for molecular analysis.\n")
|