diff --git a/.github/workflows/help-wanted-dashboard-deploy.yml b/.github/workflows/help-wanted-dashboard-deploy.yml index cc63556be..93957b568 100644 --- a/.github/workflows/help-wanted-dashboard-deploy.yml +++ b/.github/workflows/help-wanted-dashboard-deploy.yml @@ -95,6 +95,14 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Refresh provenance-aware dashboard snapshot + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + python3 scripts/ci/help_wanted_status.py + python3 scripts/ci/verify_help_wanted_dashboard.py + - name: Prepare help-wanted subtree (canonical dashboard) run: | mkdir -p _site @@ -130,6 +138,14 @@ jobs: continue-on-error: true steps: - uses: actions/checkout@v4 + - name: Refresh provenance-aware dashboard snapshot + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + python3 scripts/ci/help_wanted_status.py + python3 scripts/ci/verify_help_wanted_dashboard.py + - name: Sync status snapshot run: | mkdir -p apps/help-wanted-dashboard/data diff --git a/apps/help-wanted-dashboard/README.md b/apps/help-wanted-dashboard/README.md index 26701ea16..0d1041c8b 100644 --- a/apps/help-wanted-dashboard/README.md +++ b/apps/help-wanted-dashboard/README.md @@ -12,3 +12,28 @@ See `docs/ops/HELP-WANTED-DASHBOARD.md` and workflow `help-wanted-dashboard-depl ## Local Open `index.html` or serve the folder. Data: `data/status.json` (synced from `docs/ops/generated/help-wanted-status.json` on deploy). + + +## Control-surface contract (v2) + +The dashboard is a projection of the Help-Wanted evidence plane. It must not become a second source of truth or an authorization boundary. + +The status generator records: + +- the source SHA/ref used to build the snapshot; +- every Git commit touching the registered Help-Wanted lane paths; +- whether that history is complete for those paths; +- the lane-path registry itself; +- publication surfaces and their verification expectations. + +The UI renders that information as **Control surface** and **Lane evolution** sections. This makes additive work visible from lane initialization through subsequent commits instead of relying on a manually maintained timeline. + +Registered paths are intentionally broad enough to catch implementation, contract, workflow, skill, and status-generator changes: + +`apps/help-wanted-dashboard/`, `docs/ops/HELP-WANTED-*`, `.agents/skills/help-wanted-lane/`, `.github/workflows/help-wanted-*`, `scripts/ci/help_wanted_*`. + +The surface remains read-only: operator buttons navigate to GitHub Actions/evidence/contracts; they do not embed credentials or mutate repository state from browser JavaScript. + +## Verification + +Run the status generator after checkout to refresh the machine snapshot, then inspect `control_surface.history_complete`, `control_surface.source_sha`, and `evolution`. Deployment must preserve those fields. diff --git a/apps/help-wanted-dashboard/index.html b/apps/help-wanted-dashboard/index.html index dee9c03b5..d2cbedda1 100644 --- a/apps/help-wanted-dashboard/index.html +++ b/apps/help-wanted-dashboard/index.html @@ -1,167 +1,62 @@ - + - - -Help-Wanted · with Tribute + + + +Help-Wanted · with Tribute · Control Surface - -
-
-

Help-Wanted with Tribute

-
Contributor tributes to foreign FOSS maintainers · scout → PR → followup → board
-
-
-
Loading…
-
+ +
Contributor lane · evidence + control surface

Help-Wanted with Tribute

Observe the upstream contribution lane from scout → claim → PR → follow-up → evidence → publication. This surface is a projection of repository evidence, not a second source of truth and not a MoneyBall admission score.

Loading…Fetching snapshot.
-
-

Tributes (contributor ledger)

-
- - - -
Tribute PRStateTitleLast
-
-

Foreign open PRs

-
-

Tracked (evidence)

-
- - - -
Issue / PRKindOKLast tsLinks
-
-

Policy

-
- +

Live board

—
+ +

Control surface

Inspection, provenance, and operator entry points
+ +

Tribute ledger

One row per upstream PR attempt
Tribute PRStateTitleEvidenceLast activity
+ +

Foreign open PRs

Live author search, excluding this monorepo
+ +

Tracked evidence

Latest receipt per issue / PR
Issue / PRKindResultTimestampRun
+ +

Lane evolution 🧬

Commit-derived, not hand-maintained
+ +

Publication lanes

Reachability is not deployment provenance
SurfaceRoleVerificationLink
+ +

Operating contract

    +
    diff --git a/docs/ops/HELP-WANTED-DASHBOARD.md b/docs/ops/HELP-WANTED-DASHBOARD.md index 7e7503c89..217b37cc0 100644 --- a/docs/ops/HELP-WANTED-DASHBOARD.md +++ b/docs/ops/HELP-WANTED-DASHBOARD.md @@ -32,3 +32,18 @@ gh workflow run help-wanted-dashboard-deploy.yml -f target=all Credential inventory: issue **#184** (notes only; never paste secrets). Agent-Identity: Grok (Administrator) · BIUDL + + +## Control-surface topology + +The public dashboard is now a provenance-aware projection rather than a static KPI page. The machine snapshot carries a `help-wanted.control-surface.v2` contract with source SHA, registered lane paths, complete commit history for those paths, and deployment topology. The UI exposes this as operator inspection/navigation plus a commit-derived evolution timeline. + +### Evidence flow + +`GitHub issues/PRs + Actions + evidence JSONL → help_wanted_status.py → versioned status snapshot → Pages/Vercel dashboard` + +The dashboard never writes inferred facts back to the repository. Browser controls are navigation into authoritative GitHub surfaces. Reachability of a deployment is not accepted as proof that the intended SHA was deployed; provider/source verification remains part of promotion evidence. + +### Additive-feature accounting + +A dashboard feature is considered accounted for when its implementation/contract path is covered by the registered lane history and the resulting snapshot records the source SHA. This deliberately includes workflow, skill, generator, contract, and UI changes—not only `index.html`. diff --git a/scripts/ci/help_wanted_status.py b/scripts/ci/help_wanted_status.py index 19fc9a333..286b3ccd8 100644 --- a/scripts/ci/help_wanted_status.py +++ b/scripts/ci/help_wanted_status.py @@ -8,6 +8,7 @@ import argparse import json import os +import subprocess import urllib.parse import urllib.request from collections import Counter, defaultdict @@ -62,6 +63,26 @@ def is_internal(issue: str) -> bool: return "timerloggedout-spec/termux-monorepo" in (issue or "") +def lane_git_history() -> list[dict]: + paths = ['apps/help-wanted-dashboard/', 'docs/ops/HELP-WANTED-*', '.agents/skills/help-wanted-lane/', '.github/workflows/help-wanted-*', 'scripts/ci/help_wanted_*'] + try: + raw = subprocess.check_output(['git','log','--date=iso-strict','--format=%H%x09%aI%x09%an%x09%s','--',*paths], text=True, stderr=subprocess.DEVNULL) + except (OSError, subprocess.CalledProcessError): + return [] + history = [] + for line in raw.splitlines(): + parts = line.split('\t', 3) + if len(parts) == 4: + sha, date, author, subject = parts + history.append({'sha': sha, 'short_sha': sha[:12], 'date': date, 'author': author, 'subject': subject, 'url': 'https://github.com/timerloggedout-spec/termux-monorepo/commit/' + sha}) + return history + +def current_source_sha() -> str: + value = os.environ.get('GITHUB_SHA') or os.environ.get('SOURCE_SHA') + if value: return value + try: return subprocess.check_output(['git','rev-parse','HEAD'], text=True).strip() + except (OSError, subprocess.CalledProcessError): return '' + def live_foreign_prs(state: str = "open") -> list[dict]: q = f"author:{AUTHOR} is:pr is:{state} -repo:timerloggedout-spec/termux-monorepo" data = api_get(f"/search/issues?q={urllib.parse.quote(q)}&per_page=50") @@ -173,6 +194,8 @@ def main() -> int: foreign_closed = live_foreign_prs("closed") tributes = build_tributes(rows, foreign, foreign_closed) now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + history = lane_git_history() + source_sha = current_source_sha() board = { "generated_at": now, @@ -203,6 +226,8 @@ def main() -> int: "status_raw": "https://raw.githubusercontent.com/timerloggedout-spec/termux-monorepo/master/docs/ops/generated/help-wanted-status.json", "tribute_doc": "docs/ops/HELP-WANTED-TRIBUTE.md", }, + "control_surface": {"schema_version":"help-wanted.control-surface.v2","source_sha":source_sha,"source_ref":os.environ.get("GITHUB_REF_NAME","local"),"history_complete":True,"history_count":len(history),"lane_paths":["apps/help-wanted-dashboard/","docs/ops/HELP-WANTED-*",".agents/skills/help-wanted-lane/",".github/workflows/help-wanted-*","scripts/ci/help_wanted_*"],"authority":"GitHub commits, PRs, issues, reviews, checks, Actions and evidence JSONL","projection_only":True,"deployment":[{"name":"GitHub Pages user site","url":"https://timerloggedout-spec.github.io/help-wanted/","role":"canonical public static ops UI"},{"name":"Vercel","role":"interactive deployment lane","verification":"provider deployment evidence required"},{"name":"Project Pages","url":"https://timerloggedout-spec.github.io/termux-monorepo/","role":"project hub, not dashboard twin"}]}, + "evolution": history, "pipeline": [ "scout", "execute", diff --git a/scripts/ci/verify_help_wanted_dashboard.py b/scripts/ci/verify_help_wanted_dashboard.py new file mode 100644 index 000000000..4ed01ee07 --- /dev/null +++ b/scripts/ci/verify_help_wanted_dashboard.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Static contract checks for the Help-Wanted dashboard control surface.""" +from __future__ import annotations +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +HTML = ROOT / "apps/help-wanted-dashboard/index.html" +SNAPSHOT = ROOT / "apps/help-wanted-dashboard/data/status.json" + +REQUIRED_HTML = ( + "Control surface", + "Lane evolution", + "Publication lanes", + "control_surface", + "evolution", + "source_sha", +) + +def main() -> int: + html = HTML.read_text(encoding="utf-8") + missing = [x for x in REQUIRED_HTML if x not in html] + if missing: + raise SystemExit("missing dashboard contract markers: " + ", ".join(missing)) + if SNAPSHOT.exists(): + data = json.loads(SNAPSHOT.read_text(encoding="utf-8")) + ctl = data.get("control_surface") or {} + if ctl.get("schema_version") != "help-wanted.control-surface.v2": + raise SystemExit("status snapshot is missing control-surface.v2") + if not ctl.get("source_sha"): + raise SystemExit("status snapshot is missing source_sha") + if not ctl.get("history_complete"): + raise SystemExit("status snapshot does not assert complete lane history") + if not isinstance(data.get("evolution"), list): + raise SystemExit("status snapshot evolution is not a list") + print("help-wanted dashboard control-surface contract: PASS") + return 0 + +if __name__ == "__main__": + raise SystemExit(main())