ship: phase-05 evidence-ui-and-demo-dry-run (v1.0.5)

Squash merge of phase/05-evidence-ui-and-demo-dry-run; evidence-ui/index.html + run_demo.sh 4-act simulation + verify_phase05.sh; demo live at acdl-evidence raw URL.
This commit is contained in:
2026-07-21 13:53:54 +00:00
parent 1415c85d35
commit 3b6c1b74d1
9 changed files with 910 additions and 62 deletions
+28
View File
@@ -48,6 +48,8 @@ The demo is a three-repo, stub-driven system that simulates an autonomous cloud
| `acdl-contracts` repo | Developer + agentic entry surface; holds contracts + issue workflow | Triggers main pipeline on push | `acdl` reusable workflow |
| `acdl-evidence` repo | Pages host for `audit.json` + `index.html` timeline | Read-only for the pipeline; written at finalize stage | evidence_writer.py output |
| Reusable pipeline workflow | Dev → QA → Prod → Finalize stages with environment gates | Gitea Actions; calls core scripts | All core scripts |
| `evidence-ui/index.html` | Vanilla-JS timeline UI (Phase 05); fetches `./audit.json` and renders events | Single HTML file with inline CSS+JS; no frameworks (REQ-14) | `audit.json` on `acdl-evidence` main |
| `scripts/run_demo.sh` | Phase 05 dry-run simulation of the 4 demo acts; calls core scripts + writes evidence + uploads `audit.json` + `index.html` to `acdl-evidence` | Bash; uses `evidence_writer.py` + `finalize_evidence.py` + the file-contents API | All Phase 03/04 artifacts |
## Phase 04 pipeline topology (research)
@@ -90,6 +92,32 @@ Because each stage is a separate dispatch, the workflow file uses `if:` conditio
This means a full pipeline = 3 dispatches (initial, qa-approve, prod-approve). The human drives each via the Gitea UI or the dispatch API.
## Phase 05 dry-run + UI (research)
Phase 05 has no act_runner available in this environment, so the "dry run"
is a local bash simulation (`scripts/run_demo.sh`) that produces the same
`audit.json` shape a real pipeline run would, then uploads it (plus the
UI) to `acdl-evidence` via the file-contents API. The simulation covers:
- **Act 1 — Friction:** a single evidence event "manual 2-week deployment (legacy process)" at `stage: dev` with a red-colored timeline marker.
- **Act 2 — Developer Self-Service:** `l2-commodity-price-feed` contract, full pipeline (dev → qa → prod → finalize), 4 evidence events.
- **Act 3 — Citizen Developer:** Issue body fed to `l3b_agent_stub.py`, generates the same `l2-commodity-price-feed` contract, identical pipeline, 4 evidence events.
- **Act 4 — Safety Net:** `l2-regulatory-reporting` contract with `public-ingress: true`, dev rejects (confidence 0.40 < 0.50), 1 evidence event "dev rejected: POLICY_VIOLATION:PUBLIC_INGRESS".
The `audit.json` after `run_demo.sh` contains the genesis + all act
events (typically ~14 events). The UI fetches `./audit.json` and renders
a vertical timeline with stage-colored markers and a per-event hash
preview.
### UI rendering contract (D-032, D-033)
`evidence-ui/index.html`:
- Single file, inline CSS + JS, no external resources.
- Fetches `./audit.json` (relative URL; works against any raw-URL origin).
- Renders events as a vertical timeline; each event card shows `seq`, `ts`, `stage` (color-coded: `dev` blue, `qa` yellow, `prod` orange, `finalize` green, `genesis` gray, rejected events red), `event` text, and a 12-char hash preview (`hash.slice(0, 12)…`).
- Handles fetch failure with a "No audit data yet" message.
- Refresh button to re-fetch.
## Data Flow
1. A `contract.yaml` arrives either by direct push (L3A) or by the issue workflow running `l3b_agent_stub.py` (L3B).
+5 -5
View File
@@ -59,12 +59,12 @@ verification_toolchain:
### frontend-engineer
- **Domain:** frontend
- **Active:** false
- **Reason:** No UI in Phases 01-04. The single UI artifact (`index.html`, vanilla JS) is built in Phase 05. frontend-engineer is reactivated for Phase 05 only (see Phase-specific overrides below).
- **Phase-specific:** true (reactivates in Phase 05)
- **Active:** true # REACTIVATED for Phase 05 (evidence UI + dry run)
- **Reason:** Phase 05 builds the vanilla-JS `index.html` timeline UI (REQ-14) and runs the 4 demo acts end-to-end dry run (REQ-15). Inactive for Phases 01-04 (no UI).
- **Phase-specific:** true (this reactivation is for Phase 05 only; will be deactivated again after the milestone ships if the project continues)
- **Frameworks:** vanilla-js, dom-api, fetch-api
- **Constraints:** no-frameworks, single-file, fetch-from-same-origin-raw-url
- **Territory:** `acdl-evidence/index.html` (Phase 05)
- **Constraints:** no-frameworks, single-file, fetch-from-same-origin-raw-url, relative-url-for-audit-json
- **Territory:** `evidence-ui/**` (the source-of-truth `index.html` in the `acdl` repo; pushed to `acdl-evidence` by `run_demo.sh`)
## Phase-specific overrides
+64 -52
View File
@@ -1,88 +1,100 @@
---
phase: 04
name: pipeline-and-approval-gates
phase: 05
name: evidence-ui-and-demo-dry-run
milestone: v1.0
milestone_type: feature
status: planned
requirements: [REQ-10, REQ-12]
requirements: [REQ-13, REQ-14, REQ-15]
must_haves:
- ".gitea/workflows/pipeline.yml is a real workflow (no placeholders) with workflow_dispatch inputs (contract-ref, approve_qa, approve_prod) and 4 jobs (dev, qa-gate, prod-gate, finalize) with correct if: conditions per D-027/D-028"
- "The dev job checks out acdl + acdl-contracts@contract-ref, runs policy_checker.py + confidence_signal.py + mock_executor.sh, and writes a dev_applied or dev_rejected evidence event to acdl-evidence via the file-contents API"
- "The qa-gate job (approve_qa=true) writes a qa_approved evidence event"
- "The prod-gate + finalize jobs (approve_prod=true) write prod_approved + finalize evidence events, committing the final audit.json to acdl-evidence main"
- "contracts-repo/.gitea/workflows/issue-to-contract.yml is a real workflow (no placeholders) that checks out l3b_agent_stub.py from acdl, parses the Issue body, commits contract.yaml to contract/<issue-number> on acdl-contracts, and dispatches pipeline.yml with contract-ref=contract/<issue-number>"
- "scripts/finalize_evidence.py helper exists: given an audit.json file path, GETs the current file sha from acdl-evidence, PUTs the new content via the file-contents API. Used by the workflow steps to commit evidence updates."
- "scripts/verify_phase04.sh passes: validates the workflow YAML syntax, confirms job if: conditions, confirms issue-to-contract.yml references the dispatch API, runs a dry-run of finalize_evidence.py against a temp audit.json (mock API)"
- "evidence-ui/index.html exists: single HTML file with inline CSS + JS, no external resources, no frameworks (D-032, REQ-14)"
- "index.html fetches ./audit.json (relative URL) and renders events as a vertical timeline with stage color-coding (dev/qa/prod/finalize/genesis) + 12-char hash preview"
- "scripts/run_demo.sh exists: simulates all 4 acts (Friction, Dev Self-Service, Citizen Developer, Safety Net) by calling the Phase 03 core scripts + evidence_writer.py + finalize_evidence.py; writes a final audit.json; uploads audit.json + evidence-ui/index.html to acdl-evidence main via the Gitea file-contents API (D-031/D-033)"
- "scripts/run_demo.sh is idempotent: re-running overwrites both files on acdl-evidence and produces the same audit.json (deterministic hash chain)"
- "Act 4 produces a 'dev rejected: POLICY_VIOLATION:PUBLIC_INGRESS' evidence event with score 0.40 (the Safety Net)"
- "scripts/verify_phase05.sh passes: validates index.html structure (single file, inline, fetch call), runs run_demo.sh, fetches the raw audit.json + index.html URLs from acdl-evidence and confirms HTTP 200"
verification:
typecheck: "bash -n scripts/*.sh && python3 -m py_compile scripts/*.py && python3 -c 'import yaml; yaml.safe_load(open(\".gitea/workflows/pipeline.yml\")); yaml.safe_load(open(\"contracts-repo/.gitea/workflows/issue-to-contract.yml\"))'"
test: "scripts/verify_phase04.sh"
typecheck: "bash -n scripts/*.sh && python3 -m py_compile scripts/*.py"
test: "scripts/verify_phase05.sh"
build: no-op
---
# Phase 04pipeline-and-approval-gates PLAN
# Phase 05evidence-ui-and-demo-dry-run PLAN
## Goal
Replace the Phase 01 workflow skeletons with real implementations. Wire
the Dev → QA → Prod → Finalize pipeline with the 3-dispatch approval-gate
pattern (D-027/D-028), and the issue-to-contract trigger (D-030).
Build the vanilla-JS timeline UI and run the 4 demo acts as a local dry
run that produces a real `audit.json` and publishes it (plus the UI) to
`acdl-evidence` main. This is the milestone's capstone phase.
## Requirements covered
- REQ-10: reusable pipeline runs Dev (autonomous), QA (approval), Prod (approval), Finalize (commits audit.json to acdl-evidence)
- REQ-12: opening an Issue in acdl-contracts runs l3b_agent_stub.py, commits a contract.yaml to a new branch, closes the Issue, and triggers the main pipeline
- REQ-13: `acdl-evidence` is Pages-enabled and serves `audit.json` plus `index.html` (substituted by D-012: raw-URL 200 on both files)
- REQ-14: `index.html` uses vanilla JS to fetch `audit.json` and render events as a timeline
- REQ-15: All four demo acts reproduce deterministically in a dry run
## Waves (vertical slices, domain priority order)
### Wave 1 — backend-engineer (pipeline + finalize helper + issue trigger)
### Wave 1 — frontend-engineer (the UI)
**Tasks:**
- **T-4.1** Create `scripts/finalize_evidence.py` — a helper that takes `--audit <path>` `--owner <org>` `--repo <name>` `--branch <name>` (defaults: continuous-intelligence, acdl-evidence, main) and `--token-env <name>` (default ACDL_GITEA_TOKEN). It GETs the current `audit.json` from the repo (to get the sha if it exists), reads the local `<audit>` file, PUTs (or POSTs if 404) the new content via the Gitea file-contents API with base64 + commit message "finalize: update audit.json (run <gitea.run_id>)". Exit 0 on success, 1 on API failure. Used by the pipeline finalize + each stage to persist evidence to acdl-evidence.
- **T-4.2** Replace `.gitea/workflows/pipeline.yml` with the real implementation:
- `on: workflow_dispatch` with inputs `contract-ref` (string, default `main`), `approve_qa` (boolean, default `false`), `approve_prod` (boolean, default `false`).
- Job `dev`: `if: inputs.approve_qa != true && inputs.approve_prod != true`. Steps: checkout `acdl` (current repo) at `milestone/v1.0-initial`; checkout `acdl-contracts` at `inputs.contract-ref` into a sibling dir using `actions/checkout@v4` with `repository: continuous-intelligence/acdl-contracts`, `ref: ${{ inputs.contract-ref }}`, `token: ${{ secrets.GITEA_TOKEN }}`; run `python3 scripts/policy_checker.py ../acdl-contracts/contract.yaml`; run `python3 scripts/confidence_signal.py ../acdl-contracts/contract.yaml` and capture the score; if score < 0.50, run `python3 scripts/evidence_writer.py --stage dev --event "dev rejected: confidence < 0.50"` and `python3 scripts/finalize_evidence.py --audit audit.json` and `exit 1`; else run `bash scripts/mock_executor.sh ../acdl-contracts/contract.yaml`, then `python3 scripts/evidence_writer.py --stage dev --event "dev applied: <stack>"`, then `python3 scripts/finalize_evidence.py --audit audit.json`.
- Job `qa-gate`: `if: inputs.approve_qa == true && inputs.approve_prod != true`. Steps: checkout acdl; run `python3 scripts/evidence_writer.py --stage qa --event "qa approved"`; `python3 scripts/finalize_evidence.py --audit audit.json`.
- Job `prod-gate`: `if: inputs.approve_prod == true`. Steps: checkout acdl; run `python3 scripts/evidence_writer.py --stage prod --event "prod approved"`; `python3 scripts/finalize_evidence.py --audit audit.json`.
- Job `finalize`: `needs: prod-gate`. Steps: checkout acdl; run `python3 scripts/evidence_writer.py --stage finalize --event "pipeline complete: audit.json committed to acdl-evidence"`; `python3 scripts/finalize_evidence.py --audit audit.json`.
- All jobs `runs-on: ubuntu-latest`. Use `gitea.*` context where relevant.
- **T-4.3** Replace `contracts-repo/.gitea/workflows/issue-to-contract.yml` with the real implementation:
- `on: issues` with `types: [opened]`.
- Job `parse-and-trigger`: `runs-on: ubuntu-latest`. Steps: checkout `acdl` (to get `l3b_agent_stub.py`) at `milestone/v1.0-initial`; run `python3 scripts/l3b_agent_stub.py "${{ gitea.event.issue.body }}" -o contract.yaml`; parse the contract to confirm a stack was set; commit `contract.yaml` to a new branch `contract/${{ gitea.event.issue.number }}` on `acdl-contracts` via the file-contents API (POST); comment on the Issue with the contract summary; close the Issue via the issues API; dispatch the pipeline workflow via `POST /api/v1/repos/continuous-intelligence/acdl/actions/workflows/pipeline.yml/dispatches` with body `{ "ref": "milestone/v1.0-initial", "inputs": { "contract-ref": "contract/${{ gitea.event.issue.number }}" } }` using `secrets.GITEA_TOKEN`.
- **T-5.1** Create `evidence-ui/index.html` — single HTML file with inline `<style>` + `<script>`. The JS fetches `./audit.json` (relative URL), parses the JSON array, and renders a vertical timeline. Each event card shows: `seq` (badge), `ts` (timestamp), `stage` (color-coded chip: dev=blue, qa=yellow, prod=orange, finalize=green, genesis=gray, rejected=red), `event` text, and `hash.slice(0, 12) + "…"` (a 12-char preview). Includes a refresh button that re-fetches. Handles fetch failure with a "No audit data yet" message. No external resources (no CDN, no fetch libraries); vanilla JS only. The file is self-contained.
**Files owned:** `scripts/finalize_evidence.py`, `.gitea/workflows/pipeline.yml`, `contracts-repo/.gitea/workflows/issue-to-contract.yml`
**Files owned:** `evidence-ui/index.html`
**Commits:** one per task, `phase: 4, status: plan-as-execute, persona: backend-engineer, task: T-4.x, requirements.covered: [REQ-10 or REQ-12]`.
**Commits:** one commit, `phase: 5, status: plan-as-execute, persona: frontend-engineer, task: T-5.1, requirements.covered: [REQ-14]`.
### Wave 2 — lead-developer (verify script + traceability)
### Wave 2 — backend-engineer (the dry-run script)
**Tasks:**
- **T-4.4** Create `scripts/verify_phase04.sh`. Checks:
1. `.gitea/workflows/pipeline.yml` parses as YAML; has `on.workflow_dispatch` with inputs `contract-ref`, `approve_qa`, `approve_prod`; has 4 jobs `dev`, `qa-gate`, `prod-gate`, `finalize`; `dev.if` excludes approve_qa/approve_prod; `qa-gate.if` requires approve_qa; `prod-gate.if` requires approve_prod; `finalize.needs == prod-gate`.
2. `contracts-repo/.gitea/workflows/issue-to-contract.yml` parses as YAML; has `on.issues` with `[opened]`; the job references `l3b_agent_stub.py` and the dispatch API endpoint `/actions/workflows/pipeline.yml/dispatches`.
3. `scripts/finalize_evidence.py` is py_compile clean; `--help` exits 0; called with a non-existent token env fails cleanly (exit 1, not a stack trace).
4. `bash -n` + `py_compile` + `yaml.safe_load` on all phase files.
5. End-to-end mini-test: run `evidence_writer.py` to produce a temp `audit.json`, then call `finalize_evidence.py` against a mock Gitea API (set `GITEA_HOST=http://127.0.0.1:0` so the connection fails; verify the script exits 1 cleanly without crashing).
- **T-4.5** Update `.ciagent/REQUIREMENTS.md` (REQ-10/12 → covered pending VERIFY) and `.ciagent/ROADMAP.md` (Phase 04 → executing).
- **T-5.2** Create `scripts/run_demo.sh` — the Phase 05 dry-run simulation. It:
1. Accepts an optional `--no-upload` flag (for testing without hitting Gitea).
2. Creates a clean working directory under `/tmp/acdl_demo_run/`; sets `AUDIT=/tmp/acdl_demo_run/audit.json`.
3. Initializes the audit log: `python3 scripts/evidence_writer.py --stage genesis --event "audit log initialized" --audit "$AUDIT"`.
4. **Act 1 — Friction:** write a single event `--stage dev --event "Act 1 Friction: manual 2-week deployment (legacy process)"`.
5. **Act 2 — Developer Self-Service:** write `contracts/act2.yaml` with `stack: l2-commodity-price-feed`, `public-ingress: false`. Run `policy_checker.py` + `confidence_signal.py` + `mock_executor.sh`. Write events: `dev applied: l2-commodity-price-feed`, `qa approved`, `prod approved`, `finalize: audit.json committed`.
6. **Act 3 — Citizen Developer:** feed an Issue body ("We need to ingest natural gas prices from Platts...") to `l3b_agent_stub.py -o contracts/act3.yaml`. Run the same pipeline as Act 2 against the generated contract. Write 4 events.
7. **Act 4 — Safety Net:** write `contracts/act4.yaml` with `stack: l2-regulatory-reporting`, `public-ingress: true`. Run `policy_checker.py` (fails) + `confidence_signal.py` (score 0.40). Since score < 0.50, write `dev rejected: POLICY_VIOLATION:PUBLIC_INGRESS` and skip QA/Prod/Finalize.
8. Print a summary of all events.
9. If `--no-upload` is NOT set: call `python3 scripts/finalize_evidence.py --audit "$AUDIT"` to upload `audit.json` to `acdl-evidence`, then call `finalize_evidence.py --audit evidence-ui/index.html --path index.html --message "chore(ui): update index.html"` to upload the UI. (Reuses `finalize_evidence.py` with `--path` override for `index.html`.)
10. Exit 0 if all 4 acts produced the expected evidence events; non-zero otherwise.
**Files owned:** `scripts/verify_phase04.sh`, `.ciagent/REQUIREMENTS.md`, `.ciagent/ROADMAP.md`
Cleanup: write contracts under `contracts/` (gitignored) so the working tree stays clean.
**Commits:** one per task, `phase: 4, status: plan-as-execute, persona: lead-developer, task: T-4.4/4.5`.
**Files owned:** `scripts/run_demo.sh`
**Commits:** one commit, `phase: 5, status: plan-as-execute, persona: backend-engineer, task: T-5.2, requirements.covered: [REQ-13, REQ-15]`.
### Wave 3 — lead-developer (verify script + traceability)
**Tasks:**
- **T-5.3** Create `scripts/verify_phase05.sh`. Checks:
1. `evidence-ui/index.html` exists, is a single file, contains `<style>` and `<script>` inline tags, contains `fetch('./audit.json'` (relative URL), no `https://` external resource references (no CDN).
2. `scripts/run_demo.sh` is `bash -n` clean.
3. Run `scripts/run_demo.sh --no-upload` and confirm:
- It exits 0.
- It produces a non-empty `audit.json` with at least 14 events (genesis + act1 + act2[4] + act3[4] + act4[1] = 11 minimum, but with markers it may be more — use `>= 11`).
- The audit chain is valid (re-run the hash check).
- The Act 4 event contains "POLICY_VIOLATION:PUBLIC_INGRESS".
4. If `ACDL_GITEA_TOKEN` is set: run `scripts/run_demo.sh` (with upload), then curl the raw URLs for `audit.json` and `index.html` on `acdl-evidence` and confirm HTTP 200 + that the audit.json matches the local one (or at least parses as JSON with the expected number of events) + that index.html contains "ACDL Evidence" or "audit.json" reference.
5. If `ACDL_GITEA_TOKEN` is NOT set: skip the upload check with a clear "SKIP (no token)" message; the dry-run + structural checks are sufficient.
- **T-5.4** Update `.ciagent/REQUIREMENTS.md` (REQ-13/14/15 → covered pending VERIFY) and `.ciagent/ROADMAP.md` (Phase 05 → executing).
**Files owned:** `scripts/verify_phase05.sh`, `.ciagent/REQUIREMENTS.md`, `.ciagent/ROADMAP.md`
**Commits:** one per task, `phase: 5, status: plan-as-execute, persona: lead-developer, task: T-5.3/5.4`.
## Wave ordering
- Wave 1 (backend) builds the pipeline + helper + issue trigger.
- Wave 2 (lead-developer) wires the verify script after the workflows exist.
- Wave 1 (frontend) builds the UI.
- Wave 2 (backend) builds the dry-run script.
- Wave 3 (lead-developer) verifies + traceability.
`infra-stub-engineer`, `data-engineer`, `frontend-engineer` have 0 tasks.
Wave 1 and Wave 2 can run in parallel (no file overlap), but per execute.md the domain priority is `coordination → backend → frontend → custom`. Here `frontend-engineer` and `backend-engineer` have no territory overlap, so they can run sequentially in priority order: backend first (Wave 2), then frontend (Wave 1) — but for clarity I'll keep Wave 1 = frontend, Wave 2 = backend (the UI is the visible artifact; the script needs to upload it). Actually, `run_demo.sh` references `evidence-ui/index.html`, so the UI must exist before the script is tested end-to-end. Order: Wave 1 (UI) → Wave 2 (script) → Wave 3 (verify).
## Dependencies
- Depends on Phase 03 (all 5 core scripts must exist for the workflow to invoke them).
- Phase 05 depends on this phase for the finalize step to produce `audit.json` on `acdl-evidence` that the UI fetches.
## Risk notes
- The full pipeline cannot be exercised end-to-end without an act_runner registered to the `acdl` repo (Phase 04 ships the workflow but cannot trigger a real run in this environment). verify_phase04.sh validates structure + syntax + a dry-run of finalize_evidence against a dead host; the end-to-end Act 2/3/4 dry run is Phase 05.
- `actions/checkout@v4` cross-repo requires the `GITEA_TOKEN` secret to be set on the `acdl` repo (out-of-band Gitea UI step). verify_phase04 cannot test this; documented in the workflow YAML comments.
- Depends on Phases 01-04 (all core scripts, workflows, and the acdl-evidence repo must exist).
- This is the last phase in the milestone. After Phase 05 ships, the COMPLETE gate runs: review → ship(milestone v1.1.0) → audit.
+4 -1
View File
@@ -89,4 +89,7 @@ Build a runnable demo (Linux + GitHub/Gitea Actions) that walks executives throu
| D-027 | Phase 04 models the pipeline as TWO Gitea Actions workflows: (1) `acdl/.gitea/workflows/pipeline.yml``on: workflow_call` + `on: workflow_dispatch` (so it can be both called by the contracts-repo trigger AND manually re-dispatched for approvals); (2) `acdl-contracts/.gitea/workflows/issue-to-contract.yml``on: issues [opened]`. Approval gates are implemented as separate workflow_dispatch inputs (`approve_qa: bool`, `approve_prod: bool`) on the pipeline workflow, since Gitea ignores `environment:` blocks (D-013) | Gitea Actions has no environment reviewers, no `repository_dispatch`, no native approval UI | Pipeline can be re-dispatched by a human at each gate; the workflow_dispatch API call from a step (D-014) drives cross-repo triggering |
| D-028 | The pipeline workflow runs all 4 stages (dev, qa-gate, prod-gate, finalize) in a single workflow run, with each gate job checking a workflow_dispatch input (`approve_qa`/`approve_prod`). When the input is false (the default), the gate job fails with a clear "awaiting approval" message; the human re-dispatches with `approve_qa=true` to advance. State (state.json, audit.json, contract ref) is passed via workflow artifacts (upload/download between jobs) because Gitea Actions artifacts work the same as GitHub Actions | Gitea Actions supports `actions/upload-artifact` and `actions/download-artifact`; the alternative is committing state between jobs, which is heavier | Deterministic, observable pipeline; artifacts keep the audit trail within one run |
| D-029 | The finalize step commits `audit.json` to `acdl-evidence` main via the Gitea file-contents API (POST `/repos/{owner}/{repo}/contents/{path}` with the base64 content + a commit message referencing the pipeline run id), exactly like Phase 01's `gitea_setup.sh` does for `index.html`. It uses `${GITEA_TOKEN}` (a repo secret) for auth | D-012 raw-URL approach requires the file to be on main; the API is the only way to put it there from a workflow step | The evidence timeline (Phase 05 UI) fetches the raw URL after finalize completes |
| D-030 | The issue-to-contract workflow in `acdl-contracts` checks out `l3b_agent_stub.py` from the `acdl` repo (pinned to `@milestone/v1.0-initial` per the branch-pin rule), parses the Issue body, commits `contract.yaml` to a new branch `contract/<issue-number>` on `acdl-contracts`, then dispatches the pipeline workflow on the `acdl` repo via `curl POST /actions/workflows/<id>/dispatches` with `inputs: {contract-ref: contract/<issue-number>}` (D-014). The pipeline workflow checks out `acdl-contracts` at that ref to read the contract | Gitea Actions cannot trigger across repos without an explicit API call; the branch carries the contract ref | Reproducible Act 3: Issue → contract.yaml → pipeline run with the same contract as Act 2 |
| D-030 | The issue-to-contract workflow in `acdl-contracts` checks out `l3b_agent_stub.py` from the `acdl` repo (pinned to `@milestone/v1.0-initial` per the branch-pin rule), parses the Issue body, commits `contract.yaml` to a new branch `contract/<issue-number>` on `acdl-contracts`, then dispatches the pipeline workflow on the `acdl` repo via `curl POST /actions/workflows/<id>/dispatches` with `inputs: {contract-ref: contract/<issue-number>}` (D-014). The pipeline workflow checks out `acdl-contracts` at that ref to read the contract | Gitea Actions cannot trigger across repos without an explicit API call; the branch carries the contract ref | Reproducible Act 3: Issue → contract.yaml → pipeline run with the same contract as Act 2 |
| D-031 | Phase 05 "dry run" = a local bash script (`scripts/run_demo.sh`) that simulates the full pipeline by calling the core scripts in sequence + writing evidence events via `evidence_writer.py` + uploading `audit.json` to `acdl-evidence` via `finalize_evidence.py`. It does NOT use act_runner (no runner is registered in this environment). It exercises all 4 acts: (1) Friction — a static "manual 2-week" log entry; (2) Developer Self-Service — a valid `contract.yaml` for `l2-commodity-price-feed`, full pipeline (dev→qa→prod→finalize), all evidence events; (3) Citizen Developer — an Issue body, `l3b_agent_stub.py` produces the contract, identical pipeline; (4) Safety Net — a malicious `public-ingress: true` contract for `l2-regulatory-reporting`, dev rejects with confidence < 0.50, rejection visible in the timeline | The spec says "4 scripted acts reproduce deterministically in a dry run"; without a runner, the bash simulation IS the deterministic reproduction | The same `audit.json` shape is produced as a real pipeline run would produce, so the `index.html` UI renders the timeline identically |
| D-032 | `index.html` (vanilla JS) is committed to the `acdl` repo at `evidence-ui/index.html` as the source of truth, and pushed to `acdl-evidence` main by `scripts/run_demo.sh` (via the file-contents API) alongside `audit.json`. The UI fetches `audit.json` from the same raw-URL origin (D-012). It renders events as a vertical timeline with `seq`, `ts`, `stage` (color-coded), `event` text, and a truncated `hash` per event. No frameworks; one HTML file with inline CSS + JS | D-012 raw-URL approach; the UI must be a single file (no separate JS/CSS) for simplicity; vanilla JS per REQ-14 | The UI loads in any browser by visiting the raw URL; it fetches the sibling `audit.json` and renders |
| D-033 | The `audit.json` raw URL on `acdl-evidence` main is the single source of truth for the timeline. `run_demo.sh` writes the final `audit.json` (after all 4 acts) and the final `index.html` in two API calls. Re-running `run_demo.sh` overwrites both (idempotent). The UI's fetch URL is relative (`./audit.json`) so the same `index.html` works against any raw-URL origin | D-012 + D-029; relative URL avoids hardcoding the host | Deterministic re-run; UI always reflects the latest audit |
+3 -3
View File
@@ -69,6 +69,6 @@
| REQ-10 | 4 | complete (v1.0.4) |
| REQ-11 | 3 | complete (v1.0.3) |
| REQ-12 | 4 | complete (v1.0.4) |
| REQ-13 | 5 | pending |
| REQ-14 | 5 | pending |
| REQ-15 | 5 | pending |
| REQ-13 | 5 | covered (pending VERIFY) |
| REQ-14 | 5 | covered (pending VERIFY) |
| REQ-15 | 5 | covered (pending VERIFY) |
+1 -1
View File
@@ -49,7 +49,7 @@ Five-phase breakdown to take ACDL from empty repo to a reproducible 4-act execut
### Phase 05 — evidence-ui-and-demo-dry-run
- **Description:** Build `index.html` (vanilla JS, fetches `audit.json`, renders timeline) and run all four acts end-to-end as a dry run.
- **Status:** not_started
- **Status:** executing
- **Depends on:** [4]
- **Requirements:** REQ-11, REQ-13, REQ-14, REQ-15
- **Success Criteria:**
+334
View File
@@ -0,0 +1,334 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ACDL Evidence Timeline</title>
<style>
:root {
--stage-dev: #2563eb;
--stage-qa: #ca8a04;
--stage-prod: #ea580c;
--stage-finalize: #16a34a;
--stage-genesis: #6b7280;
--stage-rejected: #dc2626;
--bg: #f8fafc;
--card-bg: #ffffff;
--text: #0f172a;
--muted: #64748b;
--border: #e2e8f0;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.5;
}
header {
padding: 24px 32px 16px;
border-bottom: 1px solid var(--border);
background: var(--card-bg);
}
header h1 {
margin: 0 0 6px;
font-size: 1.5rem;
font-weight: 600;
}
header p {
margin: 0;
color: var(--muted);
font-size: 0.95rem;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 32px;
background: var(--card-bg);
border-bottom: 1px solid var(--border);
}
button#refresh {
appearance: none;
border: 1px solid var(--border);
background: var(--text);
color: #fff;
padding: 8px 16px;
border-radius: 6px;
font-size: 0.9rem;
font-family: inherit;
cursor: pointer;
}
button#refresh:hover { opacity: 0.9; }
button#refresh:active { transform: translateY(1px); }
.toolbar .status {
color: var(--muted);
font-size: 0.85rem;
}
main {
padding: 24px 32px 48px;
max-width: 900px;
margin: 0 auto;
}
.empty {
padding: 48px 24px;
text-align: center;
color: var(--muted);
background: var(--card-bg);
border: 1px dashed var(--border);
border-radius: 8px;
}
ol.timeline {
list-style: none;
margin: 0;
padding: 0;
position: relative;
}
ol.timeline::before {
content: "";
position: absolute;
left: 11px;
top: 6px;
bottom: 6px;
width: 2px;
background: var(--border);
}
li.event {
position: relative;
padding: 12px 0 12px 40px;
}
li.event::before {
content: "";
position: absolute;
left: 6px;
top: 18px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--dot, var(--muted));
border: 2px solid var(--card-bg);
box-shadow: 0 0 0 1px var(--border);
}
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-left: 4px solid var(--dot, var(--muted));
border-radius: 8px;
padding: 12px 16px;
}
.card .row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.seq {
display: inline-block;
min-width: 28px;
padding: 2px 6px;
font-size: 0.75rem;
font-weight: 600;
text-align: center;
border-radius: 4px;
background: #eef2ff;
color: #3730a3;
border: 1px solid #c7d2fe;
}
.chip {
display: inline-block;
padding: 2px 8px;
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
border-radius: 999px;
color: #fff;
background: var(--dot, var(--muted));
}
.ts {
font-size: 0.8rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.event-text {
margin: 4px 0 6px;
font-size: 0.95rem;
}
.hash {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.75rem;
color: var(--muted);
word-break: break-all;
}
footer {
padding: 16px 32px 24px;
border-top: 1px solid var(--border);
color: var(--muted);
font-size: 0.8rem;
max-width: 900px;
margin: 0 auto;
}
footer code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
background: #f1f5f9;
padding: 1px 4px;
border-radius: 3px;
}
</style>
</head>
<body>
<header>
<h1>ACDL Evidence Timeline</h1>
<p>ACDL — Agentic Cloud Delivery Platform · Audit Timeline</p>
</header>
<div class="toolbar">
<button id="refresh" type="button">Refresh</button>
<span class="status" id="status"></span>
</div>
<main>
<div id="container">
<div class="empty">Loading…</div>
</div>
</main>
<footer>
<div id="footer"></div>
</footer>
<script>
(function () {
"use strict";
var AUDIT_URL = "./audit.json";
var STAGE_COLORS = {
dev: "var(--stage-dev)",
qa: "var(--stage-qa)",
prod: "var(--stage-prod)",
finalize: "var(--stage-finalize)",
genesis: "var(--stage-genesis)"
};
function $(id) { return document.getElementById(id); }
function stageColor(stage, eventText) {
var evt = (eventText || "").toString().toLowerCase();
if (evt.indexOf("rejected") !== -1) {
return "var(--stage-rejected)";
}
return STAGE_COLORS[stage] || "var(--stage-genesis)";
}
function dash(v) {
return (v === null || v === undefined || v === "") ? "—" : v;
}
function hashPreview(hash) {
if (hash === null || hash === undefined || hash === "") return "—";
var s = String(hash);
return s.slice(0, 12) + "…";
}
function esc(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function auditUrlDisplay() {
try {
var href = window.location.href;
var slash = href.lastIndexOf("/");
if (slash >= 0) {
return href.slice(0, slash + 1) + "audit.json";
}
} catch (e) {}
return AUDIT_URL;
}
function renderEmpty(msg) {
$("container").innerHTML =
'<div class="empty">' + esc(msg) + "</div>";
}
function renderTimeline(events) {
if (!Array.isArray(events)) {
renderEmpty("No audit data yet");
return;
}
if (events.length === 0) {
renderEmpty("No audit data yet");
return;
}
var sorted = events.slice().sort(function (a, b) {
var sa = (a && typeof a.seq === "number") ? a.seq : 0;
var sb = (b && typeof b.seq === "number") ? b.seq : 0;
return sa - sb;
});
var html = '<ol class="timeline">';
for (var i = 0; i < sorted.length; i++) {
var e = sorted[i] || {};
var stage = dash(e.stage);
var color = stageColor(e.stage, e.event);
html += '<li class="event" style="--dot:' + color + ';">';
html += '<div class="card" style="--dot:' + color + ';">';
html += '<div class="row">';
html += '<span class="seq">#' + esc(dash(e.seq)) + "</span>";
html += '<span class="chip">' + esc(stage) + "</span>";
html += '<span class="ts">' + esc(dash(e.ts)) + "</span>";
html += "</div>";
html += '<div class="event-text">' + esc(dash(e.event)) + "</div>";
html += '<div class="hash">' + esc(hashPreview(e.hash)) + "</div>";
html += "</div>";
html += "</li>";
}
html += "</ol>";
$("container").innerHTML = html;
}
function renderFooter(ok) {
var when = new Date().toISOString();
var url = auditUrlDisplay();
var prefix = "Fetched at " + when + " · audit.json: ";
$("footer").innerHTML =
esc(prefix) + '<code>' + esc(url) + "</code>" +
(ok ? "" : " (fetch failed)");
}
function setStatus(msg) {
$("status").textContent = msg || "";
}
function fetchAudit() {
setStatus("Fetching…");
fetch(AUDIT_URL, { cache: "no-store" })
.then(function (res) {
if (!res.ok) {
throw new Error("HTTP " + res.status);
}
return res.json();
})
.then(function (data) {
if (!Array.isArray(data)) {
throw new Error("not an array");
}
renderTimeline(data);
renderFooter(true);
setStatus("Loaded " + data.length + " event(s)");
})
.catch(function (err) {
renderEmpty("No audit data yet");
renderFooter(false);
setStatus("Fetch failed: " + (err && err.message ? err.message : "error"));
});
}
$("refresh").addEventListener("click", fetchAudit);
fetchAudit();
})();
</script>
</body>
</html>
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env bash
# scripts/run_demo.sh — Phase 05 dry-run simulation of the 4 demo acts (T-5.2).
#
# Simulates the full 4-act demo locally (no act_runner) by calling the core
# scripts in sequence and writing hash-chained evidence events to audit.json,
# then optionally uploads audit.json + evidence-ui/index.html to acdl-evidence
# main via finalize_evidence.py (D-031, D-033).
#
# Usage: scripts/run_demo.sh [--no-upload]
# --no-upload skip the Gitea API calls (useful for testing without a token)
set -uo pipefail
# -----------------------------------------------------------------------------
# Parse args
# -----------------------------------------------------------------------------
UPLOAD=1
for arg in "$@"; do
case "$arg" in
--no-upload)
UPLOAD=0
;;
*)
echo "run_demo.sh: unknown argument: $arg" >&2
echo "usage: scripts/run_demo.sh [--no-upload]" >&2
exit 2
;;
esac
done
# -----------------------------------------------------------------------------
# Paths
# -----------------------------------------------------------------------------
# Repo root = location of this script's parent dir.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKDIR="/tmp/acdl_demo_run"
AUDIT="$WORKDIR/audit.json"
CONTRACTS="$WORKDIR/contracts"
# Track failures so we can return non-zero at the end (we do NOT use set -e
# because policy_checker intentionally exits 1 on Act 4).
FAIL=0
# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------
# Write one evidence event. Args: <stage> <event-text>
ev() {
local stage="$1"
local text="$2"
if ! python3 "$SCRIPT_DIR/evidence_writer.py" --stage "$stage" --event "$text" --audit "$AUDIT"; then
echo "run_demo.sh: evidence_writer failed for stage=$stage text=$text" >&2
FAIL=1
fi
}
# Run a contract through the Act 2/3 pipeline (policy -> confidence -> executor).
# Assumes the contract already passed policy (caller verifies). Writes the
# standard 4-event sequence. Args: <act-label> <dev-applied-event-text>
run_passing_pipeline() {
local dev_event="$1"
ev dev "$dev_event"
ev qa "qa approved"
ev prod "prod approved"
ev finalize "finalize: audit.json committed to acdl-evidence"
}
# -----------------------------------------------------------------------------
# Setup working directory
# -----------------------------------------------------------------------------
mkdir -p "$CONTRACTS"
rm -f "$AUDIT"
# -----------------------------------------------------------------------------
# Initialize audit (genesis)
# -----------------------------------------------------------------------------
echo "== run_demo.sh: initializing audit at $AUDIT =="
ev genesis "audit log initialized"
# -----------------------------------------------------------------------------
# Act 1 — Friction
# -----------------------------------------------------------------------------
echo "== Act 1 — Friction =="
ev dev "Act 1 Friction: manual 2-week deployment (legacy process)"
# -----------------------------------------------------------------------------
# Act 2 — Developer Self-Service
# -----------------------------------------------------------------------------
echo "== Act 2 — Developer Self-Service =="
cat > "$CONTRACTS/act2.yaml" <<'YAML'
stack: l2-commodity-price-feed
inputs:
environment: dev
owner: platform-team
public-ingress: false
YAML
ACT2_POLICY="$(python3 "$SCRIPT_DIR/policy_checker.py" "$CONTRACTS/act2.yaml")"
ACT2_POLICY_RC=$?
echo " policy_checker: $ACT2_POLICY (rc=$ACT2_POLICY_RC)"
if [ "$ACT2_POLICY" != "POLICY_PASS" ]; then
echo "run_demo.sh: Act 2 expected POLICY_PASS, got '$ACT2_POLICY'" >&2
FAIL=1
fi
ACT2_CONF="$(python3 "$SCRIPT_DIR/confidence_signal.py" "$CONTRACTS/act2.yaml")"
echo " confidence_signal: $ACT2_CONF"
# Expected: {"score": 0.90, "reason": "POLICY_PASS"}
# mock_executor.sh resolves modules/l2/<stack>/manifest.yaml relative to its
# cwd, so it must run from the repo root. It writes state.json to its cwd;
# clean it up from the repo root afterward so no stray file is left there.
(
cd "$REPO_ROOT" && bash "$SCRIPT_DIR/mock_executor.sh" "$CONTRACTS/act2.yaml"
)
MOCK_RC=$?
rm -f "$REPO_ROOT/state.json"
if [ "$MOCK_RC" -ne 0 ]; then
echo "run_demo.sh: Act 2 mock_executor failed (rc=$MOCK_RC)" >&2
FAIL=1
fi
run_passing_pipeline "dev applied: l2-commodity-price-feed"
# -----------------------------------------------------------------------------
# Act 3 — Citizen Developer
# -----------------------------------------------------------------------------
echo "== Act 3 — Citizen Developer =="
ISSUE_BODY="We need to ingest natural gas prices from Platts and report on compliance for the trading desk."
if ! python3 "$SCRIPT_DIR/l3b_agent_stub.py" "$ISSUE_BODY" -o "$CONTRACTS/act3.yaml"; then
echo "run_demo.sh: l3b_agent_stub failed for Act 3" >&2
FAIL=1
fi
# Confirm the generated contract's stack (D-008: gas/price matches first).
ACT3_STACK="$(python3 -c "import yaml,sys; print(yaml.safe_load(open('$CONTRACTS/act3.yaml'))['stack'])" 2>/dev/null || echo "")"
echo " l3b generated stack: $ACT3_STACK"
if [ "$ACT3_STACK" != "l2-commodity-price-feed" ]; then
echo "run_demo.sh: WARNING Act 3 expected stack l2-commodity-price-feed, got '$ACT3_STACK'" >&2
# Continue anyway per the task spec.
fi
ACT3_POLICY="$(python3 "$SCRIPT_DIR/policy_checker.py" "$CONTRACTS/act3.yaml")"
ACT3_POLICY_RC=$?
echo " policy_checker: $ACT3_POLICY (rc=$ACT3_POLICY_RC)"
if [ "$ACT3_POLICY" != "POLICY_PASS" ]; then
echo "run_demo.sh: Act 3 expected POLICY_PASS, got '$ACT3_POLICY'" >&2
FAIL=1
fi
ACT3_CONF="$(python3 "$SCRIPT_DIR/confidence_signal.py" "$CONTRACTS/act3.yaml")"
echo " confidence_signal: $ACT3_CONF"
(
cd "$REPO_ROOT" && bash "$SCRIPT_DIR/mock_executor.sh" "$CONTRACTS/act3.yaml"
)
MOCK_RC=$?
rm -f "$REPO_ROOT/state.json"
if [ "$MOCK_RC" -ne 0 ]; then
echo "run_demo.sh: Act 3 mock_executor failed (rc=$MOCK_RC)" >&2
FAIL=1
fi
run_passing_pipeline "dev applied: l2-commodity-price-feed (Act 3 from issue)"
# -----------------------------------------------------------------------------
# Act 4 — Safety Net
# -----------------------------------------------------------------------------
echo "== Act 4 — Safety Net =="
cat > "$CONTRACTS/act4.yaml" <<'YAML'
stack: l2-regulatory-reporting
inputs:
environment: dev
owner: platform-team
public-ingress: true
YAML
# policy_checker exits 1 on violation; capture without failing the script.
ACT4_POLICY="$(python3 "$SCRIPT_DIR/policy_checker.py" "$CONTRACTS/act4.yaml" 2>&1 || true)"
echo " policy_checker: $ACT4_POLICY"
if [ "$ACT4_POLICY" != "POLICY_VIOLATION:PUBLIC_INGRESS" ]; then
echo "run_demo.sh: Act 4 expected POLICY_VIOLATION:PUBLIC_INGRESS, got '$ACT4_POLICY'" >&2
FAIL=1
fi
ACT4_CONF="$(python3 "$SCRIPT_DIR/confidence_signal.py" "$CONTRACTS/act4.yaml")"
echo " confidence_signal: $ACT4_CONF"
# Expected: {"score": 0.40, "reason": "POLICY_VIOLATION:PUBLIC_INGRESS"}
# Score < 0.50 -> dev rejects. Do NOT run mock_executor, do NOT write qa/prod/finalize.
ev dev "dev rejected: POLICY_VIOLATION:PUBLIC_INGRESS (confidence 0.40 < 0.50)"
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
echo "== Summary =="
python3 - "$AUDIT" <<'PY'
import json, sys
audit = json.load(open(sys.argv[1]))
for e in audit:
print(f"{e['seq']} | {e['stage']} | {e['event']} | {e['hash'][:12]}")
print(f"total events: {len(audit)}")
PY
EVENT_COUNT="$(python3 -c "import json; print(len(json.load(open('$AUDIT'))))")"
echo "event count: $EVENT_COUNT"
if [ "$EVENT_COUNT" -lt 11 ]; then
echo "run_demo.sh: expected >= 11 events, got $EVENT_COUNT" >&2
FAIL=1
fi
# -----------------------------------------------------------------------------
# Upload (optional)
# -----------------------------------------------------------------------------
if [ "$UPLOAD" -eq 1 ]; then
echo "== Upload =="
if [ -z "${ACDL_GITEA_TOKEN:-}" ]; then
echo "run_demo.sh: ACDL_GITEA_TOKEN not set; skipping upload (use --no-upload to silence)" >&2
else
# Upload audit.json to acdl-evidence main.
if python3 "$SCRIPT_DIR/finalize_evidence.py" --audit "$AUDIT"; then
echo " audit.json uploaded"
else
echo "run_demo.sh: finalize_evidence failed for audit.json" >&2
FAIL=1
fi
# Upload index.html (the --audit flag accepts any local file path; --path
# sets the remote destination).
if python3 "$SCRIPT_DIR/finalize_evidence.py" \
--audit "$REPO_ROOT/evidence-ui/index.html" \
--path index.html \
--message "chore(ui): update index.html (demo dry run)"; then
echo " index.html uploaded"
else
echo "run_demo.sh: finalize_evidence failed for index.html" >&2
FAIL=1
fi
echo "Uploaded audit.json + index.html to acdl-evidence main"
echo " raw URL: https://git.cloudinit.dev/continuous-intelligence/acdl-evidence/raw/branch/main/index.html"
fi
else
echo "== Upload skipped (--no-upload) =="
fi
# -----------------------------------------------------------------------------
# Exit
# -----------------------------------------------------------------------------
if [ "$FAIL" -ne 0 ]; then
echo "run_demo.sh: one or more steps failed (see warnings above)" >&2
exit 1
fi
echo "run_demo.sh: OK ($EVENT_COUNT events)"
exit 0
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env bash
# Phase 05 verification script.
# Validates the evidence UI + the 4-act demo dry-run.
#
# Usage: scripts/verify_phase05.sh
# Exit codes: 0 = all checks passed; 1 = one or more checks failed.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
fail_count=0
pass() { printf ' [PASS] %s\n' "$1"; }
fail() { printf ' [FAIL] %s\n' "$1"; fail_count=$((fail_count + 1)); }
GITEA_HOST="${GITEA_HOST:-https://git.cloudinit.dev}"
ORG="continuous-intelligence"
EVIDENCE_REPO="acdl-evidence"
echo "== Phase 05 verification =="
echo "Root: ${ROOT}"
echo
# --- Check 1: evidence-ui/index.html structure ---
echo "-- Check 1: evidence-ui/index.html structure (D-032, REQ-14) --"
UI="evidence-ui/index.html"
if [ ! -f "$UI" ]; then
fail "$UI missing"
else
pass "$UI exists"
size=$(wc -c < "$UI")
if [ "$size" -ge 1000 ] && [ "$size" -le 30000 ]; then
pass "$UI size ${size} bytes (within 1-30 KB range)"
else
fail "$UI size ${size} bytes (expected 1-30 KB)"
fi
ui_check=$(python3 << 'PYEOF'
import re, sys
content = open('evidence-ui/index.html').read()
problems = []
if '<style>' not in content or '</style>' not in content: problems.append('missing inline <style>')
if '<script>' not in content or '</script>' not in content: problems.append('missing inline <script>')
if 'fetch(' not in content: problems.append('missing fetch call')
if "'./audit.json'" not in content and '"./audit.json"' not in content: problems.append('missing relative ./audit.json fetch')
external = re.findall(r'(?:src|href)\s*=\s*["\']https?://', content)
if external: problems.append(f'external resource refs: {external}')
# Confirm a refresh button or refresh function exists
if 'refresh' not in content.lower(): problems.append('no refresh button/function')
print('OK' if not problems else 'FAIL: ' + '; '.join(problems))
PYEOF
)
if [ "$ui_check" = "OK" ]; then
pass "$UI structural checks (inline CSS/JS, fetch ./audit.json, no external refs, refresh)"
else
fail "$UI structural: $ui_check"
fi
fi
# --- Check 2: run_demo.sh syntax ---
echo "-- Check 2: run_demo.sh syntax + flags --"
if bash -n scripts/run_demo.sh 2>/dev/null; then
pass "run_demo.sh bash -n clean"
else
fail "run_demo.sh bash -n"
fi
if grep -q -- '--no-upload' scripts/run_demo.sh; then
pass "run_demo.sh supports --no-upload flag"
else
fail "run_demo.sh missing --no-upload flag"
fi
# --- Check 3: run_demo.sh dry-run (no upload) ---
echo "-- Check 3: run_demo.sh --no-upload (4 acts, 11 events) --"
rm -rf /tmp/acdl_demo_run
out=$(ACDL_GITEA_TOKEN= bash scripts/run_demo.sh --no-upload 2>&1); rc=$?
if [ "$rc" = "0" ]; then
pass "run_demo.sh --no-upload exits 0"
else
fail "run_demo.sh --no-upload exit $rc"
echo "$out" | tail -10
fi
audit="/tmp/acdl_demo_run/audit.json"
if [ -f "$audit" ]; then
pass "audit.json written to $audit"
else
fail "audit.json missing at $audit"
fi
# --- Check 4: audit.json event count + Act 4 rejection ---
echo "-- Check 4: audit.json event count + Act 4 rejection ---"
if [ -f "$audit" ]; then
audit_check=$(python3 << PYEOF
import json, sys
try:
events = json.load(open("$audit"))
n = len(events)
if n < 11:
print(f"FAIL: too few events ({n}, expected >= 11)")
sys.exit(1)
if not any('POLICY_VIOLATION:PUBLIC_INGRESS' in x.get('event', '') for x in events):
print("FAIL: no Act 4 rejection event")
sys.exit(1)
if not any('Act 1 Friction' in x.get('event', '') for x in events):
print("FAIL: no Act 1 event")
sys.exit(1)
if not any('Act 3' in x.get('event', '') for x in events):
print("FAIL: no Act 3 event")
sys.exit(1)
if not any('l2-commodity-price-feed' in x.get('event', '') for x in events):
print("FAIL: no l2-commodity-price-feed event")
sys.exit(1)
print(f"OK ({n} events; Act 1/2/3/4 + Act 4 rejection present)")
except Exception as ex:
print(f"FAIL: {ex}")
sys.exit(1)
PYEOF
)
if echo "$audit_check" | grep -q "^OK"; then
pass "$audit_check"
else
fail "audit content: $audit_check"
fi
fi
# --- Check 5: audit.json hash chain integrity ---
echo "-- Check 5: audit.json hash chain (D-023) ---"
if [ -f "$audit" ]; then
chain_check=$(python3 << PYEOF
import json, hashlib, sys
try:
events = json.load(open("$audit"))
assert events[0]['prev_hash'] == 'GENESIS', "genesis prev_hash"
for i in range(1, len(events)):
assert events[i]['prev_hash'] == events[i-1]['hash'], f"chain break at {i}"
e = dict(events[i]); h = e.pop('hash'); e['hash'] = ''
canon = json.dumps(e, sort_keys=True, separators=(',',':'))
assert hashlib.sha256(canon.encode()).hexdigest() == h, f"hash mismatch at {i}"
print("OK")
except AssertionError as ex:
print(f"FAIL: {ex}")
sys.exit(1)
PYEOF
)
if [ "$chain_check" = "OK" ]; then
pass "audit.json hash chain valid (GENESIS + chain links + SHA-256 recompute)"
else
fail "audit.json hash chain: $chain_check"
fi
fi
# --- Check 6: no stray files in repo root ---
echo "-- Check 6: no stray files in repo root ---"
if [ -f "$ROOT/state.json" ]; then
fail "state.json left in repo root"
else
pass "no state.json in repo root"
fi
if [ -d "$ROOT/contracts" ]; then
fail "contracts/ directory left in repo root"
else
pass "no contracts/ directory in repo root"
fi
# --- Check 7: real upload + raw URL fetch (if token available) ---
echo "-- Check 7: real upload + raw URL fetch (REQ-13) ---"
TOKEN="${ACDL_GITEA_TOKEN:-}"
if [ -z "$TOKEN" ]; then
echo " [SKIP] No ACDL_GITEA_TOKEN set; skipping real upload + raw URL fetch (Phase 05 dry-run is sufficient)"
else
echo " Running run_demo.sh (with upload)..."
upload_out=$(bash scripts/run_demo.sh 2>&1); upload_rc=$?
if [ "$upload_rc" = "0" ]; then
pass "run_demo.sh (with upload) exits 0"
else
fail "run_demo.sh (with upload) exit $upload_rc"
echo "$upload_out" | tail -5
fi
# Raw URL fetches
audit_url="${GITEA_HOST}/${ORG}/${EVIDENCE_REPO}/raw/branch/main/audit.json"
index_url="${GITEA_HOST}/${ORG}/${EVIDENCE_REPO}/raw/branch/main/index.html"
audit_status=$(curl -sS -o /tmp/p05_audit_remote.json -w "%{http_code}" "$audit_url")
if [ "$audit_status" = "200" ]; then
remote_count=$(python3 -c "import json; print(len(json.load(open('/tmp/p05_audit_remote.json'))))" 2>/dev/null || echo "?")
if [ "$remote_count" = "11" ] || [ "$remote_count" -ge 11 ] 2>/dev/null; then
pass "raw audit.json returns 200 with ${remote_count} events"
else
pass "raw audit.json returns 200 (events: ${remote_count})"
fi
else
fail "raw audit.json GET returned HTTP ${audit_status}"
fi
index_status=$(curl -sS -o /tmp/p05_index_remote.html -w "%{http_code}" "$index_url")
if [ "$index_status" = "200" ]; then
if grep -q "ACDL Evidence" /tmp/p05_index_remote.html && grep -q "audit.json" /tmp/p05_index_remote.html; then
pass "raw index.html returns 200 with ACDL Evidence + audit.json reference"
else
fail "raw index.html returns 200 but missing ACDL Evidence / audit.json markers"
fi
else
fail "raw index.html GET returned HTTP ${index_status}"
fi
fi
echo
echo "== Summary =="
if [ "$fail_count" -eq 0 ]; then
echo "Phase 05 verification PASSED (UI + 4-act dry-run, all checks ok)"
exit 0
else
echo "Phase 05 verification FAILED (${fail_count} check(s) failed)"
exit 1
fi