Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/dashboard_refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ on:
branches: [main]
paths:
- "registry.yaml"
- "decisions.yaml"
- "decisions/**"
- "pulse/**"
pull_request:

Expand Down
22 changes: 22 additions & 0 deletions CHECKIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,25 @@ policy. Pulse scheduling metadata never changes measurement qualification or
Heart readiness. Migration preserves original text and provenance; historical
headers inside those texts are not a live queue. Resolve old Mind paths via
`migration.yaml`; use the pinned source commit for unmigrated historical paths.

## Record a key decision

When the user says "record this decision" or explicitly flags a key campaign
decision, use `decisions/README.md` to produce a high-level campaign summary.
Read the relevant campaign/task history and cited project/Cortex evidence;
preserve hardware, dataset, model and cold/warm-start distinctions. Summarize
the evidence, alternatives, the human's choice and its implications, with
source links and limitations. Do not infer an accepted choice from timings,
a passing run, or an agent recommendation. If the user is still deciding,
prepare a draft and ask for the missing choice before adding a history entry.

Write one canonical Markdown record in the campaign-leading organ and link it
from `decisions.yaml` on each relevant board. Do not categorize by repository
or dataset and do not duplicate the prose. Explicit direction to record a
stated decision authorizes the capture; ask only about genuinely missing facts.
Keep scientific sources authoritative and cite them. Preserve old decisions;
a changed choice receives a new dated record with supersession links.
Publish the owning record before adding its cross-dashboard link, verify the
GitHub destination, regenerate and check both affected boards through the
normal development workflow. Recording a decision does not authorize its
implementation or compute. Never turn an illustrative title into a decision.
9 changes: 9 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,12 @@ Rectangular, MGE, KNN, MGE Mass, Sersic, then other families alphabetically.

Sersic retains one plain menu entry when only unclassified latent tools exist;
this does not assign a backend. Known Sersic variants replace that fallback.

## Decision History

`decisions.yaml` indexes human-authored campaign summaries; `decisions/README.md`
defines capture, ownership, record format and supersession. The flat list follows
Results in HTML and Markdown. Both boards can link to one canonical record.
The reader validates IDs, dates, GitHub destinations and owned record metadata;
it never makes a scientific decision. Remote record existence is checked when
publishing the cross-dashboard link, not by offline rendering.
2 changes: 1 addition & 1 deletion dashboard.html

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,9 @@ Project `autolens_profiling`, scope `setup-catalogue`, read from [PyAutoLabs/aut
- The v1 production feed remains published during migration; this is the companion v2 catalogue.
- Indexed-only evidence and static estimates are not successful measurements or measured peak VRAM.
</details>


<!-- decision-history:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945 -->
## Decision History

No decisions recorded yet.
2 changes: 2 additions & 0 deletions decisions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Human decisions only; see decisions/README.md for capture and record format.
decisions: []
68 changes: 68 additions & 0 deletions decisions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Decision records

Decision History is a flat, newest-first list below the dashboard's Results.
It is not divided by repository, dataset or likelihood. Entries open readable
GitHub Markdown pages in new tabs.

## Capture a decision

Say **"record this decision"**, or explicitly flag a key campaign decision,
in the ongoing campaign conversation. Follow CHECKIN.md's decision capture
procedure. A dedicated skill is not required: the check-in instructions are
the entry point for both natural-language forms.

Write one canonical `decisions/<id>.md` in the organ leading the campaign.
Use the template below; do not index the template or an undecided draft.
Add the same id, title, date and canonical URL to `decisions.yaml` in each
relevant dashboard. Cross-dashboard entries link to the same page; never copy
the decision prose. The date is the date the human made the decision, not the
board refresh date. Quote it in YAML. IDs are stable lowercase kebab-case.

The index has exactly one `decisions` list; each entry has `id`, `title`,
`date` (YYYY-MM-DD string), and `url` pointing to
`https://github.com/PyAutoLabs/<owning-organ>/blob/main/decisions/<id>.md`.
The owning organ is PyAutoPulse or PyAutoInsight. Its local record must exist
and its heading and Date line must match the index. Publish the canonical
record before adding a link from the other dashboard, then verify that link.
Evidence references within the record should pin commits or immutable results.

Keep old decisions visible. A changed choice gets a new dated record with a
"Supersedes" link; add a "Superseded by" link to the old page without rewriting
its original reasoning. Clarifying factual corrections are dated annotations.
An agent's proposed interpretation is not a human decision. Cortex/project
ledgers remain authoritative for scientific facts and human conclusions;
these pages summarize and cite them. Recording does not implement the choice,
submit compute, change defaults or certify scientific acceptance.

## Record template (not a published decision)

```markdown
# <Decision title>

Date: YYYY-MM-DD
Decision maker: <human>
Decision source: <human statement and durable source link, or dated conversation quotation>

## Campaign
<Goal, motivation and scope across repositories/datasets.>

## Evidence
<High-level findings with pinned links; hardware, precision, model, cold/warm
start and timing definitions where relevant. Include uncertainty and failures.>

## Options considered
<Alternatives and consequential tradeoffs.>

## Decision
<The human's actual choice, its scope and any exceptions.>

## Implications
<Effects on likelihoods, SLaM/inference defaults, compatibility and follow-up
work. Distinguish decided direction from implementation already shipped.>

## Revisit conditions
<Remaining uncertainty and what new evidence would change the choice.>

## History
<Supersedes/superseded-by links when applicable.>
```
6 changes: 5 additions & 1 deletion pulse/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from datetime import UTC, datetime
from pathlib import Path

from pulse import ORGAN_ROOT, campaigns, setup_browser
from pulse import ORGAN_ROOT, campaigns, decisions, setup_browser
from pulse import summary as summary_mod
from pulse.ingest import Snapshot

Expand Down Expand Up @@ -359,6 +359,7 @@ def render_markdown(views, now: str | None = None, campaign_data: dict | None =
"</details>",
"",
]
out += ["", decisions.markdown()]
return "\n".join(out).rstrip("\n") + "\n"
out = [
"# PyAutoPulse — profiling dashboard",
Expand Down Expand Up @@ -392,6 +393,7 @@ def render_markdown(views, now: str | None = None, campaign_data: dict | None =
out += ["", NOTIONS]
for s in views:
out += _md_detail(s, now)
out += ["", decisions.markdown()]
return "\n".join(out).rstrip("\n") + "\n"


Expand Down Expand Up @@ -572,6 +574,7 @@ def render_html(views, now: str | None = None, campaign_data: dict | None = None
navigation=[
{"href": "#campaigns", "label": "Active campaigns"},
{"href": "#evidence", "label": "Profiling Results"},
{"href": "#decision-history", "label": "Decision History"},
],
)
+ "<main>"
Expand All @@ -584,6 +587,7 @@ def render_html(views, now: str | None = None, campaign_data: dict | None = None
],
)
+ "".join(content)
+ decisions.render_html()
+ "</main>"
+ f"<script>{shared.JS}\n{JS}\n{browser_js}</script></body></html>\n"
)
Expand Down
14 changes: 13 additions & 1 deletion pulse/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import sys
from pathlib import Path

from pulse import ORGAN_ROOT, board, campaigns, ingest, registry
from pulse import ORGAN_ROOT, board, campaigns, decisions, ingest, registry
from pulse import summary as summary_mod


Expand Down Expand Up @@ -157,6 +157,12 @@ def _check_state(path: Path) -> list[str]:

def cmd_check(args) -> int:
problems = []
try:
decision_rows = decisions.load()
except ValueError as exc:
print(f"FAIL decisions: {exc}")
return 1

try:
campaign_data = campaigns.load()
except campaigns.CampaignError as exc:
Expand Down Expand Up @@ -210,6 +216,12 @@ def cmd_check(args) -> int:
problems.append("dashboard.md / dashboard.html missing")
print("FAIL dashboard: dashboard.md / dashboard.html missing — run `pyauto-pulse board`")
else:
if (
decisions.marker(decision_rows) not in md.read_text()
or decisions.marker(decision_rows) not in page.read_text()
):
problems.append("decision history stale — regenerate board")
print("FAIL decisions: dashboard does not match the decision index")
expected = campaigns.marker(campaign_data)
if expected not in md.read_text() or expected not in page.read_text():
problems.append("campaign dashboard stale — run pyauto-pulse board")
Expand Down
106 changes: 106 additions & 0 deletions pulse/decisions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Flat links to human-authored campaign decisions; never infer conclusions."""

import hashlib
import html
import json
import re
from datetime import date
from pathlib import Path
from urllib.parse import urlsplit

import yaml

from pulse import ORGAN_ROOT

REPO = "PyAutoPulse"


def load(root: Path = ORGAN_ROOT) -> list[dict]:
"""Validate the index and locally owned records; remote records stay links."""
try:
document = yaml.safe_load((root / "decisions.yaml").read_text())
except (OSError, yaml.YAMLError) as exc:
raise ValueError(f"Decision history: {exc}") from exc
if (
not isinstance(document, dict)
or set(document) != {"decisions"}
or not isinstance(document["decisions"], list)
):
raise ValueError("Decision history requires a decisions list")
seen = set()
rows = []
for row in document["decisions"]:
if not isinstance(row, dict) or set(row) != {"id", "title", "date", "url"}:
raise ValueError("Decision requires id, title, date and url")
if any(
not isinstance(v, str) or not v.strip() or any(ord(c) < 32 for c in v)
for v in row.values()
):
raise ValueError("Decision fields must be nonempty single-line strings; quote dates")
key = row["id"]
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", key) or key in seen:
raise ValueError(f"Invalid or duplicate decision id: {key}")
seen.add(key)
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", row["date"]):
raise ValueError("Decision date must be YYYY-MM-DD")
date.fromisoformat(row["date"])
url = urlsplit(row["url"])
match = re.fullmatch(
r"/PyAutoLabs/(PyAutoPulse|PyAutoInsight)/blob/main/decisions/([a-z0-9-]+)\.md",
url.path,
)
if (
url.scheme != "https"
or url.netloc != "github.com"
or url.query
or url.fragment
or not match
or match[2] != key
):
raise ValueError(
"Decision URL must link to its canonical GitHub decisions/<id>.md record"
)
if match[1] == REPO:
path = root / "decisions" / f"{key}.md"
if not path.is_file():
raise ValueError(f"Missing decision record: {path}")
content = path.read_text()
if (
not content.startswith(f"# {row['title']}\n")
or f"Date: {row['date']}\n" not in content
):
raise ValueError(f"Decision title/date differ from index: {key}")
rows.append(dict(row))
return sorted(rows, key=lambda row: (row["date"], row["id"]), reverse=True)


def marker(rows):
digest = hashlib.sha256(json.dumps(rows, sort_keys=True).encode()).hexdigest()
return f"<!-- decision-history:{digest} -->"


def render_html(rows=None):
rows = load() if rows is None else rows
links = "".join(
f'<a class="model-choice" href="{html.escape(row["url"], quote=True)}" target="_blank" rel="noopener" title="Opens in a new tab">'
f"{html.escape(row['title'])}</a>"
for row in rows
)
return (
marker(rows)
+ '<section id="decision-history"><h2>Decision History</h2>'
+ (links or "<p>No decisions recorded yet.</p>")
+ "</section>"
)


def markdown(rows=None):
rows = load() if rows is None else rows
# HTML anchors also work in GitHub Markdown and safely escape arbitrary titles.
links = [
f'- <a href="{html.escape(row["url"], quote=True)}">{html.escape(row["title"])}</a>'
for row in rows
]
return "\n".join(
[marker(rows), "## Decision History", "", *(links or ["No decisions recorded yet."]), ""]
)
13 changes: 13 additions & 0 deletions tests/browser_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import sys
from pathlib import Path
from unittest.mock import patch

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from pulse import board # noqa: E402
Expand Down Expand Up @@ -185,6 +186,18 @@ def write(destination):
numba_row["evidence"] = numba["evidence"]
readable.doc["records"].append(numba_row)
(destination / "readable.html").write_text(board.render_html([readable]))
with patch(
"pulse.decisions.load",
return_value=[
{
"id": "solver",
"title": "NNLS Solver Setup",
"date": "2026-10-08",
"url": "https://github.com/PyAutoLabs/PyAutoInsight/blob/main/decisions/solver.md",
}
],
):
(destination / "decision-history.html").write_text(board.render_html([readable]))

menu = copy.deepcopy(readable)
for model in ("mge_mass", "knn", "mge", "rectangular"):
Expand Down
11 changes: 11 additions & 0 deletions tests/browser_setup.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,17 @@ const { execFileSync } = require("node:child_process");
"implementation",
) === "numba",
);
await page.goto(base + "/decision-history.html");
const history = page.locator("#decision-history");
const historyDetails = history.locator("xpath=ancestor-or-self::details[1]");
assert.equal(await historyDetails.getAttribute("open"), null);
await historyDetails.locator("summary").first().click();
const decisionLink = history.getByRole("link", {name: "NNLS Solver Setup", exact: true});
assert.equal(await decisionLink.getAttribute("target"), "_blank");
assert.equal(await decisionLink.getAttribute("href"), "https://github.com/PyAutoLabs/PyAutoInsight/blob/main/decisions/solver.md");
assert(await decisionLink.isVisible());
await page.setViewportSize({width: 390, height: 844});
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
await page.goto(base + "/menu-order.html");
assert.deepEqual(
await page.locator('[data-family="imaging"] .model-choice').allTextContents(),
Expand Down
24 changes: 24 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,27 @@ def test_board_from_a_local_file_is_labelled_local(argv, tmp_path, capsys):
def test_unknown_instance_is_a_clean_error(argv, capsys):
assert cli.main(argv("fetch", "--instance", "nope")) == 1
assert "no instance 'nope'" in capsys.readouterr().err


def test_decision_only_change_requires_regeneration(argv, web, monkeypatch, capsys):
from pulse import decisions

web.publish(GH, SHA_A, PATH, fixture_doc("lens_summary_v1.json"))
monkeypatch.setattr(decisions, "load", lambda: [])
assert cli.main(argv("board")) == 0
monkeypatch.setattr(
decisions,
"load",
lambda: [
{
"id": "solver",
"title": "Solver choice",
"date": "2026-10-08",
"url": "https://github.com/PyAutoLabs/PyAutoInsight/blob/main/decisions/solver.md",
}
],
)
assert cli.main(argv("check", "--offline")) == 1
assert "FAIL decisions" in capsys.readouterr().out
assert cli.main(argv("board", "--offline")) == 0
assert cli.main(argv("check", "--offline")) == 0
Loading
Loading