Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
762ef52
fix: close generic-completeness audit findings (C1-C4, U1-U5)
maruwork Jun 19, 2026
2547d30
fix: close round-2/3 security, sync, and dashboard-fidelity audit fin…
maruwork Jun 19, 2026
1d8ed8e
fix: dashboard fidelity, Windows concurrency, summary scaling, lint p…
maruwork Jun 19, 2026
ea9a814
feat: schema-version tolerance, cross-project aggregate, coverage gate
maruwork Jun 19, 2026
453987f
style: make ruff check + ruff format clean for CI (pre-commit gate)
maruwork Jun 19, 2026
a0c678f
fix(ci): make mypy green by collapsing dead relative-import branch
maruwork Jun 19, 2026
f650046
ci: re-trigger PR checks
maruwork Jun 19, 2026
69c66fc
feat(html): add a back-to-top button to the dashboard
maruwork Jun 19, 2026
827902a
chore: declutter repo root (move community files to .github/, drop st…
maruwork Jun 19, 2026
1db33fc
fix: init creates nested overlay parent; summary hides stale structur…
maruwork Jun 19, 2026
c0144f2
chore: convert to a clean pure-tool repo (drop self-dogfooding appara…
maruwork Jun 19, 2026
77b8ce5
docs: add a minimal, self-contained worked example under examples/
maruwork Jun 19, 2026
8300d4f
fix(ci): repair invalid ci.yml YAML that startup-failed every run sin…
maruwork Jun 19, 2026
db142d5
test: de-flake concurrent id-minting test on loaded CI (12 -> 5 workers)
maruwork Jun 19, 2026
6f2ba5d
ci: trigger hosted run on fixed tree
maruwork Jun 19, 2026
3f31731
chore: block secrets/.env/keys from ever being committed
maruwork Jun 19, 2026
3375bb1
Merge main (squash of #11) into branch, keeping the branch superset
maruwork Jun 19, 2026
7b5a2a0
test: give concurrent id-mint test an effectively-unbounded retry budget
maruwork Jun 19, 2026
559ed34
Merge main into branch before re-merge (keep branch superset)
maruwork Jun 19, 2026
4a1c7ba
docs: fill in the Unreleased changelog (was incorrectly 'Nothing yet')
maruwork Jun 19, 2026
bb0a860
Merge main into branch (keep branch superset)
maruwork Jun 19, 2026
3b006b6
fix(html): "Latest update" shows the most-recent artifact with its date
maruwork Jun 20, 2026
0a7fda1
Merge main (keep branch superset)
maruwork Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ All notable changes to this project are documented in this file.

- Distribution: `adop.json` now lists `adop_html.py` and the HTML template, so a manifest-synced runtime can start and render (previously crashed with `ModuleNotFoundError`)
- Dashboard now surfaces the trial-packet allow/deny envelope, block reason, intake "why now", watch interest, reject reason, and deprecated/migrating/archived/hold reasoning (were blank or showed the stale promote judgment)
- Dashboard "Latest update" column now shows the genuinely most-recent artifact and its date (previously picked an earlier-stage note by id number — e.g. the intake over a later trial packet — and showed only a cryptic artifact id)
- Write-trial guard: `task-scoped` and `phase-scoped` trials now also require an isolated write sandbox
- Windows: artifact-write lock maps `PermissionError` like `FileExistsError` so concurrent id minting retries instead of crashing; stale locks are reclaimed
- Performance: `summary` / state resolution read judgments from a single in-memory load instead of re-reading the artifact root per trial
Expand Down
37 changes: 35 additions & 2 deletions shared/python/adop_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,11 +541,44 @@ def _landing_target(scene_items: list[dict[str, Any]]) -> str:
return "-"


# Lifecycle stage rank for "latest update": later stage = more recent action.
# Used as a same-day tiebreak so the headline never picks an earlier-stage note
# just because its id number happens to be larger (e.g. ci-002 over tr-001).
_EVIDENCE_STAGE: dict[str, int] = {
"watch-note": 0,
"candidate-intake-note": 1,
"coupling-note": 1,
"blocked-note": 2,
"comparison-note": 3,
"trial-packet": 4,
"trial-result": 5,
"judgment-report": 6,
"hold-note": 6,
"reject-note": 6,
"promotion-note": 6,
"deprecation-note": 7,
"migration-note": 8,
"archive-note": 9,
}


def _last_evidence(scene_items: list[dict[str, Any]]) -> str:
if not scene_items:
return "-"
item = sorted(scene_items, key=_id_sort_key)[-1]
return f"{_artifact_label(str(item.get('artifact_type', '-')))} {item.get('artifact_id', '-')}"

def recency_key(item: dict[str, Any]) -> tuple[str, int, int]:
created = str(item.get("created_at", ""))
stage = _EVIDENCE_STAGE.get(str(item.get("artifact_type", "")), 0)
raw = str(item.get("artifact_id", ""))
prefix = raw.split("-", 1)[0] if "-" in raw else ""
number = parse_numeric_id(raw, prefix) if prefix else None
return (created, stage, number or 0)

item = max(scene_items, key=recency_key)
label = _artifact_label(str(item.get("artifact_type", "-")))
created = str(item.get("created_at", "")).strip()
# Date first so the "Latest update" column reads as a date and sorts chronologically.
return f"{created} · {label}" if created else label


def _artifact_label(artifact_type: str) -> str:
Expand Down
60 changes: 60 additions & 0 deletions tests/test_html_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,3 +801,63 @@ def test_dashboard_has_back_to_top(run, root):
assert '<button class="to-top"' in html
assert 'aria-label="Back to top"' in html
assert "window.scrollTo({ top: 0" in html


def test_latest_update_is_most_advanced_artifact_with_date(run, root):
from adop_html import build_dashboard_payload

assert (
run(
"quick-intake",
"--artifact-root",
root,
"--candidate",
"mypy",
"--source",
"doc",
"--use-case",
"types",
"--why-now",
"x",
)
== 0
)
assert (
run(
"quick-compare",
"--artifact-root",
root,
"--use-case",
"types",
"--candidate",
"mypy",
"--candidate",
"pyright",
"--selected",
"mypy",
)
== 0
)
assert (
run(
"quick-trial",
"--artifact-root",
root,
"--use-case",
"types",
"--mode",
"review-assist",
"--executor",
"ci",
"--decision-owner",
"lead",
"--landing-target",
"ci/types",
)
== 0
)
lane = next(ln for ln in build_dashboard_payload(Path(root))["lanes"] if ln["scene"] == "types")
ev = lane["last_evidence"]
assert "Trial plan" in ev # the most-advanced artifact, not the intake
assert "Intake" not in ev
assert ev[:4].isdigit() and ev[4] == "-" # leads with a date (YYYY-...)